diff --git a/examples/src/Repositories/ClientRepository.php b/examples/src/Repositories/ClientRepository.php index 0b19d57d7..047fe77f7 100644 --- a/examples/src/Repositories/ClientRepository.php +++ b/examples/src/Repositories/ClientRepository.php @@ -59,7 +59,10 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType): bo return false; } - if (password_verify($clientSecret, $clients[$clientIdentifier]['secret']) === false) { + if ( + $clients[$clientIdentifier]['is_confidential'] === true + && password_verify($clientSecret, $clients[$clientIdentifier]['secret']) === false + ) { return false; } diff --git a/src/Entities/ClientEntityInterface.php b/src/Entities/ClientEntityInterface.php index f3838b11c..138e831f7 100644 --- a/src/Entities/ClientEntityInterface.php +++ b/src/Entities/ClientEntityInterface.php @@ -38,4 +38,9 @@ public function getRedirectUri(): string|array; * Returns true if the client is confidential. */ public function isConfidential(): bool; + + /** + * Returns true if the client handles the given grant type. + */ + public function hasGrantType(string $grantType): bool; } diff --git a/src/Entities/Traits/ClientTrait.php b/src/Entities/Traits/ClientTrait.php index b179cfac4..0b4327c35 100644 --- a/src/Entities/Traits/ClientTrait.php +++ b/src/Entities/Traits/ClientTrait.php @@ -52,4 +52,12 @@ public function isConfidential(): bool { return $this->isConfidential; } + + /** + * Returns true if the client handles the given grant type. + */ + public function hasGrantType(string $grantType): bool + { + return true; + } } diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php index 9eff92456..ff1b2fb47 100644 --- a/src/Exception/OAuthServerException.php +++ b/src/Exception/OAuthServerException.php @@ -252,8 +252,17 @@ public static function slowDown(string $hint = '', Throwable $previous = null): } /** + * Unauthorized client error. + */ + public static function unauthorizedClient(?string $hint = null): static { - return $this->errorType; + return new static( + 'The authenticated client is not authorized to use this authorization grant type.', + 14, + 'unauthorized_client', + 400, + $hint + ); } /** diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index ea0064c3b..af13c3e34 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -189,6 +189,10 @@ protected function getClientEntityOrFail(string $clientId, ServerRequestInterfac throw OAuthServerException::invalidClient($request); } + if (!$client->hasGrantType($this->getIdentifier())) { + throw OAuthServerException::unauthorizedClient(); + } + return $client; } @@ -486,7 +490,7 @@ protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ? { $refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); - if ($refreshToken === null) { + if ($refreshToken === null || !$accessToken->getClient()->hasGrantType('refresh_token')) { return null; } diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index 8a24a8e95..1a4a1587e 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -96,14 +96,7 @@ public function respondToAccessTokenRequest( ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { - list($clientId) = $this->getClientCredentials($request); - - $client = $this->getClientEntityOrFail($clientId, $request); - - // Only validate the client if it is confidential - if ($client->isConfidential()) { - $this->validateClient($request); - } + $client = $this->validateClient($request); $encryptedAuthCode = $this->getRequestParameter('code', $request); diff --git a/src/Grant/ClientCredentialsGrant.php b/src/Grant/ClientCredentialsGrant.php index bee6abaa1..a24266c4c 100644 --- a/src/Grant/ClientCredentialsGrant.php +++ b/src/Grant/ClientCredentialsGrant.php @@ -34,9 +34,7 @@ public function respondToAccessTokenRequest( ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { - list($clientId) = $this->getClientCredentials($request); - - $client = $this->getClientEntityOrFail($clientId, $request); + $client = $this->validateClient($request); if (!$client->isConfidential()) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); @@ -44,9 +42,6 @@ public function respondToAccessTokenRequest( throw OAuthServerException::invalidClient($request); } - // Validate request - $this->validateClient($request); - $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); // Finalize the requested scopes diff --git a/tests/Grant/AbstractGrantTest.php b/tests/Grant/AbstractGrantTest.php index adfb880be..7e2193e87 100644 --- a/tests/Grant/AbstractGrantTest.php +++ b/tests/Grant/AbstractGrantTest.php @@ -398,6 +398,7 @@ public function testIssueRefreshToken(): void $issueRefreshTokenMethod->setAccessible(true); $accessToken = new AccessTokenEntity(); + $accessToken->setClient(new ClientEntity()); /** @var RefreshTokenEntityInterface $refreshToken */ $refreshToken = $issueRefreshTokenMethod->invoke($grantMock, $accessToken); diff --git a/tests/Grant/AuthCodeGrantTest.php b/tests/Grant/AuthCodeGrantTest.php index 6a6842661..cab22a927 100644 --- a/tests/Grant/AuthCodeGrantTest.php +++ b/tests/Grant/AuthCodeGrantTest.php @@ -565,7 +565,9 @@ public function testRespondToAccessTokenRequest(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -623,13 +625,16 @@ public function testRespondToAccessTokenRequestUsingHttpBasicAuth(): void $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); @@ -686,6 +691,7 @@ public function testRespondToAccessTokenRequestForPublicClient(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -693,7 +699,9 @@ public function testRespondToAccessTokenRequestForPublicClient(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -751,6 +759,7 @@ public function testRespondToAccessTokenRequestNullRefreshToken(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -828,7 +837,9 @@ public function testRespondToAccessTokenRequestCodeChallengePlain(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -902,7 +913,9 @@ public function testRespondToAccessTokenRequestCodeChallengeS256(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -1187,6 +1200,7 @@ public function testRespondToAccessTokenRequestWithRefreshTokenInsteadOfAuthCode $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), @@ -1276,6 +1290,7 @@ public function testRespondToAccessTokenRequestExpiredCode(): void $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), @@ -1980,6 +1995,7 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -1987,7 +2003,9 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -2055,6 +2073,7 @@ public function testRefreshTokenRepositoryFailToPersist(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -2062,7 +2081,9 @@ public function testRefreshTokenRepositoryFailToPersist(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -2123,6 +2144,7 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): v $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -2130,7 +2152,9 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): v $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); diff --git a/tests/Grant/DeviceCodeGrantTest.php b/tests/Grant/DeviceCodeGrantTest.php index 396ea760f..40249bb39 100644 --- a/tests/Grant/DeviceCodeGrantTest.php +++ b/tests/Grant/DeviceCodeGrantTest.php @@ -347,7 +347,9 @@ public function testRespondToAccessTokenRequest(): void $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); diff --git a/tests/Grant/PasswordGrantTest.php b/tests/Grant/PasswordGrantTest.php index 8c60a8c78..011a9605e 100644 --- a/tests/Grant/PasswordGrantTest.php +++ b/tests/Grant/PasswordGrantTest.php @@ -46,7 +46,9 @@ public function testRespondToRequest(): void $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); diff --git a/tests/Grant/RefreshTokenGrantTest.php b/tests/Grant/RefreshTokenGrantTest.php index b37001a80..4a4adfb25 100644 --- a/tests/Grant/RefreshTokenGrantTest.php +++ b/tests/Grant/RefreshTokenGrantTest.php @@ -59,7 +59,9 @@ public function testRespondToRequest(): void $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -181,7 +183,9 @@ public function testRespondToReducedScopes(): void $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -567,10 +571,12 @@ public function testRespondToRequestFinalizeScopes(): void ->with($scopes, $grant->getIdentifier(), $client) ->willReturn($finalizedScopes); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); $accessTokenRepositoryMock ->method('getNewToken') ->with($client, $finalizedScopes) - ->willReturn(new AccessTokenEntity()); + ->willReturn($accessToken); $oldRefreshToken = json_encode( [