From 965c39b393e3aab40ba30586599c2b662da5deb4 Mon Sep 17 00:00:00 2001 From: Claudiu Date: Fri, 24 Apr 2026 09:28:16 +0300 Subject: [PATCH] upgraded mezzio-oauth2 to version 3 Signed-off-by: Claudiu --- bin/composer-post-install-script.php | 2 +- composer.json | 2 +- src/Admin/src/Service/AdminService.php | 2 +- src/Core/src/Admin/src/Entity/Admin.php | 18 ++++---- .../App/src/Entity/NumericIdentifierTrait.php | 4 +- .../Security/src/Entity/OAuthAccessToken.php | 46 ++++++++----------- .../src/Security/src/Entity/OAuthAuthCode.php | 20 +++----- .../src/Security/src/Entity/OAuthClient.php | 14 ++++-- .../Security/src/Entity/OAuthRefreshToken.php | 11 ++--- .../src/Security/src/Entity/OAuthScope.php | 6 ++- .../Repository/OAuthAccessTokenRepository.php | 13 ++---- .../Repository/OAuthAuthCodeRepository.php | 10 +--- .../src/Repository/OAuthClientRepository.php | 22 +++------ .../OAuthRefreshTokenRepository.php | 10 +--- .../src/Repository/OAuthScopeRepository.php | 10 ++-- src/Core/src/User/src/Entity/User.php | 11 ++--- .../User/src/Repository/UserRepository.php | 11 ++--- src/User/src/Service/UserService.php | 2 +- test/Functional/AuthenticationTest.php | 5 +- 19 files changed, 87 insertions(+), 132 deletions(-) diff --git a/bin/composer-post-install-script.php b/bin/composer-post-install-script.php index 20671c9..ad9a835 100644 --- a/bin/composer-post-install-script.php +++ b/bin/composer-post-install-script.php @@ -54,7 +54,7 @@ function getEnvironment(): string ], [ 'source' => 'vendor/dotkernel/dot-mail/config/mail.global.php.dist', - 'destination' => 'config/autoload/mail.global.php', + 'destination' => 'config/autoload/mail.local.php', 'environment' => [ENVIRONMENT_DEVELOPMENT, ENVIRONMENT_PRODUCTION], ], ]; diff --git a/composer.json b/composer.json index 18d135b..00e63e3 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "laminas/laminas-inputfilter": "^2.31.0", "laminas/laminas-stdlib": "^3.20.0", "mezzio/mezzio": "^3.20.1", - "mezzio/mezzio-authentication-oauth2": "^2.11.0", + "mezzio/mezzio-authentication-oauth2": "^3.0.1", "mezzio/mezzio-authorization-acl": "^1.11.0", "mezzio/mezzio-authorization-rbac": "^1.8.0", "mezzio/mezzio-cors": "^1.13.0", diff --git a/src/Admin/src/Service/AdminService.php b/src/Admin/src/Service/AdminService.php index 24b33cc..f58953d 100644 --- a/src/Admin/src/Service/AdminService.php +++ b/src/Admin/src/Service/AdminService.php @@ -93,7 +93,7 @@ public function saveAdmin(array $data, ?Admin $admin = null): Admin $admin = new Admin(); } - if (array_key_exists('identity', $data) && $data['identity'] !== null && ! $admin->hasIdentity()) { + if (array_key_exists('identity', $data) && $data['identity'] !== null) { $admin->setIdentity($data['identity']); } if (array_key_exists('password', $data) && $data['password'] !== null) { diff --git a/src/Core/src/Admin/src/Entity/Admin.php b/src/Core/src/Admin/src/Entity/Admin.php index d24326a..9e5b277 100644 --- a/src/Core/src/Admin/src/Entity/Admin.php +++ b/src/Core/src/Admin/src/Entity/Admin.php @@ -32,9 +32,9 @@ class Admin extends AbstractEntity implements UserEntityInterface use TimestampsTrait; use UuidIdentifierTrait; - /** @var non-empty-string|null $identity */ + /** @var non-empty-string $identity */ #[ORM\Column(name: 'identity', type: 'string', length: 191, unique: true)] - protected ?string $identity = null; + protected string $identity; #[ORM\Column(name: 'firstName', type: 'string', length: 191, nullable: true)] protected ?string $firstName = null; @@ -43,7 +43,7 @@ class Admin extends AbstractEntity implements UserEntityInterface protected ?string $lastName = null; #[ORM\Column(name: 'password', type: 'string', length: 191)] - protected ?string $password = null; + protected string $password; #[ORM\Column( type: 'admin_status_enum', @@ -76,11 +76,6 @@ public function getIdentity(): ?string return $this->identity; } - public function hasIdentity(): bool - { - return $this->identity !== null; - } - /** * @param non-empty-string $identity */ @@ -115,7 +110,7 @@ public function setLastName(string $lastName): self return $this; } - public function getPassword(): ?string + public function getPassword(): string { return $this->password; } @@ -213,9 +208,12 @@ public function isActive(): bool return $this->status === AdminStatusEnum::Active; } + /** + * @return non-empty-string + */ public function getIdentifier(): string { - return (string) $this->identity; + return $this->identity; } /** diff --git a/src/Core/src/App/src/Entity/NumericIdentifierTrait.php b/src/Core/src/App/src/Entity/NumericIdentifierTrait.php index 54fb41a..6383bbc 100644 --- a/src/Core/src/App/src/Entity/NumericIdentifierTrait.php +++ b/src/Core/src/App/src/Entity/NumericIdentifierTrait.php @@ -11,9 +11,9 @@ trait NumericIdentifierTrait #[ORM\Id] #[ORM\Column(name: 'id', type: 'integer', options: ['unsigned' => true])] #[ORM\GeneratedValue(strategy: 'AUTO')] - protected ?int $id; + protected int $id; - public function getId(): ?int + public function getId(): int { return $this->id; } diff --git a/src/Core/src/Security/src/Entity/OAuthAccessToken.php b/src/Core/src/Security/src/Entity/OAuthAccessToken.php index db7af22..bda2994 100644 --- a/src/Core/src/Security/src/Entity/OAuthAccessToken.php +++ b/src/Core/src/Security/src/Entity/OAuthAccessToken.php @@ -15,7 +15,7 @@ use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; -use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; @@ -33,8 +33,9 @@ class OAuthAccessToken implements AccessTokenEntityInterface #[ORM\JoinColumn(name: 'client_id', referencedColumnName: 'id')] private ClientEntityInterface $client; - #[ORM\Column(name: 'user_id', type: 'string', length: 25, nullable: true)] - private ?string $userId = null; + /** @var non-empty-string $userId */ + #[ORM\Column(name: 'user_id', type: 'string', length: 25)] + private string $userId; /** @var non-empty-string $token */ #[ORM\Column(name: 'token', type: 'string', length: 100)] @@ -53,7 +54,7 @@ class OAuthAccessToken implements AccessTokenEntityInterface #[ORM\Column(name: 'expires_at', type: 'datetime_immutable')] private DateTimeImmutable $expiresAt; - private ?CryptKey $privateKey = null; + private ?CryptKeyInterface $privateKey = null; private ?Configuration $jwtConfiguration = null; @@ -62,11 +63,9 @@ public function __construct() $this->scopes = new ArrayCollection(); } - public function setClient(ClientEntityInterface $client): self + public function setClient(ClientEntityInterface $client): void { $this->client = $client; - - return $this; } public function getClient(): ClientEntityInterface @@ -85,11 +84,9 @@ public function getToken(): string /** * @param non-empty-string $token */ - public function setToken(string $token): self + public function setToken(string $token): void { $this->token = $token; - - return $this; } public function setIsRevoked(bool $isRevoked): self @@ -122,37 +119,33 @@ public function getIdentifier(): string /** * @param mixed $identifier */ - public function setIdentifier($identifier): self + public function setIdentifier($identifier): void { - return $this->setToken($identifier); + $this->setToken($identifier); } /** - * @param string|int|null $identifier + * @param non-empty-string|int $identifier */ - public function setUserIdentifier($identifier): self + public function setUserIdentifier($identifier): void { if (is_int($identifier)) { $identifier = (string) $identifier; } $this->userId = $identifier; - - return $this; } - public function getUserIdentifier(): ?string + public function getUserIdentifier(): string { return $this->userId; } - public function addScope(ScopeEntityInterface $scope): self + public function addScope(ScopeEntityInterface $scope): void { if (! $this->scopes->contains($scope)) { $this->scopes->add($scope); } - - return $this; } public function removeScope(OAuthScope $scope): self @@ -178,18 +171,14 @@ public function getExpiryDateTime(): DateTimeImmutable return $this->expiresAt; } - public function setExpiryDateTime(DateTimeImmutable $dateTime): self + public function setExpiryDateTime(DateTimeImmutable $dateTime): void { $this->expiresAt = $dateTime; - - return $this; } - public function setPrivateKey(CryptKey $privateKey): self + public function setPrivateKey(CryptKeyInterface $privateKey): void { $this->privateKey = $privateKey; - - return $this; } public function initJwtConfiguration(): self @@ -238,4 +227,9 @@ public function __toString(): string { return $this->convertToJWT()->toString(); } + + public function toString(): string + { + return $this->convertToJWT()->toString(); + } } diff --git a/src/Core/src/Security/src/Entity/OAuthAuthCode.php b/src/Core/src/Security/src/Entity/OAuthAuthCode.php index 2b184ea..b7bfa99 100644 --- a/src/Core/src/Security/src/Entity/OAuthAuthCode.php +++ b/src/Core/src/Security/src/Entity/OAuthAuthCode.php @@ -48,11 +48,9 @@ public function __construct() $this->scopes = new ArrayCollection(); } - public function setClient(ClientEntityInterface $client): self + public function setClient(ClientEntityInterface $client): void { $this->client = $client; - - return $this; } public function getClient(): ClientEntityInterface @@ -68,19 +66,17 @@ public function getIdentifier(): string /** * @param mixed $identifier */ - public function setIdentifier($identifier): self + public function setIdentifier($identifier): void { $this->setId($identifier); - - return $this; } /** * @param string|int|null $identifier */ - public function setUserIdentifier($identifier): self + public function setUserIdentifier($identifier): void { - return $this; + $this->setIdentifier($identifier); } public function getUserIdentifier(): ?string @@ -114,13 +110,11 @@ public function revoke(): self return $this; } - public function addScope(ScopeEntityInterface $scope): self + public function addScope(ScopeEntityInterface $scope): void { if (! $this->scopes->contains($scope)) { $this->scopes->add($scope); } - - return $this; } public function removeScope(ScopeEntityInterface $scope): self @@ -156,8 +150,8 @@ public function getExpiryDateTime(): DateTimeImmutable return $this->getExpiresDatetime(); } - public function setExpiryDateTime(DateTimeImmutable $dateTime): self + public function setExpiryDateTime(DateTimeImmutable $dateTime): void { - return $this->setExpiresDatetime($dateTime); + $this->setExpiresDatetime($dateTime); } } diff --git a/src/Core/src/Security/src/Entity/OAuthClient.php b/src/Core/src/Security/src/Entity/OAuthClient.php index a52ace1..434b601 100644 --- a/src/Core/src/Security/src/Entity/OAuthClient.php +++ b/src/Core/src/Security/src/Entity/OAuthClient.php @@ -16,13 +16,14 @@ class OAuthClient implements ClientEntityInterface { use NumericIdentifierTrait; - public const NAME_ADMIN = 'admin'; - public const NAME_FRONTEND = 'frontend'; + public const string NAME_ADMIN = 'admin'; + public const string NAME_FRONTEND = 'frontend'; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true)] private ?User $user = null; + /** @var non-empty-string */ #[ORM\Column(name: 'name', type: 'string', length: 40)] private string $name; @@ -55,11 +56,15 @@ public function getIdentity(): string return $this->getName(); } + /** + * @return non-empty-string + */ public function getIdentifier(): string { return $this->getName(); } + /** @param non-empty-string $name */ public function setName(string $name): self { $this->name = $name; @@ -67,6 +72,9 @@ public function setName(string $name): self return $this; } + /** + * @return non-empty-string + */ public function getName(): string { return $this->name; @@ -96,7 +104,7 @@ public function getRedirect(): string return $this->redirect; } - public function getRedirectUri(): ?string + public function getRedirectUri(): string { return $this->getRedirect(); } diff --git a/src/Core/src/Security/src/Entity/OAuthRefreshToken.php b/src/Core/src/Security/src/Entity/OAuthRefreshToken.php index ae6dc16..e65172a 100644 --- a/src/Core/src/Security/src/Entity/OAuthRefreshToken.php +++ b/src/Core/src/Security/src/Entity/OAuthRefreshToken.php @@ -32,16 +32,13 @@ public function getIdentifier(): string return (string) $this->getId(); } - public function setIdentifier(mixed $identifier): self + public function setIdentifier(mixed $identifier): void { - return $this; } - public function setAccessToken(AccessTokenEntityInterface $accessToken): self + public function setAccessToken(AccessTokenEntityInterface $accessToken): void { $this->accessToken = $accessToken; - - return $this; } public function getAccessToken(): OAuthAccessToken|AccessTokenEntityInterface @@ -73,10 +70,8 @@ public function getExpiryDateTime(): DateTimeImmutable return $this->expiresAt; } - public function setExpiryDateTime(DateTimeImmutable $dateTime): self + public function setExpiryDateTime(DateTimeImmutable $dateTime): void { $this->expiresAt = $dateTime; - - return $this; } } diff --git a/src/Core/src/Security/src/Entity/OAuthScope.php b/src/Core/src/Security/src/Entity/OAuthScope.php index 9afe161..b0f5d68 100644 --- a/src/Core/src/Security/src/Entity/OAuthScope.php +++ b/src/Core/src/Security/src/Entity/OAuthScope.php @@ -22,8 +22,9 @@ class OAuthScope implements ScopeEntityInterface use NumericIdentifierTrait; use ScopeTrait; + /** @var non-empty-string */ #[ORM\Column(name: 'scope', type: 'string', length: 191)] - private string $scope = ''; + private string $scope; /** @var Collection */ #[ORM\ManyToMany(targetEntity: OAuthAccessToken::class, mappedBy: 'scopes')] @@ -39,11 +40,13 @@ public function __construct() $this->authCodes = new ArrayCollection(); } + /** @return non-empty-string */ public function getIdentifier(): string { return $this->getScope(); } + /** @param non-empty-string $scope */ public function setScope(string $scope): self { $this->scope = $scope; @@ -51,6 +54,7 @@ public function setScope(string $scope): self return $this; } + /** @return non-empty-string */ public function getScope(): string { return $this->scope; diff --git a/src/Core/src/Security/src/Repository/OAuthAccessTokenRepository.php b/src/Core/src/Security/src/Repository/OAuthAccessTokenRepository.php index 3cd6b17..1f39aba 100644 --- a/src/Core/src/Security/src/Repository/OAuthAccessTokenRepository.php +++ b/src/Core/src/Security/src/Repository/OAuthAccessTokenRepository.php @@ -42,7 +42,8 @@ public function getNewToken( array $scopes, $userIdentifier = null ): OAuthAccessToken { - $accessToken = (new OAuthAccessToken())->setClient($clientEntity); + $accessToken = new OAuthAccessToken(); + $accessToken->setClient($clientEntity); foreach ($scopes as $scope) { $accessToken->addScope($scope); } @@ -71,10 +72,7 @@ public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEnt $this->getEntityManager()->flush(); } - /** - * @param string $tokenId - */ - public function revokeAccessToken($tokenId): void + public function revokeAccessToken(string $tokenId): void { $accessTokenEntity = $this->findOneBy(['token' => $tokenId]); if ($accessTokenEntity instanceof OAuthAccessToken) { @@ -83,10 +81,7 @@ public function revokeAccessToken($tokenId): void } } - /** - * @param string $tokenId - */ - public function isAccessTokenRevoked($tokenId): bool + public function isAccessTokenRevoked(string $tokenId): bool { $accessTokenEntity = $this->findOneBy(['token' => $tokenId]); if ($accessTokenEntity instanceof OAuthAccessToken) { diff --git a/src/Core/src/Security/src/Repository/OAuthAuthCodeRepository.php b/src/Core/src/Security/src/Repository/OAuthAuthCodeRepository.php index a606ee0..ea42fb7 100644 --- a/src/Core/src/Security/src/Repository/OAuthAuthCodeRepository.php +++ b/src/Core/src/Security/src/Repository/OAuthAuthCodeRepository.php @@ -24,10 +24,7 @@ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): voi $this->getEntityManager()->flush(); } - /** - * @param string $codeId - */ - public function revokeAuthCode($codeId): void + public function revokeAuthCode(string $codeId): void { $authCodeEntity = $this->find($codeId); if ($authCodeEntity instanceof OAuthAuthCode) { @@ -36,10 +33,7 @@ public function revokeAuthCode($codeId): void } } - /** - * @param string $codeId - */ - public function isAuthCodeRevoked($codeId): bool + public function isAuthCodeRevoked(string $codeId): bool { $authCodeEntity = $this->find($codeId); if ($authCodeEntity instanceof OAuthAuthCode) { diff --git a/src/Core/src/Security/src/Repository/OAuthClientRepository.php b/src/Core/src/Security/src/Repository/OAuthClientRepository.php index 53dd362..590ff7d 100644 --- a/src/Core/src/Security/src/Repository/OAuthClientRepository.php +++ b/src/Core/src/Security/src/Repository/OAuthClientRepository.php @@ -16,22 +16,19 @@ #[Entity(name: OAuthClient::class)] class OAuthClientRepository extends AbstractRepository implements ClientRepositoryInterface { - private const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; - private const GRANT_TYPE_AUTHORIZATION_CODE = 'authorization_code'; - private const GRANT_TYPE_REFRESH_TOKEN = 'refresh_token'; - private const GRANT_TYPE_PASSWORD = 'password'; + private const string GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; + private const string GRANT_TYPE_AUTHORIZATION_CODE = 'authorization_code'; + private const string GRANT_TYPE_REFRESH_TOKEN = 'refresh_token'; + private const string GRANT_TYPE_PASSWORD = 'password'; - private const GRANT_TYPES = [ + private const array GRANT_TYPES = [ self::GRANT_TYPE_CLIENT_CREDENTIALS, self::GRANT_TYPE_AUTHORIZATION_CODE, self::GRANT_TYPE_REFRESH_TOKEN, self::GRANT_TYPE_PASSWORD, ]; - /** - * @param string $clientIdentifier - */ - public function getClientEntity($clientIdentifier): ?ClientEntityInterface + public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface { $client = $this->findOneBy(['name' => $clientIdentifier]); if ($client instanceof OAuthClient) { @@ -41,12 +38,7 @@ public function getClientEntity($clientIdentifier): ?ClientEntityInterface return null; } - /** - * @param string $clientIdentifier - * @param null|string $clientSecret - * @param null|string $grantType - */ - public function validateClient($clientIdentifier, $clientSecret, $grantType): bool + public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool { $client = $this->getClientEntity($clientIdentifier); if (! $client instanceof OAuthClient) { diff --git a/src/Core/src/Security/src/Repository/OAuthRefreshTokenRepository.php b/src/Core/src/Security/src/Repository/OAuthRefreshTokenRepository.php index 07b1414..d0afc07 100644 --- a/src/Core/src/Security/src/Repository/OAuthRefreshTokenRepository.php +++ b/src/Core/src/Security/src/Repository/OAuthRefreshTokenRepository.php @@ -24,10 +24,7 @@ public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshToken $this->getEntityManager()->flush(); } - /** - * @param string $tokenId - */ - public function revokeRefreshToken($tokenId): void + public function revokeRefreshToken(string $tokenId): void { $refreshTokenEntity = $this->find($tokenId); if ($refreshTokenEntity instanceof OAuthRefreshToken) { @@ -36,10 +33,7 @@ public function revokeRefreshToken($tokenId): void } } - /** - * @param string $tokenId - */ - public function isRefreshTokenRevoked($tokenId): bool + public function isRefreshTokenRevoked(string $tokenId): bool { $refreshTokenEntity = $this->find($tokenId); if ($refreshTokenEntity instanceof OAuthRefreshToken) { diff --git a/src/Core/src/Security/src/Repository/OAuthScopeRepository.php b/src/Core/src/Security/src/Repository/OAuthScopeRepository.php index a78c4ae..7e096ae 100644 --- a/src/Core/src/Security/src/Repository/OAuthScopeRepository.php +++ b/src/Core/src/Security/src/Repository/OAuthScopeRepository.php @@ -27,16 +27,12 @@ public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface return null; } - /** - * @param string $grantType - * @param null|string $userIdentifier - * @return ScopeEntityInterface[] - */ public function finalizeScopes( array $scopes, - $grantType, + string $grantType, ClientEntityInterface $clientEntity, - $userIdentifier = null + string|null $userIdentifier = null, + ?string $authCodeId = null ): array { return $scopes; } diff --git a/src/Core/src/User/src/Entity/User.php b/src/Core/src/User/src/Entity/User.php index 604748b..5e6684e 100644 --- a/src/Core/src/User/src/Entity/User.php +++ b/src/Core/src/User/src/Entity/User.php @@ -54,9 +54,9 @@ class User extends AbstractEntity implements UserEntityInterface #[ORM\InverseJoinColumn(name: 'role_id', referencedColumnName: 'id')] protected Collection $roles; - /** @var non-empty-string|null $identity */ + /** @var non-empty-string $identity */ #[ORM\Column(name: 'identity', type: 'string', length: 191, unique: true)] - protected ?string $identity = null; + protected string $identity; #[ORM\Column(name: 'password', type: 'string', length: 191)] protected ?string $password = null; @@ -205,11 +205,6 @@ public function getIdentity(): ?string return $this->identity; } - public function hasIdentity(): bool - { - return $this->identity !== null; - } - /** * @param non-empty-string $identity */ @@ -261,7 +256,7 @@ public function setHash(string $hash): self public function getIdentifier(): string { - return (string) $this->identity; + return $this->identity; } public function activate(): self diff --git a/src/Core/src/User/src/Repository/UserRepository.php b/src/Core/src/User/src/Repository/UserRepository.php index f82e675..2bef654 100644 --- a/src/Core/src/User/src/Repository/UserRepository.php +++ b/src/Core/src/User/src/Repository/UserRepository.php @@ -87,16 +87,13 @@ public function getUsers(array $params = [], array $filters = []): QueryBuilder return $queryBuilder; } - /** - * @param string $username - * @param string $password - * @param string $grantType + /** @param non-empty-string $username * @throws OAuthServerException */ public function getUserEntityByUserCredentials( - $username, - $password, - $grantType, + string $username, + string $password, + string $grantType, ClientEntityInterface $clientEntity ): ?UserEntity { $qb = $this->getEntityManager()->createQueryBuilder(); diff --git a/src/User/src/Service/UserService.php b/src/User/src/Service/UserService.php index a78769b..e0c6819 100644 --- a/src/User/src/Service/UserService.php +++ b/src/User/src/Service/UserService.php @@ -171,7 +171,7 @@ public function saveUser(array $data, ?User $user = null): User $user = new User(); } - if (array_key_exists('identity', $data) && $data['identity'] !== null && ! $user->hasIdentity()) { + if (array_key_exists('identity', $data) && $data['identity'] !== null) { $user->setIdentity($data['identity']); } if (array_key_exists('password', $data) && $data['password'] !== null) { diff --git a/test/Functional/AuthenticationTest.php b/test/Functional/AuthenticationTest.php index 4c958a7..6ba3d9f 100644 --- a/test/Functional/AuthenticationTest.php +++ b/test/Functional/AuthenticationTest.php @@ -80,13 +80,12 @@ public function testInvalidRefreshToken(): void $data = json_decode($response->getBody()->getContents(), true); - $this->assertResponseUnauthorized($response); + $this->assertResponseBadRequest($response); $this->assertArrayHasKey('error', $data); $this->assertArrayHasKey('error_description', $data); $this->assertArrayHasKey('hint', $data); - $this->assertArrayHasKey('message', $data); - $this->assertSame('invalid_request', $data['error']); + $this->assertSame('invalid_grant', $data['error']); } /**