diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 19f1fa9..e799b18 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -13,10 +13,9 @@ jobs: fail-fast: false matrix: include: - - php: 8.1 + - php: 8.2 composer: '--prefer-lowest' desc: "Lowest versions" - - php: 8.1 - php: 8.2 coverage: '--coverage-clover /tmp/clover.xml' - php: 8.3 @@ -47,4 +46,3 @@ jobs: if: ${{ matrix.coverage }} with: cli-args: "--format=php-clover build/logs/clover.xml --revision=${{ github.event.pull_request.head.sha || github.sha }}" - diff --git a/composer.json b/composer.json index 3f9b208..8051d7b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ } ], "require": { - "php": ">=8.1.0", + "php": ">=8.2.0", "improved/iterable": "^0.1.4", "jasny/immutable": "^2.1", "nesbot/carbon": "^2.27", @@ -29,17 +29,18 @@ "psr/log": "^1.1" }, "conflict": { - "hashids/hashids": "< 2.0", - "lcobucci/jwt": "< 3.4" + "hashids/hashids": "< 4.1", + "lcobucci/jwt": "< 4.0" }, "require-dev": { "ext-bcmath": "*", - "phpunit/phpunit": "^11.3", + "hashids/hashids": "^4.1 | ^5.0", "jasny/phpunit-extension": "^0.5.1", + "lcobucci/clock": "^3.2", + "lcobucci/jwt": "^4.0 | ^5.0", "phpstan/phpstan": "^1.12.0", - "squizlabs/php_codesniffer": "^3.10", - "hashids/hashids": "^2.0 | ^3.0 | ^4.0", - "lcobucci/jwt": "^3.4 | ^4.0" + "phpunit/phpunit": "^11.3", + "squizlabs/php_codesniffer": "^3.10" }, "config": { "optimize-autoloader": true, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fab9369..d2009b1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,5 @@ - - - - - tests/ diff --git a/src/Auth.php b/src/Auth.php index 9ea67cd..4bbd822 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -4,6 +4,9 @@ namespace Jasny\Auth; +use Closure; +use DateTimeImmutable; +use DateTimeInterface; use Jasny\Auth\AuthzInterface as Authz; use Jasny\Auth\Confirmation\ConfirmationInterface as Confirmation; use Jasny\Auth\Confirmation\NoConfirmation; @@ -14,9 +17,11 @@ use Jasny\Auth\User\PartiallyLoggedIn; use Jasny\Auth\UserInterface as User; use Jasny\Immutable; +use LogicException; use Psr\EventDispatcher\EventDispatcherInterface as EventDispatcher; use Psr\Log\LoggerInterface as Logger; use Psr\Log\NullLogger; +use RuntimeException; /** * Authentication and authorization. @@ -35,7 +40,7 @@ class Auth implements Authz /** * Time when logged in. */ - protected ?\DateTimeInterface $timestamp = null; + protected ?DateTimeInterface $timestamp = null; protected Session $session; protected Storage $storage; @@ -47,8 +52,8 @@ class Auth implements Authz /** Allow service to be re-initialized */ protected bool $forMultipleRequests = false; - /** @var \Closure&callable(User $user, string $code):bool */ - protected \Closure $verifyMfa; + /** @var Closure&callable(User $user, string $code):bool */ + protected Closure $verifyMfa; /** * Auth constructor. @@ -70,7 +75,7 @@ public function __construct(Authz $authz, Storage $storage, ?Confirmation $confi * * @return static */ - public function forMultipleRequests(): self + public function forMultipleRequests(): static { return $this->withProperty('forMultipleRequests', true); } @@ -78,7 +83,7 @@ public function forMultipleRequests(): self /** * Get a copy with an event dispatcher. */ - public function withEventDispatcher(EventDispatcher $dispatcher): self + public function withEventDispatcher(EventDispatcher $dispatcher): static { return $this->withProperty('dispatcher', $dispatcher); } @@ -86,7 +91,7 @@ public function withEventDispatcher(EventDispatcher $dispatcher): self /** * Get a copy with a logger. */ - public function withLogger(Logger $logger): self + public function withLogger(Logger $logger): static { return $this->withProperty('logger', $logger); } @@ -100,14 +105,14 @@ public function getLogger(): Logger } /** - * Get a copy of the service with Multi Factor Authentication (MFA) support. + * Get a copy of the service with Multi-Factor Authentication (MFA) support. * * @param callable $verify Callback to verify MFA. * @return static */ public function withMfa(callable $verify): self { - return $this->withProperty('verifyMfa', \Closure::fromCallable($verify)); + return $this->withProperty('verifyMfa', $verify(...)); } @@ -118,7 +123,7 @@ public function initialize(?Session $session = null): void { if ($this->isInitialized()) { if (!$this->forMultipleRequests) { - throw new \LogicException("Auth service is already initialized"); + throw new LogicException("Auth service is already initialized"); } $this->authz = $this->authz()->forUser(null)->inContextOf(null); @@ -133,7 +138,7 @@ public function initialize(?Session $session = null): void /** * Get user and context from session, loading objects from storage. * - * @return array{user:User|null,context:Context|null,timestamp:\DateTimeInterface|null} + * @return array{user:User|null,context:Context|null,timestamp:DateTimeInterface|null} */ protected function getInfoFromSession(): array { @@ -145,7 +150,7 @@ protected function getInfoFromSession(): array if ($uid === null || $uid instanceof User) { $user = $uid; } else { - if (substr($uid, 0, 9) === '#partial:') { + if (str_starts_with($uid, '#partial:')) { $partial = true; $uid = substr($uid, 9); } @@ -185,12 +190,12 @@ public function isInitialized(): bool /** * Throw an exception if the service hasn't been initialized yet. * - * @throws \LogicException + * @throws LogicException */ protected function assertInitialized(): void { if (!$this->isInitialized()) { - throw new \LogicException("Auth needs to be initialized before use"); + throw new LogicException("Auth needs to be initialized before use"); } } @@ -217,7 +222,7 @@ final public function isLoggedIn(): bool /** * Check if the current user is partially logged in. - * Typically this means MFA verification is required. + * Typically, this means MFA verification is required. */ final public function isPartiallyLoggedIn(): bool { @@ -274,7 +279,7 @@ final public function context(): ?Context /** * Get the login timestamp. */ - public function time(): ?\DateTimeInterface + public function time(): ?DateTimeInterface { return $this->timestamp; } @@ -290,7 +295,7 @@ public function loginAs(User $user): void $this->assertInitialized(); if ($this->authz->isLoggedIn()) { - throw new \LogicException("Already logged in"); + throw new LogicException("Already logged in"); } if ($user->requiresMfa()) { @@ -311,7 +316,7 @@ public function login(string $username, string $password): void $this->assertInitialized(); if ($this->authz->isLoggedIn()) { - throw new \LogicException("Already logged in"); + throw new LogicException("Already logged in"); } $user = $this->storage->fetchUserByUsername($username); @@ -339,8 +344,8 @@ public function login(string $username, string $password): void */ private function loginUser(User $user): void { - $event = new Event\Login($this, $user); - $this->dispatcher->dispatch($event); + /** @var Event\Login $event */ + $event = $this->dispatcher->dispatch(new Event\Login($this, $user)); if ($event->isCancelled()) { if ($this->isPartiallyLoggedIn()) { @@ -361,7 +366,7 @@ private function loginUser(User $user): void $this->authz = $this->authz->inContextOf($context); } - $this->timestamp = new \DateTimeImmutable(); + $this->timestamp = new DateTimeImmutable(); $this->updateSession(); $this->logger->info("Login successful", ['user' => $user->getAuthId()]); @@ -374,8 +379,8 @@ private function loginUser(User $user): void */ private function partialLoginUser(User $user): void { - $event = new Event\PartialLogin($this, $user); - $this->dispatcher->dispatch($event); + /** @var Event\PartialLogin $event */ + $event = $this->dispatcher->dispatch(new Event\PartialLogin($this, $user)); if ($event->isCancelled()) { $this->logger->info("Login failed: " . $event->getCancellationReason(), ['user' => $user->getAuthId()]); @@ -384,7 +389,7 @@ private function partialLoginUser(User $user): void // Beware; the `authz` property may have been changed via the partial login event. $this->authz = $this->authz->forUser(new PartiallyLoggedIn($user)); - $this->timestamp = new \DateTimeImmutable(); + $this->timestamp = new DateTimeImmutable(); $this->updateSession(); $this->logger->info("Partial login", ['user' => $user->getAuthId()]); @@ -398,7 +403,7 @@ public function mfa(string $code): void $this->assertInitialized(); if ($this->isLoggedOut()) { - throw new \RuntimeException("Unable to perform MFA verification: No user (partially) logged in"); + throw new RuntimeException("Unable to perform MFA verification: No user (partially) logged in"); } $authzUser = $this->user(); @@ -462,7 +467,7 @@ public function setContext(?Context $context): void * * @return $this */ - public function recalc(): self + public function recalc(): static { $this->authz = $this->authz->recalc(); $this->updateSession(); @@ -486,13 +491,12 @@ protected function updateSession(): void $context = $this->authz->context(); $uid = $user->getAuthId(); - $cid = $context !== null ? $context->getAuthId() : null; + $cid = $context?->getAuthId(); $checksum = $user->getAuthChecksum(); $this->session->persist($uid, $cid, $checksum, $this->timestamp); } - /** * Return read-only service for authorization of the current user and context. */ @@ -525,7 +529,6 @@ final public function outOfContext(): Authz return $this->inContextOf(null); } - /** * Get service to create or validate confirmation token. */ @@ -537,7 +540,6 @@ public function confirm(string $subject): Confirmation ->withSubject($subject); } - /** * Create an event dispatcher as null object. * @codeCoverageIgnore diff --git a/src/AuthException.php b/src/AuthException.php index b8dc5cc..3eda6e6 100644 --- a/src/AuthException.php +++ b/src/AuthException.php @@ -4,9 +4,11 @@ namespace Jasny\Auth; +use RuntimeException; + /** * Authentication exception. */ -class AuthException extends \RuntimeException +class AuthException extends RuntimeException { } diff --git a/src/AuthMiddleware.php b/src/AuthMiddleware.php index 5d21565..c28d5b6 100644 --- a/src/AuthMiddleware.php +++ b/src/AuthMiddleware.php @@ -4,15 +4,18 @@ namespace Jasny\Auth; +use Closure; use Improved as i; use Improved\IteratorPipeline\Pipeline; use Jasny\Auth\AuthzInterface as Authz; use Jasny\Auth\Session\SessionInterface; +use LogicException; use Psr\Http\Message\ServerRequestInterface as ServerRequest; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseFactoryInterface as ResponseFactory; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use Psr\Http\Server\MiddlewareInterface; +use UnexpectedValueException; /** * Middleware for access control. @@ -23,15 +26,15 @@ class AuthMiddleware implements MiddlewareInterface protected ?ResponseFactory $responseFactory = null; /** - * @var null|\Closure(ServerRequest $request):SessionInterface + * @var null|Closure(ServerRequest $request):SessionInterface */ - protected ?\Closure $getSession; + protected ?Closure $getSession; /** - * @var \Closure(ServerRequest $request):mixed + * @var Closure(ServerRequest $request):mixed * Function to get the required role from the request. */ - protected \Closure $getRequiredRole; + protected Closure $getRequiredRole; /** * Class constructor @@ -44,7 +47,7 @@ public function __construct(Authz $auth, callable $getRequiredRole, ?ResponseFac { $this->auth = $auth; $this->responseFactory = $responseFactory; - $this->getRequiredRole = \Closure::fromCallable($getRequiredRole); + $this->getRequiredRole = $getRequiredRole(...); } /** @@ -56,7 +59,7 @@ public function __construct(Authz $auth, callable $getRequiredRole, ?ResponseFac public function withSession(callable $getSession): self { $copy = clone $this; - $copy->getSession = \Closure::fromCallable($getSession); + $copy->getSession = $getSession(...); return $copy; } @@ -104,7 +107,7 @@ protected function initialize(ServerRequest $request): void { if (!$this->auth instanceof Auth) { if (isset($this->getSession)) { - throw new \LogicException("Session can't be used for immutable authz service"); + throw new LogicException("Session can't be used for immutable authz service"); } return; } @@ -125,7 +128,7 @@ protected function getSession(ServerRequest $request): ?SessionInterface return i\type_check( ($this->getSession)($request), SessionInterface::class, - new \UnexpectedValueException() + new UnexpectedValueException() ); } @@ -181,6 +184,6 @@ protected function createResponse(int $status, ?Response $originalResponse = nul return $originalResponse->withStatus($status)->withBody($body); } - throw new \LogicException('Response factory not set'); + throw new LogicException('Response factory not set'); } } diff --git a/src/Authz/Groups.php b/src/Authz/Groups.php index 755bfd5..0733240 100644 --- a/src/Authz/Groups.php +++ b/src/Authz/Groups.php @@ -69,11 +69,9 @@ public function __construct(array $groups) * Get a copy of the service with a modified property and recalculated * Returns $this if authz hasn't changed. * - * @param string $property - * @param string $value - * @return static + * Overwrites the `withProperty` method from the `Immutable\With` trait. */ - protected function withProperty(string $property, $value): self + protected function withProperty(string $property, mixed $value): static { $clone = clone $this; $clone->{$property} = $value; @@ -139,10 +137,8 @@ public function is(string $role): bool /** * Get a copy, recalculating the authz level of the user. * Returns $this if authz hasn't changed. - * - * @return static */ - public function recalc(): self + public function recalc(): static { $clone = clone $this; $clone->calcUserRoles(); diff --git a/src/Authz/Levels.php b/src/Authz/Levels.php index fb63dc3..999aefd 100644 --- a/src/Authz/Levels.php +++ b/src/Authz/Levels.php @@ -4,6 +4,7 @@ namespace Jasny\Auth\Authz; +use DomainException; use Improved as i; use Improved\IteratorPipeline\Pipeline; use Jasny\Auth\AuthzInterface; @@ -11,6 +12,7 @@ use Jasny\Auth\ContextInterface as Context; use Jasny\Auth\User\PartiallyLoggedIn; use Jasny\Auth\UserInterface as User; +use UnexpectedValueException; /** * Authorize by access level. @@ -54,12 +56,8 @@ public function __construct(array $levels) /** * Get a copy of the service with a modified property and recalculated * Returns $this if authz hasn't changed. - * - * @param string $property - * @param string $value - * @return static */ - protected function withProperty(string $property, $value): self + protected function withProperty(string $property, mixed $value): static { $clone = clone $this; $clone->{$property} = $value; @@ -99,10 +97,8 @@ public function is(string $role): bool /** * Get a copy, recalculating the authz level of the user. * Returns $this if authz hasn't changed. - * - * @return static */ - public function recalc(): self + public function recalc(): static { $clone = clone $this; $clone->calcUserLevel(); @@ -113,7 +109,7 @@ public function recalc(): self /** * Get access level of the current user. * - * @throws \DomainException for unknown level names + * @throws DomainException for unknown level names */ private function calcUserLevel(): void { @@ -127,11 +123,11 @@ private function calcUserLevel(): void $role = i\type_check( $this->user->getAuthRole($this->context), ['int', 'string'], - new \UnexpectedValueException("For authz levels the role should be string|int, %s returned (uid:$uid)") + new UnexpectedValueException("For authz levels the role should be string|int, %s returned (uid:$uid)") ); if (is_string($role) && !isset($this->levels[$role])) { - throw new \DomainException("Authorization level '$role' isn't defined (uid:$uid)"); + throw new DomainException("Authorization level '$role' isn't defined (uid:$uid)"); } $this->userLevel = is_string($role) ? $this->levels[$role] : (int)$role; diff --git a/src/Authz/StateTrait.php b/src/Authz/StateTrait.php index 0cd8096..fda10d4 100644 --- a/src/Authz/StateTrait.php +++ b/src/Authz/StateTrait.php @@ -31,8 +31,11 @@ trait StateTrait /** * Get a copy of the service for the given user. + * + * @param User|null $user + * @return static&Authz */ - public function forUser(?User $user): static + public function forUser(?User $user): Authz { return $this->withProperty('user', $user); } @@ -40,16 +43,21 @@ public function forUser(?User $user): static /** * Get a copy of the service for the given context. * Returns $this if authz hasn't changed. + * + * @param Context|null $context + * @return static&Authz */ - public function inContextOf(?Context $context): static + public function inContextOf(?Context $context): Authz { return $this->withProperty('context', $context); } /** * Alias of `inContextOf(null)`. + * + * @return static&Authz */ - final public function outOfContext(): static + final public function outOfContext(): Authz { return $this->inContextOf(null); } diff --git a/src/AuthzInterface.php b/src/AuthzInterface.php index eeef5c4..7aeb3da 100644 --- a/src/AuthzInterface.php +++ b/src/AuthzInterface.php @@ -38,8 +38,7 @@ public function outOfContext(): self; /** * Get a copy, recalculating the authz role of the user. */ - public function recalc(): self; - + public function recalc(): static; /** * Get current authenticated user. @@ -59,7 +58,7 @@ public function isLoggedIn(): bool; /** * Check if the current user is partially logged in. - * Typically this means MFA verification is required. + * Typically, this means MFA verification is required. */ public function isPartiallyLoggedIn(): bool; diff --git a/src/Confirmation/ConfirmationInterface.php b/src/Confirmation/ConfirmationInterface.php index 512eda0..a895527 100644 --- a/src/Confirmation/ConfirmationInterface.php +++ b/src/Confirmation/ConfirmationInterface.php @@ -4,6 +4,7 @@ namespace Jasny\Auth\Confirmation; +use DateTimeInterface; use Jasny\Auth\StorageInterface as Storage; use Jasny\Auth\UserInterface as User; use Psr\Log\LoggerInterface as Logger; @@ -40,7 +41,7 @@ public function withSubject(string $subject): self; /** * Generate a confirmation token. */ - public function getToken(User $user, \DateTimeInterface $expire): string; + public function getToken(User $user, DateTimeInterface $expire): string; /** * Get user by confirmation token. diff --git a/src/Confirmation/HashidsConfirmation.php b/src/Confirmation/HashidsConfirmation.php index 5613bf3..68d8f7c 100644 --- a/src/Confirmation/HashidsConfirmation.php +++ b/src/Confirmation/HashidsConfirmation.php @@ -5,12 +5,16 @@ namespace Jasny\Auth\Confirmation; use Carbon\CarbonImmutable; +use Closure; +use DateTimeInterface; +use Exception; use Hashids\Hashids; use Jasny\Auth\UserInterface as User; use Jasny\Auth\StorageInterface as Storage; use Jasny\Immutable; use Psr\Log\LoggerInterface as Logger; use Psr\Log\NullLogger; +use RuntimeException; /** * Generate and verify confirmation tokens using the Hashids library. @@ -24,13 +28,13 @@ class HashidsConfirmation implements ConfirmationInterface protected string $subject; protected string $secret; - protected \Closure $createHashids; + protected Closure $createHashids; protected Storage $storage; - /** @phpstan-var \Closure&callable(string $uid):(string|false) */ - protected \Closure $encodeUid; - /** @phpstan-var \Closure&callable(string $uid):(string|false) */ - protected \Closure $decodeUid; + /** @phpstan-var Closure&callable(string $uid):(string|false) */ + protected Closure $encodeUid; + /** @phpstan-var Closure&callable(string $uid):(string|false) */ + protected Closure $decodeUid; protected Logger $logger; @@ -45,7 +49,7 @@ public function __construct(string $secret, ?callable $createHashids = null) $this->secret = $secret; $this->createHashids = $createHashids !== null - ? \Closure::fromCallable($createHashids) + ? $createHashids(...) : fn(string $salt) => new Hashids($salt); $this->encodeUid = function (string $uid) { @@ -70,36 +74,26 @@ public function withStorage(Storage $storage): self /** * Get a copy with custom methods to encode/decode the uid. - * - * @param callable $encode - * @param callable $decode - * @return static */ - public function withUidEncoded(callable $encode, callable $decode): self + public function withUidEncoded(callable $encode, callable $decode): static { return $this - ->withProperty('encodeUid', \Closure::fromCallable($encode)) - ->withProperty('decodeUid', \Closure::fromCallable($decode)); + ->withProperty('encodeUid', $encode(...)) + ->withProperty('decodeUid', $decode(...)); } /** * Get copy with logger. - * - * @param Logger $logger - * @return static */ - public function withLogger(Logger $logger): self + public function withLogger(Logger $logger): static { return $this->withProperty('logger', $logger); } /** * Create a copy of this service with a specific subject. - * - * @param string $subject - * @return static */ - public function withSubject(string $subject): self + public function withSubject(string $subject): static { return $this->withProperty('subject', $subject); } @@ -108,7 +102,7 @@ public function withSubject(string $subject): self /** * Generate a confirmation token. */ - public function getToken(User $user, \DateTimeInterface $expire): string + public function getToken(User $user, DateTimeInterface $expire): string { $uidHex = $this->encodeUid($user->getAuthId()); $expireHex = CarbonImmutable::instance($expire)->utc()->format('YmdHis'); @@ -137,7 +131,6 @@ public function from(string $token): User throw new InvalidTokenException("Invalid confirmation token"); } - /** @var CarbonImmutable $expire */ ['checksum' => $checksum, 'expire' => $expire, 'uid' => $uid] = $info; $context += ['user' => $uid, 'expire' => $expire->format('c')]; @@ -172,7 +165,7 @@ protected function extractHex(string $hex): ?array /** @var CarbonImmutable $expire */ $expire = CarbonImmutable::createFromFormat('YmdHis', $expireHex, '+00:00'); - } catch (\Exception $exception) { + } catch (Exception $exception) { return null; } @@ -185,16 +178,13 @@ protected function extractHex(string $hex): ?array /** * Encode the uid to a hex value. - * - * @param string $uid - * @return string */ protected function encodeUid(string $uid): string { $hex = ($this->encodeUid)($uid); if ($hex === false) { - throw new \RuntimeException("Failed to encode uid"); + throw new RuntimeException("Failed to encode uid"); } return $hex; @@ -202,16 +192,13 @@ protected function encodeUid(string $uid): string /** * Decode the uid to a hex value. - * - * @param string $hex - * @return string */ protected function decodeUid(string $hex): string { $uid = ($this->decodeUid)($hex); if ($uid === false) { - throw new \RuntimeException("Failed to decode uid"); + throw new RuntimeException("Failed to decode uid"); } return $uid; @@ -279,7 +266,7 @@ protected function verifyNotExpired(CarbonImmutable $expire, array $context): vo /** * Calculate confirmation checksum. */ - protected function calcChecksum(User $user, \DateTimeInterface $expire): string + protected function calcChecksum(User $user, DateTimeInterface $expire): string { $parts = [ CarbonImmutable::instance($expire)->utc()->format('YmdHis'), @@ -302,9 +289,6 @@ public function createHashids(): Hashids /** * Create a partial token for logging. - * - * @param string $token - * @return string */ protected static function partialToken(string $token): string { diff --git a/src/Confirmation/InvalidTokenException.php b/src/Confirmation/InvalidTokenException.php index 6bfb5f7..1a6d225 100644 --- a/src/Confirmation/InvalidTokenException.php +++ b/src/Confirmation/InvalidTokenException.php @@ -4,9 +4,11 @@ namespace Jasny\Auth\Confirmation; +use RuntimeException; + /** * Exception thrown if the confirmation token isn't valid or is expired. */ -class InvalidTokenException extends \RuntimeException +class InvalidTokenException extends RuntimeException { } diff --git a/src/Confirmation/NoConfirmation.php b/src/Confirmation/NoConfirmation.php index 17fca65..05f5fce 100644 --- a/src/Confirmation/NoConfirmation.php +++ b/src/Confirmation/NoConfirmation.php @@ -4,6 +4,7 @@ namespace Jasny\Auth\Confirmation; +use DateTimeInterface; use Jasny\Auth\StorageInterface as Storage; use Jasny\Auth\UserInterface as User; use LogicException; @@ -43,7 +44,7 @@ public function withSubject(string $subject): static * * @throws LogicException */ - public function getToken(User $user, \DateTimeInterface $expire): string + public function getToken(User $user, DateTimeInterface $expire): string { throw new LogicException("Confirmation tokens are not supported"); } diff --git a/src/Confirmation/TokenConfirmation.php b/src/Confirmation/TokenConfirmation.php index ddf8394..ff1ceeb 100644 --- a/src/Confirmation/TokenConfirmation.php +++ b/src/Confirmation/TokenConfirmation.php @@ -4,6 +4,11 @@ namespace Jasny\Auth\Confirmation; +use BadMethodCallException; +use Closure; +use DateTime; +use DateTimeInterface; +use InvalidArgumentException; use Jasny\Auth\Storage\TokenStorageInterface; use Jasny\Auth\StorageInterface as Storage; use Jasny\Auth\UserInterface as User; @@ -22,7 +27,7 @@ class TokenConfirmation implements ConfirmationInterface /** @var int<1, max> */ protected int $numberOfBytes; - protected \Closure $encode; + protected Closure $encode; protected TokenStorageInterface $storage; protected Logger $logger; @@ -38,7 +43,7 @@ class TokenConfirmation implements ConfirmationInterface public function __construct(int $numberOfBytes = 16, ?callable $encode = null) { if ($numberOfBytes < 1) { - throw new \InvalidArgumentException("Number of bytes must be at least 1"); + throw new InvalidArgumentException("Number of bytes must be at least 1"); } $this->numberOfBytes = $numberOfBytes; @@ -56,7 +61,7 @@ public function __construct(int $numberOfBytes = 16, ?callable $encode = null) public function withStorage(Storage $storage): static { if (!$storage instanceof TokenStorageInterface) { - throw new \InvalidArgumentException("Storage object needs to implement " . TokenStorageInterface::class); + throw new InvalidArgumentException("Storage object needs to implement " . TokenStorageInterface::class); } return $this->withProperty('storage', $storage); @@ -81,10 +86,10 @@ public function withSubject(string $subject): static /** * @inheritDoc */ - public function getToken(User $user, \DateTimeInterface $expire): string + public function getToken(User $user, DateTimeInterface $expire): string { if (!isset($this->storage)) { - throw new \BadMethodCallException("Storage is not set"); + throw new BadMethodCallException("Storage is not set"); } $rawToken = random_bytes($this->numberOfBytes); @@ -101,7 +106,7 @@ public function getToken(User $user, \DateTimeInterface $expire): string public function from(string $token): User { if (!isset($this->storage)) { - throw new \BadMethodCallException("Storage is not set"); + throw new BadMethodCallException("Storage is not set"); } $info = $this->storage->fetchToken($this->subject, $token); @@ -113,7 +118,7 @@ public function from(string $token): User ['uid' => $uid, 'expire' => $expire] = $info; - if ($expire <= new \DateTime()) { + if ($expire <= new DateTime()) { $this->logger->debug('Expired confirmation token', ['token' => $token, 'uid' => $uid]); throw new InvalidTokenException("Token is expired"); } diff --git a/src/Session/BearerAuth.php b/src/Session/BearerAuth.php index 7a5858b..59d0b0c 100644 --- a/src/Session/BearerAuth.php +++ b/src/Session/BearerAuth.php @@ -4,6 +4,8 @@ namespace Jasny\Auth\Session; +use DateTimeInterface; +use LogicException; use Psr\Http\Message\ServerRequestInterface; /** @@ -42,18 +44,18 @@ public function getInfo(): array /** * @inheritDoc - * @throws \LogicException Since bearer authorization can't be modified server side. + * @throws LogicException Since bearer authorization can't be modified server side. */ - public function persist($userId, $contextId, ?string $checksum, ?\DateTimeInterface $timestamp): void + public function persist(mixed $userId, mixed $contextId, ?string $checksum, ?DateTimeInterface $timestamp): void { - throw new \LogicException("Unable to persist auth info when using bearer authorization"); + throw new LogicException("Unable to persist auth info when using bearer authorization"); } /** - * @throws \LogicException Since bearer authorization can't be modified server side. + * @throws LogicException Since bearer authorization can't be modified server side. */ public function clear(): void { - throw new \LogicException("Unable to persist auth info when using bearer authorization"); + throw new LogicException("Unable to persist auth info when using bearer authorization"); } } diff --git a/src/Session/GetInfoTrait.php b/src/Session/GetInfoTrait.php index 317ebea..3c71595 100644 --- a/src/Session/GetInfoTrait.php +++ b/src/Session/GetInfoTrait.php @@ -4,24 +4,29 @@ namespace Jasny\Auth\Session; +use ArrayAccess; +use DateTimeImmutable; +use DateTimeInterface; +use Throwable; + /** * Get to get info from data. */ trait GetInfoTrait { /** - * @param array|\ArrayAccess $data - * @return array{user:mixed,context:mixed,checksum:string|null,timestamp:\DateTimeInterface|null} + * @param ArrayAccess|array $data + * @return array{user:mixed,context:mixed,checksum:string|null,timestamp:DateTimeInterface|null} */ - private function getInfoFromData($data): array + private function getInfoFromData(array|ArrayAccess $data): array { $timestamp = $data['timestamp'] ?? null; try { - if ($timestamp !== null && !($timestamp instanceof \DateTimeInterface)) { - $timestamp = new \DateTimeImmutable('@' . $data['timestamp']); + if ($timestamp !== null && !($timestamp instanceof DateTimeInterface)) { + $timestamp = new DateTimeImmutable('@' . $data['timestamp']); } - } catch (\Throwable $exception) { + } catch (Throwable $exception) { trigger_error($exception->getMessage(), E_USER_WARNING); $timestamp = null; } diff --git a/src/Session/Jwt.php b/src/Session/Jwt.php index e982681..48b2441 100644 --- a/src/Session/Jwt.php +++ b/src/Session/Jwt.php @@ -4,7 +4,11 @@ namespace Jasny\Auth\Session; +use DateInterval; +use DateTime; use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; use Jasny\Auth\Session\Jwt\Cookie; use Jasny\Auth\Session\Jwt\CookieInterface; use Jasny\Immutable; @@ -73,7 +77,7 @@ public function getInfo(): array $timestamp = $token->headers()->get('iat'); if (is_array($timestamp)) { - $timestamp = new \DateTimeImmutable($timestamp['date'], new \DateTimeZone($timestamp['timezone'])); + $timestamp = new DateTimeImmutable($timestamp['date'], new DateTimeZone($timestamp['timezone'])); } return [ @@ -87,18 +91,16 @@ public function getInfo(): array /** * @inheritDoc */ - public function persist($userId, $contextId, ?string $checksum, ?\DateTimeInterface $timestamp): void + public function persist(mixed $userId, mixed $contextId, ?string $checksum, ?DateTimeInterface $timestamp): void { - $builder = clone $this->jwtConfig->builder(); - - if ($timestamp instanceof \DateTime) { - $timestamp = \DateTimeImmutable::createFromMutable($timestamp); + if ($timestamp instanceof DateTime) { + $timestamp = DateTimeImmutable::createFromMutable($timestamp); } /** @var DateTimeImmutable|null $timestamp */ - $time = $timestamp ?? new \DateTimeImmutable(); - $expire = $time->add(new \DateInterval("PT{$this->ttl}S")); + $time = $timestamp ?? new DateTimeImmutable(); + $expire = $time->add(new DateInterval("PT{$this->ttl}S")); - $builder + $builder = clone $this->jwtConfig->builder() ->withClaim('user', $userId) ->withClaim('context', $contextId) ->withClaim('checksum', $checksum) @@ -106,7 +108,7 @@ public function persist($userId, $contextId, ?string $checksum, ?\DateTimeInterf ->expiresAt($expire); if ($timestamp !== null) { - $builder->withHeader('iat', $timestamp); + $builder = $builder->withHeader('iat', $timestamp); } $this->cookie->set( diff --git a/src/Session/Jwt/Cookie.php b/src/Session/Jwt/Cookie.php index d0677fe..c062433 100644 --- a/src/Session/Jwt/Cookie.php +++ b/src/Session/Jwt/Cookie.php @@ -4,6 +4,8 @@ namespace Jasny\Auth\Session\Jwt; +use RuntimeException; + /** * Use global `$_COOKIE` and `setcookie()` for the JWT cookie. * @@ -47,7 +49,7 @@ public function set(string $value, int $expire): void $success = setcookie($this->name, $value, ['expire' => $expire] + $this->options); if (!$success) { - throw new \RuntimeException("Failed to set cookie '{$this->name}'"); + throw new RuntimeException("Failed to set cookie '{$this->name}'"); } $_COOKIE[$this->name] = $value; @@ -61,7 +63,7 @@ public function clear(): void $success = setcookie($this->name, '', ['expire' => 1] + $this->options); if (!$success) { - throw new \RuntimeException("Failed to clear cookie '{$this->name}'"); + throw new RuntimeException("Failed to clear cookie '{$this->name}'"); } unset($_COOKIE[$this->name]); diff --git a/src/Session/PhpSession.php b/src/Session/PhpSession.php index 368d092..554ca9f 100644 --- a/src/Session/PhpSession.php +++ b/src/Session/PhpSession.php @@ -4,6 +4,10 @@ namespace Jasny\Auth\Session; +use DateTimeInterface; +use RuntimeException; +use const PHP_SESSION_ACTIVE; + /** * Use PHP sessions to store auth session info. */ @@ -26,12 +30,12 @@ public function __construct(string $key = 'auth') /** * Assert that there is an active session. * - * @throws \RuntimeException if there is no active session + * @throws RuntimeException if there is no active session */ protected function assertSessionStarted(): void { - if (session_status() !== \PHP_SESSION_ACTIVE) { - throw new \RuntimeException("Unable to use session for auth info: Session not started"); + if (session_status() !== PHP_SESSION_ACTIVE) { + throw new RuntimeException("Unable to use session for auth info: Session not started"); } } @@ -48,7 +52,7 @@ public function getInfo(): array /** * @inheritDoc */ - public function persist($userId, $contextId, ?string $checksum, ?\DateTimeInterface $timestamp): void + public function persist(mixed $userId, mixed $contextId, ?string $checksum, ?DateTimeInterface $timestamp): void { $this->assertSessionStarted(); @@ -56,7 +60,7 @@ public function persist($userId, $contextId, ?string $checksum, ?\DateTimeInterf 'user' => $userId, 'context' => $contextId, 'checksum' => $checksum, - 'timestamp' => isset($timestamp) ? $timestamp->getTimestamp() : null, + 'timestamp' => $timestamp?->getTimestamp(), ]; } diff --git a/src/Session/SessionInterface.php b/src/Session/SessionInterface.php index b234354..83b8ed8 100644 --- a/src/Session/SessionInterface.php +++ b/src/Session/SessionInterface.php @@ -4,6 +4,8 @@ namespace Jasny\Auth\Session; +use DateTimeInterface; + /** * Session service for authorization. */ @@ -12,7 +14,7 @@ interface SessionInterface /** * Get auth information from session. * - * @return array{user:mixed,context:mixed,checksum:string|null,timestamp:\DateTimeInterface|null} + * @return array{user:mixed,context:mixed,checksum:string|null,timestamp:DateTimeInterface|null} */ public function getInfo(): array; @@ -22,9 +24,9 @@ public function getInfo(): array; * @param mixed $userId * @param mixed $contextId * @param string|null $checksum - * @param \DateTimeInterface|null $timestamp + * @param DateTimeInterface|null $timestamp */ - public function persist($userId, $contextId, ?string $checksum, ?\DateTimeInterface $timestamp): void; + public function persist(mixed $userId, mixed $contextId, ?string $checksum, ?DateTimeInterface $timestamp): void; /** * Remove auth information from session. diff --git a/src/Session/SessionObject.php b/src/Session/SessionObject.php index ee7d4c4..ff1a9d8 100644 --- a/src/Session/SessionObject.php +++ b/src/Session/SessionObject.php @@ -4,6 +4,8 @@ namespace Jasny\Auth\Session; +use ArrayAccess; +use DateTimeInterface; use Psr\Http\Message\ServerRequestInterface; /** @@ -15,16 +17,16 @@ class SessionObject implements SessionInterface protected string $key; - /** @var \ArrayAccess */ - protected \ArrayAccess $session; + /** @var ArrayAccess */ + protected ArrayAccess $session; /** * Service constructor. * - * @param \ArrayAccess $session + * @param ArrayAccess $session * @param string $key */ - public function __construct(\ArrayAccess $session, string $key = 'auth') + public function __construct(ArrayAccess $session, string $key = 'auth') { $this->session = $session; $this->key = $key; @@ -33,13 +35,14 @@ public function __construct(\ArrayAccess $session, string $key = 'auth') /** * Use the `session` attribute if it's an object that implements ArrayAccess. * + * @param ServerRequestInterface $request * @return self */ public function forRequest(ServerRequestInterface $request): self { $session = $request->getAttribute('session'); - if (!$session instanceof \ArrayAccess) { + if (!$session instanceof ArrayAccess) { return $this; } @@ -61,13 +64,13 @@ public function getInfo(): array /** * @inheritDoc */ - public function persist($userId, $contextId, ?string $checksum, ?\DateTimeInterface $timestamp): void + public function persist(mixed $userId, mixed $contextId, ?string $checksum, ?DateTimeInterface $timestamp): void { $this->session[$this->key] = [ 'user' => $userId, 'context' => $contextId, 'checksum' => $checksum, - 'timestamp' => isset($timestamp) ? $timestamp->getTimestamp() : null, + 'timestamp' => $timestamp?->getTimestamp(), ]; } diff --git a/src/Storage/TokenStorageInterface.php b/src/Storage/TokenStorageInterface.php index 285a567..ed479f9 100644 --- a/src/Storage/TokenStorageInterface.php +++ b/src/Storage/TokenStorageInterface.php @@ -4,6 +4,7 @@ namespace Jasny\Auth\Storage; +use DateTimeInterface; use Jasny\Auth\StorageInterface; use Jasny\Auth\UserInterface; @@ -15,12 +16,12 @@ interface TokenStorageInterface extends StorageInterface /** * Save a confirmation token to the database. */ - public function saveToken(string $subject, string $token, UserInterface $user, \DateTimeInterface $expire): void; + public function saveToken(string $subject, string $token, UserInterface $user, DateTimeInterface $expire): void; /** * Fetch a user by a confirmation token. * - * @phpstan-return array{uid:string,expire:\DateTimeInterface}|null + * @phpstan-return array{uid:string,expire:DateTimeInterface}|null */ public function fetchToken(string $subject, string $token): ?array; } diff --git a/src/User/BasicUser.php b/src/User/BasicUser.php index 14c7159..bc1ff33 100644 --- a/src/User/BasicUser.php +++ b/src/User/BasicUser.php @@ -4,21 +4,19 @@ namespace Jasny\Auth\User; +use AllowDynamicProperties; use Jasny\Auth\ContextInterface as Context; use Jasny\Auth\UserInterface; /** - * A simple user class which can be used be used instead of creating a custom user class. + * A simple user class which can be used instead of creating a custom user class. */ +#[AllowDynamicProperties] final class BasicUser implements UserInterface { - /** @var string|int */ - public $id; - + public string|int $id; protected string $hashedPassword = ''; - - /** @var string|int */ - public $role; + public string|int $role; /** * @inheritDoc @@ -47,7 +45,7 @@ public function getAuthChecksum(): string /** * @inheritDoc */ - public function getAuthRole(?Context $context = null) + public function getAuthRole(?Context $context = null): string|int { return $this->role; } @@ -63,8 +61,8 @@ public function requiresMfa(): bool /** * Factory method; create object from data loaded from DB. * - * @phpstan-param array $data - * @phpstan-return self + * @param array $data + * @return self */ public static function fromData(array $data): self { diff --git a/src/User/PartiallyLoggedIn.php b/src/User/PartiallyLoggedIn.php index 67c5e9f..88217d0 100644 --- a/src/User/PartiallyLoggedIn.php +++ b/src/User/PartiallyLoggedIn.php @@ -61,7 +61,7 @@ public function getAuthChecksum(): string /** * @inheritDoc */ - public function getAuthRole(?ContextInterface $context = null) + public function getAuthRole(?ContextInterface $context = null): int|array|string { return $this->user->getAuthRole($context); } diff --git a/src/UserInterface.php b/src/UserInterface.php index 1aff62e..2c906cb 100644 --- a/src/UserInterface.php +++ b/src/UserInterface.php @@ -35,10 +35,10 @@ public function getAuthChecksum(): string; * @param Context|null $context * @return int|string|int[]|string[] */ - public function getAuthRole(?Context $context = null); + public function getAuthRole(?Context $context = null): array|int|string; /** - * User requires Multi Factor Authentication. + * User requires Multi-Factor Authentication. */ public function requiresMfa(): bool; } diff --git a/tests/AuthMiddlewareDoublePassTest.php b/tests/AuthMiddlewareDoublePassTest.php index bd06e05..e1c32f9 100644 --- a/tests/AuthMiddlewareDoublePassTest.php +++ b/tests/AuthMiddlewareDoublePassTest.php @@ -6,15 +6,14 @@ use Jasny\Auth\AuthzInterface as Authz; use Jasny\Auth\AuthMiddleware; use Jasny\PHPUnit\CallbackMockTrait; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface as ServerRequest; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\StreamInterface as Stream; -/** - * @covers \Jasny\Auth\AuthMiddleware - */ +#[CoversClass(AuthMiddleware::class)] class AuthMiddlewareDoublePassTest extends TestCase { use CallbackMockTrait; diff --git a/tests/Authz/GroupsTest.php b/tests/Authz/GroupsTest.php index 68db4ed..6a8577c 100644 --- a/tests/Authz/GroupsTest.php +++ b/tests/Authz/GroupsTest.php @@ -7,11 +7,11 @@ use Jasny\Auth\Authz\Groups; use Jasny\PHPUnit\ExpectWarningTrait; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\MockObject\MockObject; -#[CoversClass(StateTrait::class)] +#[CoversTrait(StateTrait::class)] #[CoversClass(Groups::class)] class GroupsTest extends TestCase { @@ -139,7 +139,7 @@ public function testRecalcWithoutUser() } - public function crossReferenceProvider() + public static function crossReferenceProvider(): array { return [ 'client' => ['client'], @@ -148,9 +148,7 @@ public function crossReferenceProvider() ]; } - /** - * @dataProvider crossReferenceProvider - */ + #[DataProvider('crossReferenceProvider')] public function testCrossReference(string $role) { $this->authz = new Groups([ diff --git a/tests/Authz/LevelsTest.php b/tests/Authz/LevelsTest.php index 9c29373..127f5a3 100644 --- a/tests/Authz/LevelsTest.php +++ b/tests/Authz/LevelsTest.php @@ -12,13 +12,14 @@ use Jasny\Auth\Authz\Levels; use Jasny\PHPUnit\ExpectWarningTrait; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\TestCase; use UnexpectedValueException; #[CoversClass(Levels::class)] -#[CoversClass(StateTrait::class)] +#[CoversTrait(StateTrait::class)] class LevelsTest extends TestCase { use ExpectWarningTrait; diff --git a/tests/Event/LoginTest.php b/tests/Event/LoginTest.php index ab9ada9..f1b5361 100644 --- a/tests/Event/LoginTest.php +++ b/tests/Event/LoginTest.php @@ -10,11 +10,12 @@ use Jasny\Auth\Event\Login; use Jasny\Auth\UserInterface as User; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\TestCase; #[CoversClass(Login::class)] #[CoversClass(AbstractEvent::class)] -#[CoversClass(CancellableTrait::class)] +#[CoversTrait(CancellableTrait::class)] class LoginTest extends TestCase { public function testGetters() diff --git a/tests/Session/JwtTest.php b/tests/Session/JwtTest.php index 7adf9c7..8b42d24 100644 --- a/tests/Session/JwtTest.php +++ b/tests/Session/JwtTest.php @@ -8,7 +8,9 @@ use DateTimeImmutable; use Lcobucci\Clock\FrozenClock; use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Validation\Constraint; +use Lcobucci\JWT\Signer\Hmac\Sha256; use Jasny\Auth\Session\Jwt; use Jasny\Auth\Session\Jwt\CookieValue; use PHPUnit\Framework\Attributes\CoversClass; @@ -23,19 +25,13 @@ class JwtTest extends TestCase public function setUp(): void { - $this->jwtConfig = Configuration::forUnsecuredSigner(); - - $constraint = class_exists(Constraint\LooseValidAt::class) - // V4 - ? new Constraint\LooseValidAt( - new FrozenClock(new DateTimeImmutable('2020-01-02T00:00:00+00:00')), - new DateInterval('PT30S') - ) - // V3 - : new Constraint\ValidAt( - new FrozenClock(new DateTimeImmutable('2020-01-02T00:00:00+00:00')), - new DateInterval('PT30S') - ); + $key = InMemory::base64Encoded('hiG8DlOKvtih6AxlZn5XKImZ06yu8I3mkOzaJrEuW8yAv8Jnkw330uMt8AEqQ5LB'); + $this->jwtConfig = Configuration::forSymmetricSigner(new Sha256(), $key); + + $constraint = new Constraint\LooseValidAt( + new FrozenClock(new DateTimeImmutable('2020-01-02T00:00:00+00:00')), + new DateInterval('PT30S') + ); $this->jwtConfig->setValidationConstraints($constraint); $this->jwt = (new Jwt($this->jwtConfig)) @@ -54,9 +50,9 @@ public function testGetInfo() ->getToken($this->jwtConfig->signer(), $this->jwtConfig->signingKey()); $cookie = new CookieValue($token->toString()); - $this->jwt = $this->jwt->withCookie($cookie); + $jwt = $this->jwt->withCookie($cookie); - $info = $this->jwt->getInfo(); + $info = $jwt->getInfo(); $expected = [ 'user' => 'abc', @@ -78,9 +74,9 @@ public function testGetInfoWithoutTimestamp() $cookie = new CookieValue($token->toString()); - $this->jwt = $this->jwt->withCookie($cookie); + $jwt = $this->jwt->withCookie($cookie); - $info = $this->jwt->getInfo(); + $info = $jwt->getInfo(); $expected = [ 'user' => 'abc', @@ -103,9 +99,9 @@ public function testGetInfoWithExpiredToken() ->getToken($this->jwtConfig->signer(), $this->jwtConfig->signingKey()); $cookie = new CookieValue($token->toString()); - $this->jwt = $this->jwt->withCookie($cookie); + $jwt = $this->jwt->withCookie($cookie); - $info = $this->jwt->getInfo(); + $info = $jwt->getInfo(); $this->assertEquals( ['user' => null, 'context' => null, 'checksum' => null, 'timestamp' => null],