From 9446f0e292d03f9f5fbc731e18b71638a267acf8 Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 2 Mar 2018 22:53:46 +0100 Subject: [PATCH 01/34] Add introspection implementation according to RFC 7662 the introspection mechanism is implemented --- README.md | 1 + src/AuthorizationServer.php | 16 ++ src/Introspector.php | 169 ++++++++++++++++++++ src/ResponseTypes/IntrospectionResponse.php | 41 +++++ tests/IntrospectorTest.php | 151 +++++++++++++++++ 5 files changed, 378 insertions(+) create mode 100644 src/Introspector.php create mode 100644 src/ResponseTypes/IntrospectionResponse.php create mode 100644 tests/IntrospectorTest.php diff --git a/README.md b/README.md index b53267421..c051ff2df 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ The following RFCs are implemented: * [RFC6750 " The OAuth 2.0 Authorization Framework: Bearer Token Usage"](https://tools.ietf.org/html/rfc6750) * [RFC7519 "JSON Web Token (JWT)"](https://tools.ietf.org/html/rfc7519) * [RFC7636 "Proof Key for Code Exchange by OAuth Public Clients"](https://tools.ietf.org/html/rfc7636) +* [RFC7662 "OAuth 2.0 Token Introspection"](https://tools.ietf.org/html/rfc7662) This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](https://twitter.com/alexbilbie). diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index f1e96146b..0bd73d1a8 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -197,6 +197,22 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res throw OAuthServerException::unsupportedGrantType(); } + /** + * Return an introspection response. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function respondToIntrospectionRequest(ServerRequestInterface $request, ResponseInterface $response) + { + $introspector = new Introspector($this->accessTokenRepository, $this->privateKey); + $introspectionResponse = $introspector->respondToIntrospectionRequest($request); + + return $introspectionResponse->generateHttpResponse($response); + } + /** * Get the token type that grants will return in the HTTP response. * diff --git a/src/Introspector.php b/src/Introspector.php new file mode 100644 index 000000000..0152558d9 --- /dev/null +++ b/src/Introspector.php @@ -0,0 +1,169 @@ +accessTokenRepository = $accessTokenRepository; + $this->privateKey = $privateKey; + $this->parser = $parser; + } + + /** + * Return an introspection response. + * + * @param ServerRequestInterface $request + * + * @return IntrospectionResponse + */ + public function respondToIntrospectionRequest(ServerRequestInterface $request) + { + $jwt = $request->getParsedBody()['token'] ?? null; + + try { + $token = $this->parser->parse($jwt); + + $this->verifyToken($token); + $this->checkIfTokenIsExpired($token); + $this->checkIfTokenIsRevoked($token); + + return $this->createActiveResponse($token); + } + catch(Exception $ex) { + return $this->createInactiveResponse(); + } + } + + /** + * Validate the JWT token. + * + * @param Token $token + * + * @throws OAuthServerException + */ + private function verifyToken(Token $token) + { + $keychain = new Keychain(); + $key = $keychain->getPrivateKey($this->privateKey->getKeyPath(), $this->privateKey->getPassPhrase()); + + if (!$token->verify(new Sha256, $key)) { + throw OAuthServerException::accessDenied('Access token could not be verified'); + } + } + + /** + * Ensure access token hasn't expired + * + * @param Token $token + * + * @throws OAuthServerException + */ + private function checkIfTokenIsExpired(Token $token) + { + $data = new ValidationData(time()); + + if (!$token->validate($data)) { + throw OAuthServerException::accessDenied('Access token is invalid'); + } + } + + /** + * Check if the given access token is revoked. + * + * @param Token $token + * + * @throws OAuthServerException + */ + private function checkIfTokenIsRevoked(Token $token) + { + if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) { + throw OAuthServerException::accessDenied('Access token has been revoked'); + } + } + + /** + * Create active introspection response. + * + * @param Token $token + * + * @return IntrospectionResponse + */ + private function createActiveResponse(Token $token) + { + $response = new IntrospectionResponse(); + + $response->setIntrospectionData( + [ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => $token->getClaim('scopes', ''), + 'client_id' => $token->getClaim('aud'), + 'exp' => $token->getClaim('exp'), + 'iat' => $token->getClaim('iat'), + 'sub' => $token->getClaim('sub'), + 'jti' => $token->getClaim('jti'), + ] + ); + + return $response; + } + + /** + * Create inactive introspection response + * + * @return IntrospectionResponse + */ + private function createInactiveResponse() + { + $response = new IntrospectionResponse(); + + $response->setIntrospectionData( + [ + 'active' => false, + ] + ); + + return $response; + } +} diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php new file mode 100644 index 000000000..10ab2dfa5 --- /dev/null +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -0,0 +1,41 @@ +introspectionData = $introspectionData; + } + + /** + * @return array + */ + public function getIntrospectionData() + { + return $this->introspectionData; + } + + /** + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function generateHttpResponse(ResponseInterface $response) + { + $response->getBody()->write(json_encode($this->introspectionData)); + return $response; + } +} diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php new file mode 100644 index 000000000..0388b1a12 --- /dev/null +++ b/tests/IntrospectorTest.php @@ -0,0 +1,151 @@ +getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + new Parser() + ); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getParsedBody')->willReturn([]); + + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + + $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); + $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + } + + public function testRespondToRequestWithInvalidToken() + { + $parserMock = $this->getMockBuilder(Parser::class)->getMock(); + $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + + $introspector = new Introspector( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + $parserMock + ); + + $parserMock->method('parse')->willReturn($tokenMock); + $tokenMock->method('verify')->willReturn(false); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); + + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + + $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); + $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + } + + public function testRespondToRequestWithExpiredToken() + { + $parserMock = $this->getMockBuilder(Parser::class)->getMock(); + $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + + $introspector = new Introspector( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + $parserMock + ); + + $parserMock->method('parse')->willReturn($tokenMock); + $tokenMock->method('verify')->willReturn(true); + $tokenMock->method('validate')->willReturn(false); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); + + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + + $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); + $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + } + + public function testRespondToRequestWithRevokedToken() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $parserMock = $this->getMockBuilder(Parser::class)->getMock(); + $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + + $introspector = new Introspector( + $accessTokenRepositoryMock, + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + $parserMock + ); + + $parserMock->method('parse')->willReturn($tokenMock); + $tokenMock->method('verify')->willReturn(true); + $tokenMock->method('validate')->willReturn(true); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(true); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); + + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + + $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); + $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + } + + public function testRespondToRequestWithValidToken() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $parserMock = $this->getMockBuilder(Parser::class)->getMock(); + $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + + $introspector = new Introspector( + $accessTokenRepositoryMock, + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + $parserMock + ); + + $parserMock->method('parse')->willReturn($tokenMock); + $tokenMock->method('verify')->willReturn(true); + $tokenMock->method('validate')->willReturn(true); + $tokenMock->method('getClaim')->willReturn('value'); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); + + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + + $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); + $this->assertEquals( + [ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => 'value', + 'client_id' => 'value', + 'exp' => 'value', + 'iat' => 'value', + 'sub' => 'value', + 'jti' => 'value', + ], + $introspectionResponse->getIntrospectionData() + ); + } +} \ No newline at end of file From 1ad5514f553d176e41e9b11d59868b3ada25da0a Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 2 Mar 2018 22:58:18 +0100 Subject: [PATCH 02/34] Apply styleci fixes --- src/Grant/AuthCodeGrant.php | 1 + src/Introspector.php | 12 ++++-------- src/ResponseTypes/IntrospectionResponse.php | 1 + tests/IntrospectorTest.php | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index a3ab8a32d..77c8d7ca6 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -204,6 +204,7 @@ public function getIdentifier() * Fetch the client_id parameter from the query string. * * @return string|null + * * @throws OAuthServerException */ protected function getClientIdFromRequest($request) diff --git a/src/Introspector.php b/src/Introspector.php index 0152558d9..865d25b64 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -3,17 +3,15 @@ namespace League\OAuth2\Server; use Exception; +use Lcobucci\JWT\Parser; use Lcobucci\JWT\Signer\Keychain; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; use Lcobucci\JWT\ValidationData; -use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Lcobucci\JWT\Parser; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; +use Psr\Http\Message\ServerRequestInterface; class Introspector { @@ -43,8 +41,7 @@ public function __construct( AccessTokenRepositoryInterface $accessTokenRepository, CryptKey $privateKey, Parser $parser - ) - { + ) { $this->accessTokenRepository = $accessTokenRepository; $this->privateKey = $privateKey; $this->parser = $parser; @@ -69,8 +66,7 @@ public function respondToIntrospectionRequest(ServerRequestInterface $request) $this->checkIfTokenIsRevoked($token); return $this->createActiveResponse($token); - } - catch(Exception $ex) { + } catch(Exception $ex) { return $this->createInactiveResponse(); } } diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 10ab2dfa5..8fe57c488 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -36,6 +36,7 @@ public function getIntrospectionData() public function generateHttpResponse(ResponseInterface $response) { $response->getBody()->write(json_encode($this->introspectionData)); + return $response; } } diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 0388b1a12..4510efbd0 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -148,4 +148,4 @@ public function testRespondToRequestWithValidToken() $introspectionResponse->getIntrospectionData() ); } -} \ No newline at end of file +} From 651ee9bbde52444ef80c2b8e555268f0d4b11f6e Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 2 Mar 2018 22:59:32 +0100 Subject: [PATCH 03/34] Apply styleci fixes --- src/Introspector.php | 4 ++-- src/ResponseTypes/IntrospectionResponse.php | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Introspector.php b/src/Introspector.php index 865d25b64..c9cc4b6f9 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -66,7 +66,7 @@ public function respondToIntrospectionRequest(ServerRequestInterface $request) $this->checkIfTokenIsRevoked($token); return $this->createActiveResponse($token); - } catch(Exception $ex) { + } catch (Exception $ex) { return $this->createInactiveResponse(); } } @@ -121,7 +121,7 @@ private function checkIfTokenIsRevoked(Token $token) /** * Create active introspection response. * - * @param Token $token + * @param Token $token * * @return IntrospectionResponse */ diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 8fe57c488..543a2d948 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -2,7 +2,6 @@ namespace League\OAuth2\Server\ResponseTypes; -use League\OAuth2\Server\ResponseTypes\AbstractResponseType; use Psr\Http\Message\ResponseInterface; class IntrospectionResponse extends AbstractResponseType From 225553f129ba6cc8616bbb5077c5f55ed9cf78be Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 2 Mar 2018 23:08:00 +0100 Subject: [PATCH 04/34] Fix phpstan errors --- src/AuthorizationServer.php | 3 ++- src/Introspector.php | 2 +- tests/IntrospectorTest.php | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 0bd73d1a8..679a57860 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -23,6 +23,7 @@ use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Lcobucci\JWT\Parser; class AuthorizationServer implements EmitterAwareInterface { @@ -207,7 +208,7 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res */ public function respondToIntrospectionRequest(ServerRequestInterface $request, ResponseInterface $response) { - $introspector = new Introspector($this->accessTokenRepository, $this->privateKey); + $introspector = new Introspector($this->accessTokenRepository, $this->privateKey, new Parser); $introspectionResponse = $introspector->respondToIntrospectionRequest($request); return $introspectionResponse->generateHttpResponse($response); diff --git a/src/Introspector.php b/src/Introspector.php index c9cc4b6f9..506bc472d 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -83,7 +83,7 @@ private function verifyToken(Token $token) $keychain = new Keychain(); $key = $keychain->getPrivateKey($this->privateKey->getKeyPath(), $this->privateKey->getPassPhrase()); - if (!$token->verify(new Sha256, $key)) { + if (!$token->verify(new Sha256, $key->getContent())) { throw OAuthServerException::accessDenied('Access token could not be verified'); } } diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 4510efbd0..fb563b117 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -31,7 +31,7 @@ public function testRespondToRequestWithoutToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn([]); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); @@ -54,7 +54,7 @@ public function testRespondToRequestWithInvalidToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); @@ -78,7 +78,7 @@ public function testRespondToRequestWithExpiredToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); @@ -104,7 +104,7 @@ public function testRespondToRequestWithRevokedToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); @@ -131,7 +131,7 @@ public function testRespondToRequestWithValidToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new Response); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertEquals( From 880b4bd00552c3c85d98858ec86a4576075b1f4e Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 2 Mar 2018 23:09:27 +0100 Subject: [PATCH 05/34] Apply styleci fixes --- src/AuthorizationServer.php | 2 +- tests/IntrospectorTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 679a57860..2a786d38c 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -10,6 +10,7 @@ namespace League\OAuth2\Server; use Defuse\Crypto\Key; +use Lcobucci\JWT\Parser; use League\Event\EmitterAwareInterface; use League\Event\EmitterAwareTrait; use League\OAuth2\Server\Exception\OAuthServerException; @@ -23,7 +24,6 @@ use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Lcobucci\JWT\Parser; class AuthorizationServer implements EmitterAwareInterface { diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index fb563b117..c6e74ff4b 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -10,7 +10,6 @@ use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response; class IntrospectorTest extends TestCase { From 487241b23935d1849ef0b992b7d526f573151210 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Fri, 20 Jul 2018 15:28:29 +0100 Subject: [PATCH 06/34] Refactor introspection response to not use exceptions to control the flow Co-authored-by: Rob Taylor --- src/Introspector.php | 45 +++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/Introspector.php b/src/Introspector.php index 506bc472d..77b2244fd 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -3,6 +3,7 @@ namespace League\OAuth2\Server; use Exception; +use InvalidArgumentException; use Lcobucci\JWT\Parser; use Lcobucci\JWT\Signer\Keychain; use Lcobucci\JWT\Signer\Rsa\Sha256; @@ -60,15 +61,23 @@ public function respondToIntrospectionRequest(ServerRequestInterface $request) try { $token = $this->parser->parse($jwt); - - $this->verifyToken($token); - $this->checkIfTokenIsExpired($token); - $this->checkIfTokenIsRevoked($token); - - return $this->createActiveResponse($token); - } catch (Exception $ex) { + } catch (InvalidArgumentException $e) { return $this->createInactiveResponse(); } + + return $this->isTokenValid($token) ? + $this->createActiveResponse($token) : + $this->createInactiveResponse(); + } + + /** + * Validate the JWT and make sure it has not expired or been revoked + * + * @return bool + */ + private function isTokenValid(Token $token) + { + return $this->verifyToken($token) && !$this->isTokenExpired($token) && !$this->isTokenRevoked($token); } /** @@ -76,16 +85,14 @@ public function respondToIntrospectionRequest(ServerRequestInterface $request) * * @param Token $token * - * @throws OAuthServerException + * @return bool */ private function verifyToken(Token $token) { $keychain = new Keychain(); $key = $keychain->getPrivateKey($this->privateKey->getKeyPath(), $this->privateKey->getPassPhrase()); - if (!$token->verify(new Sha256, $key->getContent())) { - throw OAuthServerException::accessDenied('Access token could not be verified'); - } + return $token->verify(new Sha256, $key->getContent()); } /** @@ -93,15 +100,13 @@ private function verifyToken(Token $token) * * @param Token $token * - * @throws OAuthServerException + * @return bool */ - private function checkIfTokenIsExpired(Token $token) + private function isTokenExpired(Token $token) { $data = new ValidationData(time()); - if (!$token->validate($data)) { - throw OAuthServerException::accessDenied('Access token is invalid'); - } + return !$token->validate($data); } /** @@ -109,13 +114,11 @@ private function checkIfTokenIsExpired(Token $token) * * @param Token $token * - * @throws OAuthServerException + * @return bool */ - private function checkIfTokenIsRevoked(Token $token) + private function isTokenRevoked(Token $token) { - if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) { - throw OAuthServerException::accessDenied('Access token has been revoked'); - } + return $this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti')); } /** From eba79d73651ee8efcb1b2bbc59b8fb437d82b5b9 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Fri, 20 Jul 2018 17:19:25 +0100 Subject: [PATCH 07/34] refactor response to be more inline with other package reponses --- src/AuthorizationServer.php | 28 ++++++++- src/Introspector.php | 48 ++++------------ src/ResponseTypes/IntrospectionResponse.php | 64 ++++++++++++++++++--- tests/IntrospectorTest.php | 36 ++++++++---- 4 files changed, 118 insertions(+), 58 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 2a786d38c..6b4ff8cee 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -21,6 +21,7 @@ use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\AbstractResponseType; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; +use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -198,6 +199,28 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res throw OAuthServerException::unsupportedGrantType(); } + /** + * @param IntrospectionResponse $response + */ + public function setIntrospectionReponseType(IntrospectionResponse $reponseType) + { + $this->introspectionResponseType = $reponseType; + } + + /** + * Get the introspection response + * + * @return ResponseTypeInterface + */ + protected function getIntrospectionResponseType() + { + if ($this->introspectionResponseType instanceof IntrospectionResponse === false) { + $this->introspectionResponseType = new IntrospectionResponse; + } + + return $this->introspectionResponseType; + } + /** * Return an introspection response. * @@ -209,7 +232,10 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res public function respondToIntrospectionRequest(ServerRequestInterface $request, ResponseInterface $response) { $introspector = new Introspector($this->accessTokenRepository, $this->privateKey, new Parser); - $introspectionResponse = $introspector->respondToIntrospectionRequest($request); + $introspectionResponse = $introspector->respondToIntrospectionRequest( + $request, + $this->getIntrospectionResponseType() + ); return $introspectionResponse->generateHttpResponse($response); } diff --git a/src/Introspector.php b/src/Introspector.php index 77b2244fd..877751e9f 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -55,19 +55,22 @@ public function __construct( * * @return IntrospectionResponse */ - public function respondToIntrospectionRequest(ServerRequestInterface $request) + public function respondToIntrospectionRequest( + ServerRequestInterface $request, + IntrospectionResponse $responseType + ) { $jwt = $request->getParsedBody()['token'] ?? null; try { $token = $this->parser->parse($jwt); } catch (InvalidArgumentException $e) { - return $this->createInactiveResponse(); + return $responseType; } return $this->isTokenValid($token) ? - $this->createActiveResponse($token) : - $this->createInactiveResponse(); + $this->setTokenOnResponse($token, $responseType) : + $responseType; } /** @@ -128,41 +131,10 @@ private function isTokenRevoked(Token $token) * * @return IntrospectionResponse */ - private function createActiveResponse(Token $token) + private function setTokenOnResponse(Token $token, IntrospectionResponse $responseType) { - $response = new IntrospectionResponse(); - - $response->setIntrospectionData( - [ - 'active' => true, - 'token_type' => 'access_token', - 'scope' => $token->getClaim('scopes', ''), - 'client_id' => $token->getClaim('aud'), - 'exp' => $token->getClaim('exp'), - 'iat' => $token->getClaim('iat'), - 'sub' => $token->getClaim('sub'), - 'jti' => $token->getClaim('jti'), - ] - ); - - return $response; - } - - /** - * Create inactive introspection response - * - * @return IntrospectionResponse - */ - private function createInactiveResponse() - { - $response = new IntrospectionResponse(); - - $response->setIntrospectionData( - [ - 'active' => false, - ] - ); + $responseType->setToken($token); - return $response; + return $responseType; } } diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 543a2d948..69ab1ddce 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -2,29 +2,47 @@ namespace League\OAuth2\Server\ResponseTypes; +use Lcobucci\JWT\Token; use Psr\Http\Message\ResponseInterface; class IntrospectionResponse extends AbstractResponseType { /** - * @var array + * @var Token */ - private $introspectionData; + protected $token; /** - * @param array $introspectionData + * Set the token against the response + * + * @param Token */ - public function setIntrospectionData(array $introspectionData) + public function setToken(Token $token) { - $this->introspectionData = $introspectionData; + $this->token = $token; } /** - * @return array + * Extract the introspection params from the token */ - public function getIntrospectionData() + public function getValidIntrospectionParams() { - return $this->introspectionData; + $token = $this->token; + + if (!$token){ + return []; + } + + return [ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => $token->getClaim('scopes', ''), + 'client_id' => $token->getClaim('aud'), + 'exp' => $token->getClaim('exp'), + 'iat' => $token->getClaim('iat'), + 'sub' => $token->getClaim('sub'), + 'jti' => $token->getClaim('jti'), + ]; } /** @@ -34,8 +52,36 @@ public function getIntrospectionData() */ public function generateHttpResponse(ResponseInterface $response) { - $response->getBody()->write(json_encode($this->introspectionData)); + if ($this->token) { + $responseParams = $this->getValidIntrospectionParams(); + $responseParams = array_merge($this->getExtraParams(), $responseParams); + } + else { + $responseParams = [ + 'active' => false, + ]; + } + + $response = $response + ->withStatus(200) + ->withHeader('pragma', 'no-cache') + ->withHeader('cache-control', 'no-store') + ->withHeader('content-type', 'application/json; charset=UTF-8'); + + $response->getBody()->write(json_encode($responseParams)); return $response; } + + /** + * Add custom fields to your Introspection response here, then set your introspection + * reponse in AuthorizationServer::setIntrospectionResponseType() to pull in your version of + * this class rather than the default. + * + * @return array + */ + protected function getExtraParams() + { + return []; + } } diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index c6e74ff4b..79c8fce09 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -30,10 +30,14 @@ public function testRespondToRequestWithoutToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn([]); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + $this->assertAttributeEquals(null, 'token', $introspectionResponse); + $this->assertEquals( + [], + $introspectionResponse->getValidIntrospectionParams() + ); } public function testRespondToRequestWithInvalidToken() @@ -53,10 +57,14 @@ public function testRespondToRequestWithInvalidToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + $this->assertAttributeEquals(null, 'token', $introspectionResponse); + $this->assertEquals( + [], + $introspectionResponse->getValidIntrospectionParams() + ); } public function testRespondToRequestWithExpiredToken() @@ -77,10 +85,14 @@ public function testRespondToRequestWithExpiredToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + $this->assertAttributeEquals(null, 'token', $introspectionResponse); + $this->assertEquals( + [], + $introspectionResponse->getValidIntrospectionParams() + ); } public function testRespondToRequestWithRevokedToken() @@ -103,10 +115,14 @@ public function testRespondToRequestWithRevokedToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertEquals(['active' => false], $introspectionResponse->getIntrospectionData()); + $this->assertAttributeEquals(null, 'token', $introspectionResponse); + $this->assertEquals( + [], + $introspectionResponse->getValidIntrospectionParams() + ); } public function testRespondToRequestWithValidToken() @@ -130,7 +146,7 @@ public function testRespondToRequestWithValidToken() $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock); + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertEquals( @@ -144,7 +160,7 @@ public function testRespondToRequestWithValidToken() 'sub' => 'value', 'jti' => 'value', ], - $introspectionResponse->getIntrospectionData() + $introspectionResponse->getValidIntrospectionParams() ); } } From caf15b943c7056eb500bdc55a7dda5c177e67c47 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Fri, 20 Jul 2018 17:32:45 +0100 Subject: [PATCH 08/34] update code style --- src/AuthorizationServer.php | 2 +- src/Introspector.php | 2 -- src/ResponseTypes/IntrospectionResponse.php | 5 ++--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 6b4ff8cee..8000d01a7 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -200,7 +200,7 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res } /** - * @param IntrospectionResponse $response + * @param IntrospectionResponse $response */ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) { diff --git a/src/Introspector.php b/src/Introspector.php index 877751e9f..b13e511a5 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -2,14 +2,12 @@ namespace League\OAuth2\Server; -use Exception; use InvalidArgumentException; use Lcobucci\JWT\Parser; use Lcobucci\JWT\Signer\Keychain; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; use Lcobucci\JWT\ValidationData; -use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use Psr\Http\Message\ServerRequestInterface; diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 69ab1ddce..e340adcbf 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -29,7 +29,7 @@ public function getValidIntrospectionParams() { $token = $this->token; - if (!$token){ + if (!$token) { return []; } @@ -55,8 +55,7 @@ public function generateHttpResponse(ResponseInterface $response) if ($this->token) { $responseParams = $this->getValidIntrospectionParams(); $responseParams = array_merge($this->getExtraParams(), $responseParams); - } - else { + } else { $responseParams = [ 'active' => false, ]; From d143c462ac7af09bc2252bb43ec7a9edc50f2638 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Fri, 20 Jul 2018 17:34:13 +0100 Subject: [PATCH 09/34] update code style --- src/Introspector.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Introspector.php b/src/Introspector.php index b13e511a5..4c234f775 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -56,8 +56,7 @@ public function __construct( public function respondToIntrospectionRequest( ServerRequestInterface $request, IntrospectionResponse $responseType - ) - { + ) { $jwt = $request->getParsedBody()['token'] ?? null; try { From f00b07e73f9fb5f8b57e381378792cb28628fd94 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 09:36:57 +0100 Subject: [PATCH 10/34] fix type hints for tests --- src/AuthorizationServer.php | 11 +++++-- src/Introspector.php | 1 + src/ResponseTypes/IntrospectionResponse.php | 35 +++++++++++---------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 8000d01a7..f66799e75 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -55,6 +55,11 @@ class AuthorizationServer implements EmitterAwareInterface */ protected $responseType; + /** + * @var null|IntrospectionResponse + */ + protected $introspectionResponseType; + /** * @var ClientRepositoryInterface */ @@ -200,7 +205,7 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res } /** - * @param IntrospectionResponse $response + * @param IntrospectionResponse $reponseType */ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) { @@ -210,7 +215,7 @@ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) /** * Get the introspection response * - * @return ResponseTypeInterface + * @return IntrospectionResponse */ protected function getIntrospectionResponseType() { @@ -225,7 +230,7 @@ protected function getIntrospectionResponseType() * Return an introspection response. * * @param ServerRequestInterface $request - * @param ResponseInterface $response + * @param ResponseInterface $response * * @return ResponseInterface */ diff --git a/src/Introspector.php b/src/Introspector.php index 4c234f775..ed864f705 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -50,6 +50,7 @@ public function __construct( * Return an introspection response. * * @param ServerRequestInterface $request + * @param IntrospectionResponse $responseType * * @return IntrospectionResponse */ diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index e340adcbf..59a2a94c7 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -15,33 +15,37 @@ class IntrospectionResponse extends AbstractResponseType /** * Set the token against the response * - * @param Token + * @param Token $token */ public function setToken(Token $token) { $this->token = $token; } + private function hasToken() + { + return $this->token !== null; + } /** * Extract the introspection params from the token */ public function getValidIntrospectionParams() { - $token = $this->token; - - if (!$token) { - return []; + if (!$this->hasToken()) { + return [ + 'active' => false + ]; } return [ 'active' => true, 'token_type' => 'access_token', - 'scope' => $token->getClaim('scopes', ''), - 'client_id' => $token->getClaim('aud'), - 'exp' => $token->getClaim('exp'), - 'iat' => $token->getClaim('iat'), - 'sub' => $token->getClaim('sub'), - 'jti' => $token->getClaim('jti'), + 'scope' => $this->token->getClaim('scopes', ''), + 'client_id' => $this->token->getClaim('aud'), + 'exp' => $this->token->getClaim('exp'), + 'iat' => $this->token->getClaim('iat'), + 'sub' => $this->token->getClaim('sub'), + 'jti' => $this->token->getClaim('jti'), ]; } @@ -52,13 +56,10 @@ public function getValidIntrospectionParams() */ public function generateHttpResponse(ResponseInterface $response) { - if ($this->token) { - $responseParams = $this->getValidIntrospectionParams(); + $responseParams = $this->getValidIntrospectionParams(); + + if ($this->hasToken()) { $responseParams = array_merge($this->getExtraParams(), $responseParams); - } else { - $responseParams = [ - 'active' => false, - ]; } $response = $response From 595cacedb580e2e9aa1698c4e510b795093caa02 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 13:10:45 +0100 Subject: [PATCH 11/34] fix code style and unit tests and rename introspection params function --- src/AuthorizationServer.php | 2 +- src/Introspector.php | 2 +- src/ResponseTypes/IntrospectionResponse.php | 6 ++--- tests/IntrospectorTest.php | 26 ++++++++++++++------- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index f66799e75..e03f72e8f 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -230,7 +230,7 @@ protected function getIntrospectionResponseType() * Return an introspection response. * * @param ServerRequestInterface $request - * @param ResponseInterface $response + * @param ResponseInterface $response * * @return ResponseInterface */ diff --git a/src/Introspector.php b/src/Introspector.php index ed864f705..db91b787f 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -50,7 +50,7 @@ public function __construct( * Return an introspection response. * * @param ServerRequestInterface $request - * @param IntrospectionResponse $responseType + * @param IntrospectionResponse $responseType * * @return IntrospectionResponse */ diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 59a2a94c7..d1f5ed04f 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -29,11 +29,11 @@ private function hasToken() /** * Extract the introspection params from the token */ - public function getValidIntrospectionParams() + public function getIntrospectionParams() { if (!$this->hasToken()) { return [ - 'active' => false + 'active' => false, ]; } @@ -56,7 +56,7 @@ public function getValidIntrospectionParams() */ public function generateHttpResponse(ResponseInterface $response) { - $responseParams = $this->getValidIntrospectionParams(); + $responseParams = $this->getIntrospectionParams(); if ($this->hasToken()) { $responseParams = array_merge($this->getExtraParams(), $responseParams); diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 79c8fce09..3638cc48b 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -35,8 +35,10 @@ public function testRespondToRequestWithoutToken() $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( - [], - $introspectionResponse->getValidIntrospectionParams() + [ + 'active' => false + ], + $introspectionResponse->getIntrospectionParams() ); } @@ -62,8 +64,10 @@ public function testRespondToRequestWithInvalidToken() $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( - [], - $introspectionResponse->getValidIntrospectionParams() + [ + 'active' => false + ], + $introspectionResponse->getIntrospectionParams() ); } @@ -90,8 +94,10 @@ public function testRespondToRequestWithExpiredToken() $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( - [], - $introspectionResponse->getValidIntrospectionParams() + [ + 'active' => false + ], + $introspectionResponse->getIntrospectionParams() ); } @@ -120,8 +126,10 @@ public function testRespondToRequestWithRevokedToken() $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( - [], - $introspectionResponse->getValidIntrospectionParams() + [ + 'active' => false + ], + $introspectionResponse->getIntrospectionParams() ); } @@ -160,7 +168,7 @@ public function testRespondToRequestWithValidToken() 'sub' => 'value', 'jti' => 'value', ], - $introspectionResponse->getValidIntrospectionParams() + $introspectionResponse->getIntrospectionParams() ); } } From 33eef792dd08026cb9835a2cdab755e008097f87 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 13:47:45 +0100 Subject: [PATCH 12/34] add validate request method --- src/AuthorizationServer.php | 33 ++++++++++++++++++++++++++++++++- src/Introspector.php | 15 +++++++++++++++ tests/IntrospectorTest.php | 23 +++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index e03f72e8f..1664e52f9 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -60,6 +60,11 @@ class AuthorizationServer implements EmitterAwareInterface */ protected $introspectionResponseType; + /** + * @var null|Introspector + */ + protected $introspector; + /** * @var ClientRepositoryInterface */ @@ -236,7 +241,8 @@ protected function getIntrospectionResponseType() */ public function respondToIntrospectionRequest(ServerRequestInterface $request, ResponseInterface $response) { - $introspector = new Introspector($this->accessTokenRepository, $this->privateKey, new Parser); + $introspector = $this->getIntrospector(); + $introspectionResponse = $introspector->respondToIntrospectionRequest( $request, $this->getIntrospectionResponseType() @@ -245,6 +251,31 @@ public function respondToIntrospectionRequest(ServerRequestInterface $request, R return $introspectionResponse->generateHttpResponse($response); } + /** + * Return an introspection response. + * + * @param ServerRequestInterface $request + */ + public function validateIntrospectionRequest(ServerRequestInterface $request) + { + $introspector = $this->getIntrospector(); + $introspector->validateIntrospectionRequest($request); + } + + /** + * Returns the introspector + * + * @return Introspector + */ + private function getIntrospector() + { + if (!isset($this->introspector)){ + $this->introspector = new Introspector($this->accessTokenRepository, $this->privateKey, new Parser); + } + + return $this->introspector; + } + /** * Get the token type that grants will return in the HTTP response. * diff --git a/src/Introspector.php b/src/Introspector.php index db91b787f..455435031 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -8,6 +8,7 @@ use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; use Lcobucci\JWT\ValidationData; +use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use Psr\Http\Message\ServerRequestInterface; @@ -46,6 +47,20 @@ public function __construct( $this->parser = $parser; } + /** + * Validate the request + * + * @param ServerRequestInterface $request + * + * @throws OAuthServerException + */ + public function validateIntrospectionRequest(ServerRequestInterface $request) + { + if ($request->getMethod() !== 'POST'){ + throw OAuthServerException::accessDenied('Invalid request method'); + } + } + /** * Return an introspection response. * diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 3638cc48b..fcd795d2c 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -5,6 +5,7 @@ use Lcobucci\JWT\Parser; use Lcobucci\JWT\Token; use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Introspector; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; @@ -19,6 +20,28 @@ public function setUp() chmod(__DIR__ . '/Stubs/private.key', 0600); } + public function testGetRequest() + { + $introspector = new Introspector( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + new Parser() + ); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getMethod')->willReturn('GET'); + $this->expectException(OAuthServerException::class); + + try { + $introspectionResponse = $introspector->validateIntrospectionRequest($requestMock); + } catch (OAuthServerException $e) { + $this->assertEquals('access_denied', $e->getErrorType()); + $this->assertEquals(401, $e->getHttpStatusCode()); + + throw $e; + } + } + public function testRespondToRequestWithoutToken() { $introspector = new Introspector( From e4b49c64f34e02e465dcbc57a8da95c102b59b1d Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 13:47:54 +0100 Subject: [PATCH 13/34] add test for extra params --- src/ResponseTypes/IntrospectionResponse.php | 41 ++++++++++++------ tests/IntrospectorTest.php | 47 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index d1f5ed04f..2cc3e9138 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -26,18 +26,13 @@ private function hasToken() { return $this->token !== null; } + /** - * Extract the introspection params from the token + * @return array */ - public function getIntrospectionParams() + private function validTokenResponse() { - if (!$this->hasToken()) { - return [ - 'active' => false, - ]; - } - - return [ + $responseParams = [ 'active' => true, 'token_type' => 'access_token', 'scope' => $this->token->getClaim('scopes', ''), @@ -47,6 +42,30 @@ public function getIntrospectionParams() 'sub' => $this->token->getClaim('sub'), 'jti' => $this->token->getClaim('jti'), ]; + + return array_merge($this->getExtraParams(), $responseParams); + } + + /** + * @return array + */ + private function invalidTokenResponse() + { + return [ + 'active' => false, + ]; + } + + /** + * Extract the introspection params from the token + * + * @return array + */ + public function getIntrospectionParams() + { + return $this->hasToken() ? + $this->validTokenResponse() : + $this->invalidTokenResponse(); } /** @@ -58,10 +77,6 @@ public function generateHttpResponse(ResponseInterface $response) { $responseParams = $this->getIntrospectionParams(); - if ($this->hasToken()) { - $responseParams = array_merge($this->getExtraParams(), $responseParams); - } - $response = $response ->withStatus(200) ->withHeader('pragma', 'no-cache') diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index fcd795d2c..a21508075 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -194,4 +194,51 @@ public function testRespondToRequestWithValidToken() $introspectionResponse->getIntrospectionParams() ); } + + public function testRespondToRequestWithValidTokenWithExtraParams() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $parserMock = $this->getMockBuilder(Parser::class)->getMock(); + $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + + $introspector = new Introspector( + $accessTokenRepositoryMock, + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + $parserMock + ); + + $parserMock->method('parse')->willReturn($tokenMock); + $tokenMock->method('verify')->willReturn(true); + $tokenMock->method('validate')->willReturn(true); + $tokenMock->method('getClaim')->willReturn('value'); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); + + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new class extends IntrospectionResponse { + protected function getExtraParams() + { + return [ + 'custom' => 'parameter' + ]; + } + }); + + $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); + $this->assertEquals( + [ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => 'value', + 'client_id' => 'value', + 'exp' => 'value', + 'iat' => 'value', + 'sub' => 'value', + 'jti' => 'value', + 'custom' => 'parameter', + ], + $introspectionResponse->getIntrospectionParams() + ); + } } From baa74fb4690bab2b408aef1b873eb216707458ad Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 13:51:02 +0100 Subject: [PATCH 14/34] code style fixes --- src/AuthorizationServer.php | 2 +- src/Introspector.php | 2 +- tests/IntrospectorTest.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 1664e52f9..89a49a6ab 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -269,7 +269,7 @@ public function validateIntrospectionRequest(ServerRequestInterface $request) */ private function getIntrospector() { - if (!isset($this->introspector)){ + if (!isset($this->introspector)) { $this->introspector = new Introspector($this->accessTokenRepository, $this->privateKey, new Parser); } diff --git a/src/Introspector.php b/src/Introspector.php index 455435031..3bf6947f5 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -56,7 +56,7 @@ public function __construct( */ public function validateIntrospectionRequest(ServerRequestInterface $request) { - if ($request->getMethod() !== 'POST'){ + if ($request->getMethod() !== 'POST') { throw OAuthServerException::accessDenied('Invalid request method'); } } diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index a21508075..1b6526a63 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -59,7 +59,7 @@ public function testRespondToRequestWithoutToken() $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( [ - 'active' => false + 'active' => false, ], $introspectionResponse->getIntrospectionParams() ); @@ -88,7 +88,7 @@ public function testRespondToRequestWithInvalidToken() $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( [ - 'active' => false + 'active' => false, ], $introspectionResponse->getIntrospectionParams() ); @@ -118,7 +118,7 @@ public function testRespondToRequestWithExpiredToken() $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( [ - 'active' => false + 'active' => false, ], $introspectionResponse->getIntrospectionParams() ); @@ -150,7 +150,7 @@ public function testRespondToRequestWithRevokedToken() $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( [ - 'active' => false + 'active' => false, ], $introspectionResponse->getIntrospectionParams() ); From 8bf9c365f9b0e0927d5b87f3631b4ed728cb643f Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 13:52:15 +0100 Subject: [PATCH 15/34] code style for test --- tests/IntrospectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 1b6526a63..98c90259c 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -220,7 +220,7 @@ public function testRespondToRequestWithValidTokenWithExtraParams() protected function getExtraParams() { return [ - 'custom' => 'parameter' + 'custom' => 'parameter', ]; } }); From 4af0d2ae49c5a562ead47373dc8479a0e6232151 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 14:11:13 +0100 Subject: [PATCH 16/34] add more introspection tests --- tests/IntrospectorTest.php | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 98c90259c..6f2f8f599 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -10,7 +10,9 @@ use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\Response; class IntrospectorTest extends TestCase { @@ -42,6 +44,19 @@ public function testGetRequest() } } + public function testPostRequest() + { + $introspector = new Introspector( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + new Parser() + ); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getMethod')->willReturn('POST'); + $this->assertNull($introspector->validateIntrospectionRequest($requestMock)); + } + public function testRespondToRequestWithoutToken() { $introspector = new Introspector( @@ -241,4 +256,67 @@ protected function getExtraParams() $introspectionResponse->getIntrospectionParams() ); } + + public function testGenerateHttpResponseWithNoToken() + { + $responseType = new IntrospectionResponse(); + + $response = $responseType->generateHttpResponse(new Response()); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('no-cache', $response->getHeader('pragma')[0]); + $this->assertEquals('no-store', $response->getHeader('cache-control')[0]); + $this->assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + + $response->getBody()->rewind(); + $json = json_decode($response->getBody()->getContents()); + + $this->assertAttributeEquals(false, 'active', $json); + } + + public function testGenerateHttpResponseWithValidToken() + { + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $parserMock = $this->getMockBuilder(Parser::class)->getMock(); + $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + + $introspector = new Introspector( + $accessTokenRepositoryMock, + new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), + $parserMock + ); + + $parserMock->method('parse')->willReturn($tokenMock); + $tokenMock->method('verify')->willReturn(true); + $tokenMock->method('validate')->willReturn(true); + $tokenMock->method('getClaim')->willReturn('value'); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); + + $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); + + $response = $introspectionResponse->generateHttpResponse(new Response()); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('no-cache', $response->getHeader('pragma')[0]); + $this->assertEquals('no-store', $response->getHeader('cache-control')[0]); + $this->assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + + $response->getBody()->rewind(); + $json = json_decode($response->getBody()->getContents()); + + $this->assertAttributeEquals(true, 'active', $json); + $this->assertAttributeEquals('access_token', 'token_type', $json); + $this->assertAttributeEquals('value', 'scope', $json); + $this->assertAttributeEquals('value', 'client_id', $json); + $this->assertAttributeEquals('value', 'exp', $json); + $this->assertAttributeEquals('value', 'iat', $json); + $this->assertAttributeEquals('value', 'sub', $json); + $this->assertAttributeEquals('value', 'jti', $json); + } } From b00e6fab70e533a6a5b0eebb3d153da20834c246 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 14:11:20 +0100 Subject: [PATCH 17/34] add introspect example --- examples/public/introspect.php | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/public/introspect.php diff --git a/examples/public/introspect.php b/examples/public/introspect.php new file mode 100644 index 000000000..36a3d9553 --- /dev/null +++ b/examples/public/introspect.php @@ -0,0 +1,59 @@ + function () { + + // Setup the authorization server + $server = new AuthorizationServer( + new ClientRepository(), // instance of ClientRepositoryInterface + new AccessTokenRepository(), // instance of AccessTokenRepositoryInterface + new ScopeRepository(), // instance of ScopeRepositoryInterface + 'file://' . __DIR__ . '/../private.key', // path to private key + 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' // encryption key + ); + + return $server; + }, +]); + +$app->post( + '/introspect', + function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { + + /* @var \League\OAuth2\Server\AuthorizationServer $server */ + $server = $app->getContainer()->get(AuthorizationServer::class); + + try { + // Validate the given introspect request + $server->validateIntrospectionRequest($request); + + // Try to respond to the introspection request + return $server->respondToIntrospectionRequest($request, $response); + } catch (OAuthServerException $exception) { + + // All instances of OAuthServerException can be converted to a PSR-7 response + return $exception->generateHttpResponse($response); + } catch (\Exception $exception) { + + // Catch unexpected exceptions + $body = $response->getBody(); + $body->write($exception->getMessage()); + + return $response->withStatus(500)->withBody($body); + } + } +); + +$app->run(); From 48052434f1f8b0c1f09e71b0f1b35647b7f862e4 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 22 Jul 2018 14:11:58 +0100 Subject: [PATCH 18/34] remove blank linbe --- tests/IntrospectorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 6f2f8f599..fa24d9271 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -277,7 +277,6 @@ public function testGenerateHttpResponseWithNoToken() public function testGenerateHttpResponseWithValidToken() { - $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $parserMock = $this->getMockBuilder(Parser::class)->getMock(); $tokenMock = $this->getMockBuilder(Token::class)->getMock(); From 5eeb6245b4ae5d731944621d034217a540b43d0a Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Tue, 25 Sep 2018 21:03:13 +0100 Subject: [PATCH 19/34] add missing brackets to new class --- src/AuthorizationServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 89a49a6ab..bb07392f9 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -225,7 +225,7 @@ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) protected function getIntrospectionResponseType() { if ($this->introspectionResponseType instanceof IntrospectionResponse === false) { - $this->introspectionResponseType = new IntrospectionResponse; + $this->introspectionResponseType = new IntrospectionResponse(); } return $this->introspectionResponseType; From b0e6eff75ed1147eeaeb6103ded5267c87c2c1c9 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Tue, 25 Sep 2018 21:04:27 +0100 Subject: [PATCH 20/34] update phpdoc to reflect the function --- src/AuthorizationServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index bb07392f9..eabfc30d2 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -252,7 +252,7 @@ public function respondToIntrospectionRequest(ServerRequestInterface $request, R } /** - * Return an introspection response. + * Validate an introspection request. * * @param ServerRequestInterface $request */ From 7def7a8e52e4655786a7e9a2c105ccc1a042e5c1 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Tue, 25 Sep 2018 21:06:49 +0100 Subject: [PATCH 21/34] add missing doc --- src/AuthorizationServer.php | 2 +- src/ResponseTypes/IntrospectionResponse.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index eabfc30d2..0cd7b36b8 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -263,7 +263,7 @@ public function validateIntrospectionRequest(ServerRequestInterface $request) } /** - * Returns the introspector + * Returns the introspector. * * @return Introspector */ diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 2cc3e9138..5f73158f3 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -22,6 +22,11 @@ public function setToken(Token $token) $this->token = $token; } + /** + * Return wether the token has been set + * + * @return boolean + */ private function hasToken() { return $this->token !== null; From 99cb04a56a5e8ad81fe0a217d5f3078b356b92bd Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Tue, 25 Sep 2018 21:15:50 +0100 Subject: [PATCH 22/34] fix return type --- src/ResponseTypes/IntrospectionResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 5f73158f3..248fcf2d9 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -25,7 +25,7 @@ public function setToken(Token $token) /** * Return wether the token has been set * - * @return boolean + * @return bool */ private function hasToken() { From 917562894ae69b17aa02798c4cbf2c2e38e348d5 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Tue, 25 Sep 2018 21:16:10 +0100 Subject: [PATCH 23/34] add missing full stop --- src/ResponseTypes/IntrospectionResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 248fcf2d9..ee64e55d7 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -23,7 +23,7 @@ public function setToken(Token $token) } /** - * Return wether the token has been set + * Return wether the token has been set. * * @return bool */ From d088a3fb5958145b6ab7b3f0bc20465fa538c93e Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 11 Nov 2018 12:32:18 +0000 Subject: [PATCH 24/34] create bearer token validator --- .../BearerTokenValidator.php | 128 ++++++++++++++++ .../BearerTokenValidatorTest.php | 145 ++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 src/IntrospectionValidators/BearerTokenValidator.php create mode 100644 tests/IntrospectionValidators/BearerTokenValidatorTest.php diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php new file mode 100644 index 000000000..52bb21e6f --- /dev/null +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -0,0 +1,128 @@ +accessTokenRepository = $accessTokenRepository; + } + + /** + * Set the public key + * + * @param \League\OAuth2\Server\CryptKey $key + */ + public function setPrivateKey(CryptKey $key) + { + $this->privateKey = $key; + } + + /** + * Validates the given token from the request + * + * @param ServerRequestInterface $request + * @return bool + */ + public function validateIntrospection(ServerRequestInterface $request) + { + try { + $token = $this->getTokenFromRequest($request); + } catch (InvalidArgumentException $e) { + return false; + } + + if ( + $this->isTokenRevoked($token) || + $this->isTokenExpired($token) || + $this->isTokenUnverified($token) + ) { + return false; + } + + return true; + } + + /** + * Gets the token from the request body. + * + * @param ServerRequestInterface $request + * @return Token + */ + public function getTokenFromRequest(ServerRequestInterface $request) + { + $jwt = $request->getParsedBody()['token'] ?? null; + + return (new Parser()) + ->parse($jwt); + } + + /** + * Validate the JWT token. + * + * @param Token $token + * + * @return bool + */ + private function isTokenUnverified(Token $token) + { + $keychain = new Keychain(); + + $key = $keychain->getPrivateKey( + $this->privateKey->getKeyPath(), + $this->privateKey->getPassPhrase() + ); + + return $token->verify(new Sha256(), $key->getContent()) === false; + } + + /** + * Ensure access token hasn't expired + * + * @param Token $token + * + * @return bool + */ + private function isTokenExpired(Token $token) + { + $data = new ValidationData(time()); + + return ! $token->validate($data); + } + + /** + * Check if the given token is revoked. + * + * @param Token $token + * + * @return bool + */ + private function isTokenRevoked(Token $token) + { + return $this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti')); + } +} diff --git a/tests/IntrospectionValidators/BearerTokenValidatorTest.php b/tests/IntrospectionValidators/BearerTokenValidatorTest.php new file mode 100644 index 000000000..a79c009e2 --- /dev/null +++ b/tests/IntrospectionValidators/BearerTokenValidatorTest.php @@ -0,0 +1,145 @@ +getMockBuilder(BearerTokenValidator::class) + ->disableOriginalConstructor() + ->setMethods(['getTokenFromRequest']) + ->getMock(); + + $validator->method('getTokenFromRequest')->will( + $this->throwException(new InvalidArgumentException()) + ); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $this->assertFalse($validator->validateIntrospection($requestMock)); + } + + public function testReturnsFalseWhenTokenIsRevoked() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $accessTokenRepositoryMock->method('isAccessTokenRevoked') + ->willReturn(true); + + $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setConstructorArgs([$accessTokenRepositoryMock]) + ->setMethods(['getTokenFromRequest']) + ->getMock(); + + $tokenMock = $this->getMockBuilder(Token::class) + ->getMock(); + + $validator->method('getTokenFromRequest') + ->willReturn($tokenMock); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $this->assertFalse($validator->validateIntrospection($requestMock)); + } + + public function testReturnsFalseWhenTokenIsExpired() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $accessTokenRepositoryMock->method('isAccessTokenRevoked') + ->willReturn(false); + + $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setConstructorArgs([$accessTokenRepositoryMock]) + ->setMethods(['getTokenFromRequest']) + ->getMock(); + + $tokenMock = $this->getMockBuilder(Token::class) + ->getMock(); + + $tokenMock->method('validate')->willReturn(false); + + $validator->method('getTokenFromRequest') + ->willReturn($tokenMock); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $this->assertFalse($validator->validateIntrospection($requestMock)); + } + + public function testReturnsFalseWhenTokenIsUnverified() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $accessTokenRepositoryMock->method('isAccessTokenRevoked') + ->willReturn(false); + + $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setConstructorArgs([$accessTokenRepositoryMock]) + ->setMethods(['getTokenFromRequest']) + ->getMock(); + + $validator->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $tokenMock = $this->getMockBuilder(Token::class) + ->getMock(); + + $tokenMock->method('validate')->willReturn(true); + $tokenMock->method('verify')->willReturn(false); + + $validator->method('getTokenFromRequest') + ->willReturn($tokenMock); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $this->assertFalse($validator->validateIntrospection($requestMock)); + } + + public function testReturnsTrueWhenTokenIsValid() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $accessTokenRepositoryMock->method('isAccessTokenRevoked') + ->willReturn(false); + + $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setConstructorArgs([$accessTokenRepositoryMock]) + ->setMethods(['getTokenFromRequest']) + ->getMock(); + + $validator->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $tokenMock = $this->getMockBuilder(Token::class) + ->getMock(); + + $tokenMock->method('validate')->willReturn(true); + $tokenMock->method('verify')->willReturn(true); + + $validator->method('getTokenFromRequest') + ->willReturn($tokenMock); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $this->assertTrue($validator->validateIntrospection($requestMock)); + } +} From 17377522cb45a859abf5541612882a0eea191e75 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 11 Nov 2018 12:34:03 +0000 Subject: [PATCH 25/34] add bearer token introspection response --- .../BearerTokenIntrospectionResponse.php | 41 +++++++ .../BearerTokenIntrospectionResponseTest.php | 102 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/ResponseTypes/BearerTokenIntrospectionResponse.php create mode 100644 tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php diff --git a/src/ResponseTypes/BearerTokenIntrospectionResponse.php b/src/ResponseTypes/BearerTokenIntrospectionResponse.php new file mode 100644 index 000000000..4eeb8485c --- /dev/null +++ b/src/ResponseTypes/BearerTokenIntrospectionResponse.php @@ -0,0 +1,41 @@ +getTokenFromRequest(); + + $responseParams = [ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => $token->getClaim('scopes', ''), + 'client_id' => $token->getClaim('aud'), + 'exp' => $token->getClaim('exp'), + 'iat' => $token->getClaim('iat'), + 'sub' => $token->getClaim('sub'), + 'jti' => $token->getClaim('jti'), + ]; + + return array_merge($this->getExtraParams(), $responseParams); + } + + /** + * @return Token + */ +protected function getTokenFromRequest() + { + $jwt = $this->request->getParsedBody()['token'] ?? null; + + return (new Parser()) + ->parse($jwt); + } +} diff --git a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php new file mode 100644 index 000000000..a598edd0b --- /dev/null +++ b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php @@ -0,0 +1,102 @@ +generateHttpResponse(new Response()); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertCorrectIntrospectionHeaders($response); + + $response->getBody()->rewind(); + $json = json_decode($response->getBody()->getContents()); + $this->assertAttributeEquals(false, 'active', $json); + } + + public function testValidIntrospectionResponse() + { + $responseType = $this->getMockBuilder(BearerTokenIntrospectionResponse::class) + ->setMethods(['getTokenFromRequest']) + ->getMock(); + + $tokenMock = $this->getMockBuilder(Token::class) + ->getMock(); + + $tokenMock->method('getClaim')->willReturn('value'); + + $responseType->method('getTokenFromRequest') + ->willReturn($tokenMock); + + $responseType->setValidity(true); + $response = $responseType->generateHttpResponse(new Response()); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertCorrectIntrospectionHeaders($response); + + $response->getBody()->rewind(); + $json = json_decode($response->getBody()->getContents()); + $this->assertAttributeEquals(true, 'active', $json); + $this->assertAttributeEquals('access_token', 'token_type', $json); + $this->assertAttributeEquals('value', 'scope', $json); + $this->assertAttributeEquals('value', 'client_id', $json); + $this->assertAttributeEquals('value', 'exp', $json); + $this->assertAttributeEquals('value', 'iat', $json); + $this->assertAttributeEquals('value', 'sub', $json); + $this->assertAttributeEquals('value', 'jti', $json); + } + + public function testValidIntrospectionResponseWithExtraParams() + { + $responseType = $this->getMockBuilder(BearerTokenIntrospectionResponse::class) + ->setMethods(['getTokenFromRequest', 'getExtraParams']) + ->getMock(); + + $tokenMock = $this->getMockBuilder(Token::class) + ->getMock(); + + $tokenMock->method('getClaim')->willReturn('value'); + + $responseType->method('getTokenFromRequest') + ->willReturn($tokenMock); + + $responseType->method('getExtraParams') + ->willReturn(['extra' => 'param']); + + $responseType->setValidity(true); + $response = $responseType->generateHttpResponse(new Response()); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertCorrectIntrospectionHeaders($response); + + $response->getBody()->rewind(); + $json = json_decode($response->getBody()->getContents()); + $this->assertAttributeEquals(true, 'active', $json); + $this->assertAttributeEquals('access_token', 'token_type', $json); + $this->assertAttributeEquals('value', 'scope', $json); + $this->assertAttributeEquals('value', 'client_id', $json); + $this->assertAttributeEquals('value', 'exp', $json); + $this->assertAttributeEquals('value', 'iat', $json); + $this->assertAttributeEquals('value', 'sub', $json); + $this->assertAttributeEquals('value', 'jti', $json); + $this->assertAttributeEquals('param', 'extra', $json); + + } + + private function assertCorrectIntrospectionHeaders(ResponseInterface $response) + { + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('no-cache', $response->getHeader('pragma')[0]); + $this->assertEquals('no-store', $response->getHeader('cache-control')[0]); + $this->assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + } +} From af2cde202c9477ea1325294cb11466fd1afb45c3 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 11 Nov 2018 12:35:12 +0000 Subject: [PATCH 26/34] add introspection validator interface --- .../IntrospectionValidatorInterface.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/IntrospectionValidators/IntrospectionValidatorInterface.php diff --git a/src/IntrospectionValidators/IntrospectionValidatorInterface.php b/src/IntrospectionValidators/IntrospectionValidatorInterface.php new file mode 100644 index 000000000..e87b01322 --- /dev/null +++ b/src/IntrospectionValidators/IntrospectionValidatorInterface.php @@ -0,0 +1,17 @@ + Date: Sun, 11 Nov 2018 12:36:50 +0000 Subject: [PATCH 27/34] refactor introspection response The JWT logic has been moved from the introspection response and is now in the child class BearerTokenIntrospectionResponse --- src/ResponseTypes/IntrospectionResponse.php | 59 +++++++++++---------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index ee64e55d7..d9bed55f5 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -4,48 +4,45 @@ use Lcobucci\JWT\Token; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; class IntrospectionResponse extends AbstractResponseType { /** - * @var Token + * @var boolean */ - protected $token; + protected $valid = false; /** - * Set the token against the response - * - * @param Token $token + * @var ServerRequestInterface */ - public function setToken(Token $token) + protected $request; + + /** + * @param boolean $bool + * @return void + */ + public function setValidity(bool $bool) { - $this->token = $token; + $this->valid = $bool; } /** - * Return wether the token has been set. - * - * @return bool + * @param ServerRequestInterface $request + * @return void */ - private function hasToken() + public function setRequest(ServerRequestInterface $request) { - return $this->token !== null; + $this->request = $request; } /** * @return array */ - private function validTokenResponse() + protected function validIntrospectionResponse() { $responseParams = [ 'active' => true, - 'token_type' => 'access_token', - 'scope' => $this->token->getClaim('scopes', ''), - 'client_id' => $this->token->getClaim('aud'), - 'exp' => $this->token->getClaim('exp'), - 'iat' => $this->token->getClaim('iat'), - 'sub' => $this->token->getClaim('sub'), - 'jti' => $this->token->getClaim('jti'), ]; return array_merge($this->getExtraParams(), $responseParams); @@ -54,7 +51,7 @@ private function validTokenResponse() /** * @return array */ - private function invalidTokenResponse() + protected function invalidIntrospectionResponse() { return [ 'active' => false, @@ -62,15 +59,23 @@ private function invalidTokenResponse() } /** - * Extract the introspection params from the token + * Extract the introspection response * * @return array */ - public function getIntrospectionParams() + public function getIntrospectionResponseParams() + { + return $this->isValid() ? + $this->validIntrospectionResponse() : + $this->invalidIntrospectionResponse(); + } + + /** + * @return boolean + */ + protected function isValid() { - return $this->hasToken() ? - $this->validTokenResponse() : - $this->invalidTokenResponse(); + return $this->valid === true; } /** @@ -80,7 +85,7 @@ public function getIntrospectionParams() */ public function generateHttpResponse(ResponseInterface $response) { - $responseParams = $this->getIntrospectionParams(); + $responseParams = $this->getIntrospectionResponseParams(); $response = $response ->withStatus(200) From 4611bed761d77ff997679365f9b63ecf78cbe7c8 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 11 Nov 2018 12:40:29 +0000 Subject: [PATCH 28/34] update introspector to use introspection validator interface --- src/AuthorizationServer.php | 38 +++++- src/Introspector.php | 101 +++------------ tests/IntrospectorTest.php | 247 ++---------------------------------- 3 files changed, 70 insertions(+), 316 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 0cd7b36b8..238b99fa3 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -15,11 +15,14 @@ use League\Event\EmitterAwareTrait; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\GrantTypeInterface; +use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator; +use League\OAuth2\Server\IntrospectionValidators\IntrospectionValidatorInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\AbstractResponseType; +use League\OAuth2\Server\ResponseTypes\BearerTokenIntrospectionResponse; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; @@ -60,6 +63,11 @@ class AuthorizationServer implements EmitterAwareInterface */ protected $introspectionResponseType; + /** + * @var null|IntrospectionValidatorInterface + */ + protected $introspectionValidator; + /** * @var null|Introspector */ @@ -217,6 +225,14 @@ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) $this->introspectionResponseType = $reponseType; } + /** + * @param IntrospectionValidatorInterface $introspectionValidator + */ + public function setIntrospectionValidator(IntrospectionValidatorInterface $introspectionValidator) + { + $this->introspectionValidator = $introspectionValidator; + } + /** * Get the introspection response * @@ -225,12 +241,26 @@ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) protected function getIntrospectionResponseType() { if ($this->introspectionResponseType instanceof IntrospectionResponse === false) { - $this->introspectionResponseType = new IntrospectionResponse(); + $this->introspectionResponseType = new BearerTokenIntrospectionResponse(); } return $this->introspectionResponseType; } + /** + * Get the introspection response + * + * @return IntrospectionValidatorInterface + */ + protected function getIntrospectionValidator() + { + if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { + $this->introspectionValidator = new BearerTokenValidator($this->accessTokenRepository); + } + + return $this->introspectionValidator; + } + /** * Return an introspection response. * @@ -270,7 +300,11 @@ public function validateIntrospectionRequest(ServerRequestInterface $request) private function getIntrospector() { if (!isset($this->introspector)) { - $this->introspector = new Introspector($this->accessTokenRepository, $this->privateKey, new Parser); + $this->introspector = new Introspector( + $this->accessTokenRepository, + $this->privateKey, + $this->getIntrospectionValidator() + ); } return $this->introspector; diff --git a/src/Introspector.php b/src/Introspector.php index 3bf6947f5..b744158b1 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -2,13 +2,9 @@ namespace League\OAuth2\Server; -use InvalidArgumentException; -use Lcobucci\JWT\Parser; -use Lcobucci\JWT\Signer\Keychain; -use Lcobucci\JWT\Signer\Rsa\Sha256; -use Lcobucci\JWT\Token; -use Lcobucci\JWT\ValidationData; use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator; +use League\OAuth2\Server\IntrospectionValidators\IntrospectionValidatorInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use Psr\Http\Message\ServerRequestInterface; @@ -26,25 +22,25 @@ class Introspector private $privateKey; /** - * @var Parser + * @var null|IntrospectionValidatorInterface */ - private $parser; + private $introspectionValidator; /** * New Introspector instance. * - * @param AccessTokenRepositoryInterface $accessTokenRepository - * @param CryptKey $privateKey - * @param Parser $parser + * @param AccessTokenRepositoryInterface $accessTokenRepository + * @param CryptKey $privateKey + * @param IntrospectionValidatorInterface $introspectionValidator */ public function __construct( AccessTokenRepositoryInterface $accessTokenRepository, CryptKey $privateKey, - Parser $parser + IntrospectionValidatorInterface $introspectionValidator = null ) { $this->accessTokenRepository = $accessTokenRepository; $this->privateKey = $privateKey; - $this->parser = $parser; + $this->introspectionValidator = $introspectionValidator; } /** @@ -73,81 +69,26 @@ public function respondToIntrospectionRequest( ServerRequestInterface $request, IntrospectionResponse $responseType ) { - $jwt = $request->getParsedBody()['token'] ?? null; + $validator = $this->getIntrospectionValidator(); - try { - $token = $this->parser->parse($jwt); - } catch (InvalidArgumentException $e) { - return $responseType; + if ($validator->validateIntrospection($request)) { + $responseType->setRequest($request); + $responseType->setValidity(true); } - return $this->isTokenValid($token) ? - $this->setTokenOnResponse($token, $responseType) : - $responseType; - } - - /** - * Validate the JWT and make sure it has not expired or been revoked - * - * @return bool - */ - private function isTokenValid(Token $token) - { - return $this->verifyToken($token) && !$this->isTokenExpired($token) && !$this->isTokenRevoked($token); - } - - /** - * Validate the JWT token. - * - * @param Token $token - * - * @return bool - */ - private function verifyToken(Token $token) - { - $keychain = new Keychain(); - $key = $keychain->getPrivateKey($this->privateKey->getKeyPath(), $this->privateKey->getPassPhrase()); - - return $token->verify(new Sha256, $key->getContent()); - } - - /** - * Ensure access token hasn't expired - * - * @param Token $token - * - * @return bool - */ - private function isTokenExpired(Token $token) - { - $data = new ValidationData(time()); - - return !$token->validate($data); - } - - /** - * Check if the given access token is revoked. - * - * @param Token $token - * - * @return bool - */ - private function isTokenRevoked(Token $token) - { - return $this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti')); + return $responseType; } /** - * Create active introspection response. - * - * @param Token $token - * - * @return IntrospectionResponse + * @return IntrospectionValidatorInterface */ - private function setTokenOnResponse(Token $token, IntrospectionResponse $responseType) + protected function getIntrospectionValidator() { - $responseType->setToken($token); + if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { + $this->introspectionValidator = new BearerTokenValidator($this->accessTokenRepository); + $this->introspectionValidator->setPrivateKey($this->privateKey); + } - return $responseType; + return $this->introspectionValidator; } } diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index fa24d9271..111ff0ff9 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -2,17 +2,14 @@ namespace LeagueTests; -use Lcobucci\JWT\Parser; -use Lcobucci\JWT\Token; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\IntrospectionValidators\IntrospectionValidatorInterface; use League\OAuth2\Server\Introspector; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response; class IntrospectorTest extends TestCase { @@ -26,8 +23,7 @@ public function testGetRequest() { $introspector = new Introspector( $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - new Parser() + new CryptKey('file://' . __DIR__ . '/Stubs/private.key') ); $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); @@ -48,8 +44,7 @@ public function testPostRequest() { $introspector = new Introspector( $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - new Parser() + new CryptKey('file://' . __DIR__ . '/Stubs/private.key') ); $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); @@ -57,265 +52,49 @@ public function testPostRequest() $this->assertNull($introspector->validateIntrospectionRequest($requestMock)); } - public function testRespondToRequestWithoutToken() - { - $introspector = new Introspector( - $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - new Parser() - ); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getParsedBody')->willReturn([]); - - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); - - $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertAttributeEquals(null, 'token', $introspectionResponse); - $this->assertEquals( - [ - 'active' => false, - ], - $introspectionResponse->getIntrospectionParams() - ); - } - - public function testRespondToRequestWithInvalidToken() + public function testRespondToInvalidRequest() { - $parserMock = $this->getMockBuilder(Parser::class)->getMock(); - $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + $validator = $this->getMockBuilder(IntrospectionValidatorInterface::class)->getMock(); + $validator->method('validateIntrospection')->willReturn(false); $introspector = new Introspector( $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $parserMock + $validator ); - $parserMock->method('parse')->willReturn($tokenMock); - $tokenMock->method('verify')->willReturn(false); - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertAttributeEquals(null, 'token', $introspectionResponse); $this->assertEquals( [ 'active' => false, ], - $introspectionResponse->getIntrospectionParams() + $introspectionResponse->getIntrospectionResponseParams() ); } - public function testRespondToRequestWithExpiredToken() + public function testRespondToValidRequest() { - $parserMock = $this->getMockBuilder(Parser::class)->getMock(); - $tokenMock = $this->getMockBuilder(Token::class)->getMock(); + $validator = $this->getMockBuilder(IntrospectionValidatorInterface::class)->getMock(); + $validator->method('validateIntrospection')->willReturn(true); $introspector = new Introspector( $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $parserMock - ); - - $parserMock->method('parse')->willReturn($tokenMock); - $tokenMock->method('verify')->willReturn(true); - $tokenMock->method('validate')->willReturn(false); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); - - $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertAttributeEquals(null, 'token', $introspectionResponse); - $this->assertEquals( - [ - 'active' => false, - ], - $introspectionResponse->getIntrospectionParams() - ); - } - - public function testRespondToRequestWithRevokedToken() - { - $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $parserMock = $this->getMockBuilder(Parser::class)->getMock(); - $tokenMock = $this->getMockBuilder(Token::class)->getMock(); - - $introspector = new Introspector( - $accessTokenRepositoryMock, - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $parserMock + $validator ); - $parserMock->method('parse')->willReturn($tokenMock); - $tokenMock->method('verify')->willReturn(true); - $tokenMock->method('validate')->willReturn(true); - $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(true); - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); - - $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertAttributeEquals(null, 'token', $introspectionResponse); - $this->assertEquals( - [ - 'active' => false, - ], - $introspectionResponse->getIntrospectionParams() - ); - } - - public function testRespondToRequestWithValidToken() - { - $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $parserMock = $this->getMockBuilder(Parser::class)->getMock(); - $tokenMock = $this->getMockBuilder(Token::class)->getMock(); - - $introspector = new Introspector( - $accessTokenRepositoryMock, - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $parserMock - ); - - $parserMock->method('parse')->willReturn($tokenMock); - $tokenMock->method('verify')->willReturn(true); - $tokenMock->method('validate')->willReturn(true); - $tokenMock->method('getClaim')->willReturn('value'); - $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); $this->assertEquals( [ 'active' => true, - 'token_type' => 'access_token', - 'scope' => 'value', - 'client_id' => 'value', - 'exp' => 'value', - 'iat' => 'value', - 'sub' => 'value', - 'jti' => 'value', ], - $introspectionResponse->getIntrospectionParams() - ); - } - - public function testRespondToRequestWithValidTokenWithExtraParams() - { - $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $parserMock = $this->getMockBuilder(Parser::class)->getMock(); - $tokenMock = $this->getMockBuilder(Token::class)->getMock(); - - $introspector = new Introspector( - $accessTokenRepositoryMock, - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $parserMock + $introspectionResponse->getIntrospectionResponseParams() ); - - $parserMock->method('parse')->willReturn($tokenMock); - $tokenMock->method('verify')->willReturn(true); - $tokenMock->method('validate')->willReturn(true); - $tokenMock->method('getClaim')->willReturn('value'); - $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new class extends IntrospectionResponse { - protected function getExtraParams() - { - return [ - 'custom' => 'parameter', - ]; - } - }); - - $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertEquals( - [ - 'active' => true, - 'token_type' => 'access_token', - 'scope' => 'value', - 'client_id' => 'value', - 'exp' => 'value', - 'iat' => 'value', - 'sub' => 'value', - 'jti' => 'value', - 'custom' => 'parameter', - ], - $introspectionResponse->getIntrospectionParams() - ); - } - - public function testGenerateHttpResponseWithNoToken() - { - $responseType = new IntrospectionResponse(); - - $response = $responseType->generateHttpResponse(new Response()); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals('no-cache', $response->getHeader('pragma')[0]); - $this->assertEquals('no-store', $response->getHeader('cache-control')[0]); - $this->assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); - - $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents()); - - $this->assertAttributeEquals(false, 'active', $json); - } - - public function testGenerateHttpResponseWithValidToken() - { - $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $parserMock = $this->getMockBuilder(Parser::class)->getMock(); - $tokenMock = $this->getMockBuilder(Token::class)->getMock(); - - $introspector = new Introspector( - $accessTokenRepositoryMock, - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $parserMock - ); - - $parserMock->method('parse')->willReturn($tokenMock); - $tokenMock->method('verify')->willReturn(true); - $tokenMock->method('validate')->willReturn(true); - $tokenMock->method('getClaim')->willReturn('value'); - $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getParsedBody')->willReturn(['token' => 'token']); - - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); - - $response = $introspectionResponse->generateHttpResponse(new Response()); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals('no-cache', $response->getHeader('pragma')[0]); - $this->assertEquals('no-store', $response->getHeader('cache-control')[0]); - $this->assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); - - $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents()); - - $this->assertAttributeEquals(true, 'active', $json); - $this->assertAttributeEquals('access_token', 'token_type', $json); - $this->assertAttributeEquals('value', 'scope', $json); - $this->assertAttributeEquals('value', 'client_id', $json); - $this->assertAttributeEquals('value', 'exp', $json); - $this->assertAttributeEquals('value', 'iat', $json); - $this->assertAttributeEquals('value', 'sub', $json); - $this->assertAttributeEquals('value', 'jti', $json); } } From 27450599865a801d0681e5e8ef9abd40297fb1b2 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 11 Nov 2018 12:43:22 +0000 Subject: [PATCH 29/34] fix code style --- src/AuthorizationServer.php | 1 - src/IntrospectionValidators/BearerTokenValidator.php | 4 +++- src/ResponseTypes/BearerTokenIntrospectionResponse.php | 2 +- src/ResponseTypes/IntrospectionResponse.php | 9 +++------ .../IntrospectionValidators/BearerTokenValidatorTest.php | 2 -- .../BearerTokenIntrospectionResponseTest.php | 1 - 6 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 238b99fa3..e1798c536 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -10,7 +10,6 @@ namespace League\OAuth2\Server; use Defuse\Crypto\Key; -use Lcobucci\JWT\Parser; use League\Event\EmitterAwareInterface; use League\Event\EmitterAwareTrait; use League\OAuth2\Server\Exception\OAuthServerException; diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php index 52bb21e6f..1f2536d20 100644 --- a/src/IntrospectionValidators/BearerTokenValidator.php +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -46,6 +46,7 @@ public function setPrivateKey(CryptKey $key) * Validates the given token from the request * * @param ServerRequestInterface $request + * * @return bool */ public function validateIntrospection(ServerRequestInterface $request) @@ -71,6 +72,7 @@ public function validateIntrospection(ServerRequestInterface $request) * Gets the token from the request body. * * @param ServerRequestInterface $request + * * @return Token */ public function getTokenFromRequest(ServerRequestInterface $request) @@ -111,7 +113,7 @@ private function isTokenExpired(Token $token) { $data = new ValidationData(time()); - return ! $token->validate($data); + return !$token->validate($data); } /** diff --git a/src/ResponseTypes/BearerTokenIntrospectionResponse.php b/src/ResponseTypes/BearerTokenIntrospectionResponse.php index 4eeb8485c..d7e22e458 100644 --- a/src/ResponseTypes/BearerTokenIntrospectionResponse.php +++ b/src/ResponseTypes/BearerTokenIntrospectionResponse.php @@ -31,7 +31,7 @@ protected function validIntrospectionResponse() /** * @return Token */ -protected function getTokenFromRequest() + protected function getTokenFromRequest() { $jwt = $this->request->getParsedBody()['token'] ?? null; diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index d9bed55f5..521cb3484 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -2,14 +2,13 @@ namespace League\OAuth2\Server\ResponseTypes; -use Lcobucci\JWT\Token; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class IntrospectionResponse extends AbstractResponseType { /** - * @var boolean + * @var bool */ protected $valid = false; @@ -19,8 +18,7 @@ class IntrospectionResponse extends AbstractResponseType protected $request; /** - * @param boolean $bool - * @return void + * @param bool $bool */ public function setValidity(bool $bool) { @@ -29,7 +27,6 @@ public function setValidity(bool $bool) /** * @param ServerRequestInterface $request - * @return void */ public function setRequest(ServerRequestInterface $request) { @@ -71,7 +68,7 @@ public function getIntrospectionResponseParams() } /** - * @return boolean + * @return bool */ protected function isValid() { diff --git a/tests/IntrospectionValidators/BearerTokenValidatorTest.php b/tests/IntrospectionValidators/BearerTokenValidatorTest.php index a79c009e2..a1dd25bf7 100644 --- a/tests/IntrospectionValidators/BearerTokenValidatorTest.php +++ b/tests/IntrospectionValidators/BearerTokenValidatorTest.php @@ -2,7 +2,6 @@ namespace LeagueTests\IntrospectionValidators; -use Lcobucci\JWT\Builder; use Lcobucci\JWT\Token; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator; @@ -10,7 +9,6 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use SebastianBergmann\CodeCoverage\InvalidArgumentException; -use Zend\Diactoros\ServerRequest; class BearerTokenValidatorTest extends TestCase { diff --git a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php index a598edd0b..ac755ed56 100644 --- a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php +++ b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php @@ -89,7 +89,6 @@ public function testValidIntrospectionResponseWithExtraParams() $this->assertAttributeEquals('value', 'sub', $json); $this->assertAttributeEquals('value', 'jti', $json); $this->assertAttributeEquals('param', 'extra', $json); - } private function assertCorrectIntrospectionHeaders(ResponseInterface $response) From 66f9843bfa30cecbd5d4cc57af2544dcfcb04bd1 Mon Sep 17 00:00:00 2001 From: Steve Porter Date: Sun, 11 Nov 2018 13:27:20 +0000 Subject: [PATCH 30/34] update phpdoc --- src/AuthorizationServer.php | 6 +++++- .../BearerTokenValidator.php | 12 ++++-------- .../IntrospectionValidatorInterface.php | 2 +- src/Introspector.php | 4 +++- .../BearerTokenIntrospectionResponse.php | 4 ++++ src/ResponseTypes/IntrospectionResponse.php | 14 +++++++++++++- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index e1798c536..a147d5989 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -217,6 +217,8 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res } /** + * Set the introspection response type. + * * @param IntrospectionResponse $reponseType */ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) @@ -225,6 +227,8 @@ public function setIntrospectionReponseType(IntrospectionResponse $reponseType) } /** + * Set the validator used for introspection requests. + * * @param IntrospectionValidatorInterface $introspectionValidator */ public function setIntrospectionValidator(IntrospectionValidatorInterface $introspectionValidator) @@ -233,7 +237,7 @@ public function setIntrospectionValidator(IntrospectionValidatorInterface $intro } /** - * Get the introspection response + * Get the introspection response. * * @return IntrospectionResponse */ diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php index 1f2536d20..7b9aa5f3d 100644 --- a/src/IntrospectionValidators/BearerTokenValidator.php +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -33,7 +33,7 @@ public function __construct(AccessTokenRepositoryInterface $accessTokenRepositor } /** - * Set the public key + * Set the private key. * * @param \League\OAuth2\Server\CryptKey $key */ @@ -43,11 +43,7 @@ public function setPrivateKey(CryptKey $key) } /** - * Validates the given token from the request - * - * @param ServerRequestInterface $request - * - * @return bool + * {@inheritdoc} */ public function validateIntrospection(ServerRequestInterface $request) { @@ -84,7 +80,7 @@ public function getTokenFromRequest(ServerRequestInterface $request) } /** - * Validate the JWT token. + * Checks whether the token is unverified. * * @param Token $token * @@ -103,7 +99,7 @@ private function isTokenUnverified(Token $token) } /** - * Ensure access token hasn't expired + * Ensure access token hasn't expired. * * @param Token $token * diff --git a/src/IntrospectionValidators/IntrospectionValidatorInterface.php b/src/IntrospectionValidators/IntrospectionValidatorInterface.php index e87b01322..4c004e242 100644 --- a/src/IntrospectionValidators/IntrospectionValidatorInterface.php +++ b/src/IntrospectionValidators/IntrospectionValidatorInterface.php @@ -7,7 +7,7 @@ interface IntrospectionValidatorInterface { /** - * Determine wether the introspection request is valid + * Determine wether the introspection request is valid. * * @param ServerRequestInterface $request * diff --git a/src/Introspector.php b/src/Introspector.php index b744158b1..67b83175d 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -44,7 +44,7 @@ public function __construct( } /** - * Validate the request + * Validate the introspection request. * * @param ServerRequestInterface $request * @@ -80,6 +80,8 @@ public function respondToIntrospectionRequest( } /** + * Get the introspection validator, falling back to the bearer token validator if not set. + * * @return IntrospectionValidatorInterface */ protected function getIntrospectionValidator() diff --git a/src/ResponseTypes/BearerTokenIntrospectionResponse.php b/src/ResponseTypes/BearerTokenIntrospectionResponse.php index d7e22e458..a784c893f 100644 --- a/src/ResponseTypes/BearerTokenIntrospectionResponse.php +++ b/src/ResponseTypes/BearerTokenIntrospectionResponse.php @@ -8,6 +8,8 @@ class BearerTokenIntrospectionResponse extends IntrospectionResponse { /** + * Add the token data to the response. + * * @return array */ protected function validIntrospectionResponse() @@ -29,6 +31,8 @@ protected function validIntrospectionResponse() } /** + * Gets the token from the request body. + * * @return Token */ protected function getTokenFromRequest() diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 521cb3484..479834ca8 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -18,6 +18,8 @@ class IntrospectionResponse extends AbstractResponseType protected $request; /** + * Set the validity of the response. + * * @param bool $bool */ public function setValidity(bool $bool) @@ -26,6 +28,8 @@ public function setValidity(bool $bool) } /** + * Set the request. + * * @param ServerRequestInterface $request */ public function setRequest(ServerRequestInterface $request) @@ -34,6 +38,8 @@ public function setRequest(ServerRequestInterface $request) } /** + * Return the valid introspection parameters. + * * @return array */ protected function validIntrospectionResponse() @@ -46,6 +52,8 @@ protected function validIntrospectionResponse() } /** + * Return the invalid introspection parameters. + * * @return array */ protected function invalidIntrospectionResponse() @@ -56,7 +64,7 @@ protected function invalidIntrospectionResponse() } /** - * Extract the introspection response + * Extract the introspection response. * * @return array */ @@ -68,6 +76,8 @@ public function getIntrospectionResponseParams() } /** + * Check if the response is valid. + * * @return bool */ protected function isValid() @@ -76,6 +86,8 @@ protected function isValid() } /** + * Generate a HTTP response. + * * @param ResponseInterface $response * * @return ResponseInterface From f283fa8e44dcc4c5c3043847770bd588480ebaef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=B3l?= Date: Wed, 8 Dec 2021 20:55:44 +0100 Subject: [PATCH 31/34] Upgrade introspection codebase, fix deprecations, fix tests --- src/AuthorizationServer.php | 8 +- .../BearerTokenValidator.php | 88 ++++++----- src/Introspector.php | 2 +- .../BearerTokenIntrospectionResponse.php | 54 ++++++- .../BearerTokenValidatorTest.php | 138 +++++++++--------- tests/IntrospectorTest.php | 2 +- .../BearerTokenIntrospectionResponseTest.php | 72 +++++---- 7 files changed, 216 insertions(+), 148 deletions(-) diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 14c04640a..417c3b0d9 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -234,11 +234,11 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res /** * Set the introspection response type. * - * @param IntrospectionResponse $reponseType + * @param IntrospectionResponse $responseType */ - public function setIntrospectionReponseType(IntrospectionResponse $reponseType) + public function setIntrospectionResponseType(IntrospectionResponse $responseType) { - $this->introspectionResponseType = $reponseType; + $this->introspectionResponseType = $responseType; } /** @@ -274,6 +274,8 @@ protected function getIntrospectionValidator() { if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { $this->introspectionValidator = new BearerTokenValidator($this->accessTokenRepository); + + $this->introspectionValidator->setPublicKey($this->publicKey); } return $this->introspectionValidator; diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php index 7b9aa5f3d..f2e7ec15a 100644 --- a/src/IntrospectionValidators/BearerTokenValidator.php +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -2,12 +2,16 @@ namespace League\OAuth2\Server\IntrospectionValidators; +use DateTimeZone; use InvalidArgumentException; -use Lcobucci\JWT\Parser; -use Lcobucci\JWT\Signer\Keychain; +use Lcobucci\Clock\SystemClock; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; -use Lcobucci\JWT\ValidationData; +use Lcobucci\JWT\Validation\Constraint\LooseValidAt; +use Lcobucci\JWT\Validation\Constraint\SignedWith; +use Lcobucci\JWT\Validation\Constraint\StrictValidAt; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -20,9 +24,14 @@ class BearerTokenValidator implements IntrospectionValidatorInterface private $accessTokenRepository; /** - * @var \League\OAuth2\Server\CryptKey + * @var CryptKey */ - protected $privateKey; + protected $publicKey; + + /** + * @var Configuration + */ + protected $jwtConfiguration; /** * @param AccessTokenRepositoryInterface $accessTokenRepository @@ -35,11 +44,34 @@ public function __construct(AccessTokenRepositoryInterface $accessTokenRepositor /** * Set the private key. * - * @param \League\OAuth2\Server\CryptKey $key + * @param CryptKey $key + */ + public function setPublicKey(CryptKey $key) + { + $this->publicKey = $key; + + $this->initJwtConfiguration(); + } + + /** + * Initialise the JWT configuration. */ - public function setPrivateKey(CryptKey $key) + private function initJwtConfiguration() { - $this->privateKey = $key; + $this->jwtConfiguration = Configuration::forSymmetricSigner( + new Sha256(), + InMemory::plainText('') + ); + + $this->jwtConfiguration->setValidationConstraints( + \class_exists(StrictValidAt::class) + ? new StrictValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))) + : new LooseValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))), + new SignedWith( + new Sha256(), + InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '') + ) + ); } /** @@ -54,9 +86,8 @@ public function validateIntrospection(ServerRequestInterface $request) } if ( - $this->isTokenRevoked($token) || - $this->isTokenExpired($token) || - $this->isTokenUnverified($token) + !$this->isTokenValid($token) || + $this->isTokenRevoked($token) ) { return false; } @@ -75,52 +106,33 @@ public function getTokenFromRequest(ServerRequestInterface $request) { $jwt = $request->getParsedBody()['token'] ?? null; - return (new Parser()) + return $this->jwtConfiguration->parser() ->parse($jwt); } /** - * Checks whether the token is unverified. + * Check if the given token is revoked. * * @param Token $token * * @return bool */ - private function isTokenUnverified(Token $token) + private function isTokenRevoked(Token $token) { - $keychain = new Keychain(); - - $key = $keychain->getPrivateKey( - $this->privateKey->getKeyPath(), - $this->privateKey->getPassPhrase() - ); - - return $token->verify(new Sha256(), $key->getContent()) === false; + return $this->accessTokenRepository->isAccessTokenRevoked($token->claims()->get('jti')); } /** - * Ensure access token hasn't expired. + * Check if the given token is valid * * @param Token $token * * @return bool */ - private function isTokenExpired(Token $token) + private function isTokenValid(Token $token) { - $data = new ValidationData(time()); + $constraints = $this->jwtConfiguration->validationConstraints(); - return !$token->validate($data); - } - - /** - * Check if the given token is revoked. - * - * @param Token $token - * - * @return bool - */ - private function isTokenRevoked(Token $token) - { - return $this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti')); + return $this->jwtConfiguration->validator()->validate($token, ...$constraints); } } diff --git a/src/Introspector.php b/src/Introspector.php index 67b83175d..412f524bf 100644 --- a/src/Introspector.php +++ b/src/Introspector.php @@ -88,7 +88,7 @@ protected function getIntrospectionValidator() { if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { $this->introspectionValidator = new BearerTokenValidator($this->accessTokenRepository); - $this->introspectionValidator->setPrivateKey($this->privateKey); + $this->introspectionValidator->setPublicKey($this->privateKey); } return $this->introspectionValidator; diff --git a/src/ResponseTypes/BearerTokenIntrospectionResponse.php b/src/ResponseTypes/BearerTokenIntrospectionResponse.php index a784c893f..5a8a71307 100644 --- a/src/ResponseTypes/BearerTokenIntrospectionResponse.php +++ b/src/ResponseTypes/BearerTokenIntrospectionResponse.php @@ -2,11 +2,35 @@ namespace League\OAuth2\Server\ResponseTypes; -use Lcobucci\JWT\Parser; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; +use Lcobucci\JWT\UnencryptedToken; class BearerTokenIntrospectionResponse extends IntrospectionResponse { + /** + * @var Configuration|null + */ + private $jwtConfiguration; + + public function __construct() + { + $this->initJwtConfiguration(); + } + + + /** + * Initialise the JWT configuration. + */ + private function initJwtConfiguration() + { + $this->jwtConfiguration = Configuration::forSymmetricSigner( + new Sha256(), InMemory::plainText('') + ); + } + /** * Add the token data to the response. * @@ -19,12 +43,12 @@ protected function validIntrospectionResponse() $responseParams = [ 'active' => true, 'token_type' => 'access_token', - 'scope' => $token->getClaim('scopes', ''), - 'client_id' => $token->getClaim('aud'), - 'exp' => $token->getClaim('exp'), - 'iat' => $token->getClaim('iat'), - 'sub' => $token->getClaim('sub'), - 'jti' => $token->getClaim('jti'), + 'scope' => $this->getClaimFromToken($token, 'scopes', ''), + 'client_id' => $this->getClaimFromToken($token, 'aud'), + 'exp' => $this->getClaimFromToken($token, 'exp'), + 'iat' => $this->getClaimFromToken($token, 'iat'), + 'sub' => $this->getClaimFromToken($token, 'sub'), + 'jti' => $this->getClaimFromToken($token, 'jti'), ]; return array_merge($this->getExtraParams(), $responseParams); @@ -39,7 +63,21 @@ protected function getTokenFromRequest() { $jwt = $this->request->getParsedBody()['token'] ?? null; - return (new Parser()) + return $this->jwtConfiguration->parser() ->parse($jwt); } + + /** + * Gets single claim from the JWT token. + * + * @param UnencryptedToken $token + * @param string $claim + * @param mixed|null $default + * + * @return mixed + */ + protected function getClaimFromToken(UnencryptedToken $token, string $claim, $default = null) + { + return $token->claims()->get($claim, $default); + } } diff --git a/tests/IntrospectionValidators/BearerTokenValidatorTest.php b/tests/IntrospectionValidators/BearerTokenValidatorTest.php index a1dd25bf7..f8ad1e175 100644 --- a/tests/IntrospectionValidators/BearerTokenValidatorTest.php +++ b/tests/IntrospectionValidators/BearerTokenValidatorTest.php @@ -2,12 +2,17 @@ namespace LeagueTests\IntrospectionValidators; -use Lcobucci\JWT\Token; +use DateInterval; +use DateTimeImmutable; +use Laminas\Diactoros\ServerRequest; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Rsa\Sha256; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; +use ReflectionClass; use SebastianBergmann\CodeCoverage\InvalidArgumentException; class BearerTokenValidatorTest extends TestCase @@ -37,21 +42,26 @@ public function testReturnsFalseWhenTokenIsRevoked() $accessTokenRepositoryMock->method('isAccessTokenRevoked') ->willReturn(true); - $validator = $this->getMockBuilder(BearerTokenValidator::class) - ->setConstructorArgs([$accessTokenRepositoryMock]) - ->setMethods(['getTokenFromRequest']) - ->getMock(); + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); - $tokenMock = $this->getMockBuilder(Token::class) - ->getMock(); + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); - $validator->method('getTokenFromRequest') - ->willReturn($tokenMock); + $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); - $requestMock = $this->getMockBuilder(ServerRequestInterface::class) - ->getMock(); + $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); - $this->assertFalse($validator->validateIntrospection($requestMock)); + $this->assertFalse($bearerTokenValidator->validateIntrospection($request)); } public function testReturnsFalseWhenTokenIsExpired() @@ -59,56 +69,53 @@ public function testReturnsFalseWhenTokenIsExpired() $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) ->getMock(); - $accessTokenRepositoryMock->method('isAccessTokenRevoked') - ->willReturn(false); + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); - $validator = $this->getMockBuilder(BearerTokenValidator::class) - ->setConstructorArgs([$accessTokenRepositoryMock]) - ->setMethods(['getTokenFromRequest']) - ->getMock(); - - $tokenMock = $this->getMockBuilder(Token::class) - ->getMock(); + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); - $tokenMock->method('validate')->willReturn(false); + $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->sub(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); - $validator->method('getTokenFromRequest') - ->willReturn($tokenMock); + $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); - $requestMock = $this->getMockBuilder(ServerRequestInterface::class) - ->getMock(); - - $this->assertFalse($validator->validateIntrospection($requestMock)); + $this->assertFalse($bearerTokenValidator->validateIntrospection($request)); } - public function testReturnsFalseWhenTokenIsUnverified() + public function testReturnsFalseWhenTokenIsInvalid() { $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) ->getMock(); - $accessTokenRepositoryMock->method('isAccessTokenRevoked') - ->willReturn(false); - - $validator = $this->getMockBuilder(BearerTokenValidator::class) - ->setConstructorArgs([$accessTokenRepositoryMock]) - ->setMethods(['getTokenFromRequest']) - ->getMock(); - - $validator->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - - $tokenMock = $this->getMockBuilder(Token::class) - ->getMock(); + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); - $tokenMock->method('validate')->willReturn(true); - $tokenMock->method('verify')->willReturn(false); + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); - $validator->method('getTokenFromRequest') - ->willReturn($tokenMock); + $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key.crlf')); - $requestMock = $this->getMockBuilder(ServerRequestInterface::class) - ->getMock(); + $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); - $this->assertFalse($validator->validateIntrospection($requestMock)); + $this->assertFalse($bearerTokenValidator->validateIntrospection($request)); } public function testReturnsTrueWhenTokenIsValid() @@ -116,28 +123,25 @@ public function testReturnsTrueWhenTokenIsValid() $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) ->getMock(); - $accessTokenRepositoryMock->method('isAccessTokenRevoked') - ->willReturn(false); + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); - $validator = $this->getMockBuilder(BearerTokenValidator::class) - ->setConstructorArgs([$accessTokenRepositoryMock]) - ->setMethods(['getTokenFromRequest']) - ->getMock(); + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); - $validator->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - - $tokenMock = $this->getMockBuilder(Token::class) - ->getMock(); + $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); - $tokenMock->method('validate')->willReturn(true); - $tokenMock->method('verify')->willReturn(true); - - $validator->method('getTokenFromRequest') - ->willReturn($tokenMock); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class) - ->getMock(); + $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); - $this->assertTrue($validator->validateIntrospection($requestMock)); + $this->assertTrue($bearerTokenValidator->validateIntrospection($request)); } } diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php index 111ff0ff9..172b5292a 100644 --- a/tests/IntrospectorTest.php +++ b/tests/IntrospectorTest.php @@ -13,7 +13,7 @@ class IntrospectorTest extends TestCase { - public function setUp() + public function setUp(): void { // Make sure the keys have the correct permissions. chmod(__DIR__ . '/Stubs/private.key', 0600); diff --git a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php index ac755ed56..28ef0a1de 100644 --- a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php +++ b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php @@ -6,7 +6,7 @@ use League\OAuth2\Server\ResponseTypes\BearerTokenIntrospectionResponse; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; -use Zend\Diactoros\Response; +use Laminas\Diactoros\Response; class BearerTokenIntrospectionResponseTest extends TestCase { @@ -19,23 +19,26 @@ public function testInvalidIntrospectionResponse() $this->assertCorrectIntrospectionHeaders($response); $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents()); - $this->assertAttributeEquals(false, 'active', $json); + $json = json_decode($response->getBody()->getContents(), true); + $this->assertEquals([ + 'active' => false + ], $json); } public function testValidIntrospectionResponse() { $responseType = $this->getMockBuilder(BearerTokenIntrospectionResponse::class) - ->setMethods(['getTokenFromRequest']) + ->onlyMethods([ + 'getTokenFromRequest', + 'getClaimFromToken' + ]) ->getMock(); $tokenMock = $this->getMockBuilder(Token::class) ->getMock(); - $tokenMock->method('getClaim')->willReturn('value'); - - $responseType->method('getTokenFromRequest') - ->willReturn($tokenMock); + $responseType->method('getTokenFromRequest')->willReturn($tokenMock); + $responseType->method('getClaimFromToken')->willReturn('value'); $responseType->setValidity(true); $response = $responseType->generateHttpResponse(new Response()); @@ -44,31 +47,38 @@ public function testValidIntrospectionResponse() $this->assertCorrectIntrospectionHeaders($response); $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents()); - $this->assertAttributeEquals(true, 'active', $json); - $this->assertAttributeEquals('access_token', 'token_type', $json); - $this->assertAttributeEquals('value', 'scope', $json); - $this->assertAttributeEquals('value', 'client_id', $json); - $this->assertAttributeEquals('value', 'exp', $json); - $this->assertAttributeEquals('value', 'iat', $json); - $this->assertAttributeEquals('value', 'sub', $json); - $this->assertAttributeEquals('value', 'jti', $json); + $json = json_decode($response->getBody()->getContents(), true); + $this->assertEquals([ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => 'value', + 'client_id' => 'value', + 'exp' => 'value', + 'iat' => 'value', + 'sub' => 'value', + 'jti' => 'value', + ], $json); } public function testValidIntrospectionResponseWithExtraParams() { $responseType = $this->getMockBuilder(BearerTokenIntrospectionResponse::class) - ->setMethods(['getTokenFromRequest', 'getExtraParams']) + ->onlyMethods([ + 'getTokenFromRequest', + 'getClaimFromToken', + 'getExtraParams' + ]) ->getMock(); $tokenMock = $this->getMockBuilder(Token::class) ->getMock(); - $tokenMock->method('getClaim')->willReturn('value'); - $responseType->method('getTokenFromRequest') ->willReturn($tokenMock); + $responseType->method('getClaimFromToken') + ->willReturn('value'); + $responseType->method('getExtraParams') ->willReturn(['extra' => 'param']); @@ -79,16 +89,18 @@ public function testValidIntrospectionResponseWithExtraParams() $this->assertCorrectIntrospectionHeaders($response); $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents()); - $this->assertAttributeEquals(true, 'active', $json); - $this->assertAttributeEquals('access_token', 'token_type', $json); - $this->assertAttributeEquals('value', 'scope', $json); - $this->assertAttributeEquals('value', 'client_id', $json); - $this->assertAttributeEquals('value', 'exp', $json); - $this->assertAttributeEquals('value', 'iat', $json); - $this->assertAttributeEquals('value', 'sub', $json); - $this->assertAttributeEquals('value', 'jti', $json); - $this->assertAttributeEquals('param', 'extra', $json); + $json = json_decode($response->getBody()->getContents(), true); + $this->assertEquals([ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => 'value', + 'client_id' => 'value', + 'iat' => 'value', + 'exp' => 'value', + 'sub' => 'value', + 'jti' => 'value', + 'extra' => 'param' + ], $json); } private function assertCorrectIntrospectionHeaders(ResponseInterface $response) From 97a4ddcc1ad70bd281648dd1c4a7294f92f2009b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=B3l?= Date: Sat, 11 Dec 2021 21:27:24 +0100 Subject: [PATCH 32/34] Refactor Introspection Solution --- examples/public/introspect.php | 28 +-- src/AuthorizationServer.php | 118 ------------ src/IntrospectionServer.php | 122 ++++++++++++ .../BearerTokenValidator.php | 37 ++-- .../IntrospectionValidatorInterface.php | 7 +- src/Introspector.php | 96 ---------- .../AbstractResponseType.php} | 18 +- .../BearerTokenResponse.php} | 22 +-- .../Introspection/ResponseTypeInterface.php | 17 ++ tests/IntrospectionServerTest.php | 181 ++++++++++++++++++ .../BearerTokenValidatorTest.php | 74 +++++-- tests/IntrospectorTest.php | 100 ---------- .../BearerTokenIntrospectionResponseTest.php | 15 +- 13 files changed, 452 insertions(+), 383 deletions(-) create mode 100644 src/IntrospectionServer.php delete mode 100644 src/Introspector.php rename src/ResponseTypes/{IntrospectionResponse.php => Introspection/AbstractResponseType.php} (81%) rename src/ResponseTypes/{BearerTokenIntrospectionResponse.php => Introspection/BearerTokenResponse.php} (75%) create mode 100644 src/ResponseTypes/Introspection/ResponseTypeInterface.php create mode 100644 tests/IntrospectionServerTest.php delete mode 100644 tests/IntrospectorTest.php diff --git a/examples/public/introspect.php b/examples/public/introspect.php index 36a3d9553..b97090e02 100644 --- a/examples/public/introspect.php +++ b/examples/public/introspect.php @@ -1,10 +1,9 @@ function () { + IntrospectionServer::class => function () { // Setup the authorization server - $server = new AuthorizationServer( - new ClientRepository(), // instance of ClientRepositoryInterface - new AccessTokenRepository(), // instance of AccessTokenRepositoryInterface - new ScopeRepository(), // instance of ScopeRepositoryInterface - 'file://' . __DIR__ . '/../private.key', // path to private key - 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' // encryption key + $server = new IntrospectionServer( + new AccessTokenRepository(), + 'file://' . __DIR__ . '/../public.key' ); return $server; @@ -31,21 +27,17 @@ $app->post( '/introspect', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { - - /* @var \League\OAuth2\Server\AuthorizationServer $server */ - $server = $app->getContainer()->get(AuthorizationServer::class); + /* @var IntrospectionServer $server */ + $server = $app->getContainer()->get(IntrospectionServer::class); try { - // Validate the given introspect request - $server->validateIntrospectionRequest($request); - // Try to respond to the introspection request - return $server->respondToIntrospectionRequest($request, $response); + return $server->respondToIntrospectionRequest($request, new Response()); } catch (OAuthServerException $exception) { // All instances of OAuthServerException can be converted to a PSR-7 response return $exception->generateHttpResponse($response); - } catch (\Exception $exception) { + } catch (Exception $exception) { // Catch unexpected exceptions $body = $response->getBody(); diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 417c3b0d9..4d6862157 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -15,16 +15,12 @@ use League\Event\EmitterAwareTrait; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\GrantTypeInterface; -use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator; -use League\OAuth2\Server\IntrospectionValidators\IntrospectionValidatorInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\AbstractResponseType; -use League\OAuth2\Server\ResponseTypes\BearerTokenIntrospectionResponse; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; -use League\OAuth2\Server\ResponseTypes\IntrospectionResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -58,21 +54,6 @@ class AuthorizationServer implements EmitterAwareInterface */ protected $responseType; - /** - * @var null|IntrospectionResponse - */ - protected $introspectionResponseType; - - /** - * @var null|IntrospectionValidatorInterface - */ - protected $introspectionValidator; - - /** - * @var null|Introspector - */ - protected $introspector; - /** * @var ClientRepositoryInterface */ @@ -231,105 +212,6 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res throw OAuthServerException::unsupportedGrantType(); } - /** - * Set the introspection response type. - * - * @param IntrospectionResponse $responseType - */ - public function setIntrospectionResponseType(IntrospectionResponse $responseType) - { - $this->introspectionResponseType = $responseType; - } - - /** - * Set the validator used for introspection requests. - * - * @param IntrospectionValidatorInterface $introspectionValidator - */ - public function setIntrospectionValidator(IntrospectionValidatorInterface $introspectionValidator) - { - $this->introspectionValidator = $introspectionValidator; - } - - /** - * Get the introspection response. - * - * @return IntrospectionResponse - */ - protected function getIntrospectionResponseType() - { - if ($this->introspectionResponseType instanceof IntrospectionResponse === false) { - $this->introspectionResponseType = new BearerTokenIntrospectionResponse(); - } - - return $this->introspectionResponseType; - } - - /** - * Get the introspection response - * - * @return IntrospectionValidatorInterface - */ - protected function getIntrospectionValidator() - { - if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { - $this->introspectionValidator = new BearerTokenValidator($this->accessTokenRepository); - - $this->introspectionValidator->setPublicKey($this->publicKey); - } - - return $this->introspectionValidator; - } - - /** - * Return an introspection response. - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * - * @return ResponseInterface - */ - public function respondToIntrospectionRequest(ServerRequestInterface $request, ResponseInterface $response) - { - $introspector = $this->getIntrospector(); - - $introspectionResponse = $introspector->respondToIntrospectionRequest( - $request, - $this->getIntrospectionResponseType() - ); - - return $introspectionResponse->generateHttpResponse($response); - } - - /** - * Validate an introspection request. - * - * @param ServerRequestInterface $request - */ - public function validateIntrospectionRequest(ServerRequestInterface $request) - { - $introspector = $this->getIntrospector(); - $introspector->validateIntrospectionRequest($request); - } - - /** - * Returns the introspector. - * - * @return Introspector - */ - private function getIntrospector() - { - if (!isset($this->introspector)) { - $this->introspector = new Introspector( - $this->accessTokenRepository, - $this->privateKey, - $this->getIntrospectionValidator() - ); - } - - return $this->introspector; - } - /** * Get the token type that grants will return in the HTTP response. * diff --git a/src/IntrospectionServer.php b/src/IntrospectionServer.php new file mode 100644 index 000000000..684ceabd6 --- /dev/null +++ b/src/IntrospectionServer.php @@ -0,0 +1,122 @@ +accessTokenRepository = $accessTokenRepository; + + if ($publicKey instanceof CryptKey === false) { + $publicKey = new CryptKey($publicKey); + } + + $this->publicKey = $publicKey; + $this->introspectionValidator = $introspectionValidator; + $this->authorizationValidator = $authorizationValidator; + + if ($responseType === null) { + $this->responseType = new BearerTokenResponse(); + } else { + $this->responseType = clone $responseType; + } + } + + /** + * Get the introspection validator + * + * @return IntrospectionValidatorInterface + */ + protected function getIntrospectionValidator(): IntrospectionValidatorInterface + { + if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { + $this->introspectionValidator = new IntrospectionBearerTokenValidator($this->accessTokenRepository); + + $this->introspectionValidator->setPublicKey($this->publicKey); + } + + return $this->introspectionValidator; + } + + /** + * Get the authorization validator + * + * @return AuthorizationValidatorInterface + */ + protected function getAuthorizationValidator(): AuthorizationValidatorInterface + { + if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) { + $this->authorizationValidator = new AuthorizationBearerTokenValidator($this->accessTokenRepository); + + $this->authorizationValidator->setPublicKey($this->publicKey); + } + + return $this->authorizationValidator; + } + + /** + * Return an introspection response. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * + * @throws Exception\OAuthServerException + */ + public function respondToIntrospectionRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->getAuthorizationValidator()->validateAuthorization($request); + + $this->responseType->setRequest($request); + $this->responseType->setValidity( + $this->getIntrospectionValidator()->validateIntrospection($request) + ); + + return $this->responseType->generateHttpResponse($response); + } +} diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php index f2e7ec15a..f19e3b862 100644 --- a/src/IntrospectionValidators/BearerTokenValidator.php +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -12,7 +12,9 @@ use Lcobucci\JWT\Validation\Constraint\LooseValidAt; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\StrictValidAt; +use Lcobucci\JWT\Validation\Constraint\ValidAt; use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -56,17 +58,14 @@ public function setPublicKey(CryptKey $key) /** * Initialise the JWT configuration. */ - private function initJwtConfiguration() + private function initJwtConfiguration(): void { - $this->jwtConfiguration = Configuration::forSymmetricSigner( - new Sha256(), - InMemory::plainText('') - ); + $this->jwtConfiguration = Configuration::forSymmetricSigner(new Sha256(), InMemory::empty()); $this->jwtConfiguration->setValidationConstraints( \class_exists(StrictValidAt::class) ? new StrictValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))) - : new LooseValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))), + : new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))), new SignedWith( new Sha256(), InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '') @@ -77,8 +76,10 @@ private function initJwtConfiguration() /** * {@inheritdoc} */ - public function validateIntrospection(ServerRequestInterface $request) + public function validateIntrospection(ServerRequestInterface $request): bool { + $this->validateIntrospectionRequest($request); + try { $token = $this->getTokenFromRequest($request); } catch (InvalidArgumentException $e) { @@ -102,14 +103,28 @@ public function validateIntrospection(ServerRequestInterface $request) * * @return Token */ - public function getTokenFromRequest(ServerRequestInterface $request) + public function getTokenFromRequest(ServerRequestInterface $request): Token { - $jwt = $request->getParsedBody()['token'] ?? null; + $jwt = $request->getParsedBody()['token'] ?? ''; return $this->jwtConfiguration->parser() ->parse($jwt); } + /** + * Validate the introspection request. + * + * @param ServerRequestInterface $request + * + * @throws OAuthServerException + */ + private function validateIntrospectionRequest(ServerRequestInterface $request) + { + if ($request->getMethod() !== 'POST') { + throw OAuthServerException::accessDenied('Invalid request method'); + } + } + /** * Check if the given token is revoked. * @@ -117,7 +132,7 @@ public function getTokenFromRequest(ServerRequestInterface $request) * * @return bool */ - private function isTokenRevoked(Token $token) + private function isTokenRevoked(Token $token): bool { return $this->accessTokenRepository->isAccessTokenRevoked($token->claims()->get('jti')); } @@ -129,7 +144,7 @@ private function isTokenRevoked(Token $token) * * @return bool */ - private function isTokenValid(Token $token) + private function isTokenValid(Token $token): bool { $constraints = $this->jwtConfiguration->validationConstraints(); diff --git a/src/IntrospectionValidators/IntrospectionValidatorInterface.php b/src/IntrospectionValidators/IntrospectionValidatorInterface.php index 4c004e242..6d58a437b 100644 --- a/src/IntrospectionValidators/IntrospectionValidatorInterface.php +++ b/src/IntrospectionValidators/IntrospectionValidatorInterface.php @@ -2,16 +2,19 @@ namespace League\OAuth2\Server\IntrospectionValidators; +use League\OAuth2\Server\Exception\OAuthServerException; use Psr\Http\Message\ServerRequestInterface; interface IntrospectionValidatorInterface { /** - * Determine wether the introspection request is valid. + * Determine whether the introspection request is valid. * * @param ServerRequestInterface $request * + * @throws OAuthServerException + * * @return bool */ - public function validateIntrospection(ServerRequestInterface $request); + public function validateIntrospection(ServerRequestInterface $request): bool; } diff --git a/src/Introspector.php b/src/Introspector.php deleted file mode 100644 index 412f524bf..000000000 --- a/src/Introspector.php +++ /dev/null @@ -1,96 +0,0 @@ -accessTokenRepository = $accessTokenRepository; - $this->privateKey = $privateKey; - $this->introspectionValidator = $introspectionValidator; - } - - /** - * Validate the introspection request. - * - * @param ServerRequestInterface $request - * - * @throws OAuthServerException - */ - public function validateIntrospectionRequest(ServerRequestInterface $request) - { - if ($request->getMethod() !== 'POST') { - throw OAuthServerException::accessDenied('Invalid request method'); - } - } - - /** - * Return an introspection response. - * - * @param ServerRequestInterface $request - * @param IntrospectionResponse $responseType - * - * @return IntrospectionResponse - */ - public function respondToIntrospectionRequest( - ServerRequestInterface $request, - IntrospectionResponse $responseType - ) { - $validator = $this->getIntrospectionValidator(); - - if ($validator->validateIntrospection($request)) { - $responseType->setRequest($request); - $responseType->setValidity(true); - } - - return $responseType; - } - - /** - * Get the introspection validator, falling back to the bearer token validator if not set. - * - * @return IntrospectionValidatorInterface - */ - protected function getIntrospectionValidator() - { - if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { - $this->introspectionValidator = new BearerTokenValidator($this->accessTokenRepository); - $this->introspectionValidator->setPublicKey($this->privateKey); - } - - return $this->introspectionValidator; - } -} diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/Introspection/AbstractResponseType.php similarity index 81% rename from src/ResponseTypes/IntrospectionResponse.php rename to src/ResponseTypes/Introspection/AbstractResponseType.php index 479834ca8..25efc4c7f 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/Introspection/AbstractResponseType.php @@ -1,11 +1,11 @@ true, @@ -56,7 +56,7 @@ protected function validIntrospectionResponse() * * @return array */ - protected function invalidIntrospectionResponse() + protected function invalidIntrospectionResponse(): array { return [ 'active' => false, @@ -68,7 +68,7 @@ protected function invalidIntrospectionResponse() * * @return array */ - public function getIntrospectionResponseParams() + public function getIntrospectionResponseParams(): array { return $this->isValid() ? $this->validIntrospectionResponse() : @@ -80,7 +80,7 @@ public function getIntrospectionResponseParams() * * @return bool */ - protected function isValid() + protected function isValid(): bool { return $this->valid === true; } @@ -92,7 +92,7 @@ protected function isValid() * * @return ResponseInterface */ - public function generateHttpResponse(ResponseInterface $response) + public function generateHttpResponse(ResponseInterface $response): ResponseInterface { $responseParams = $this->getIntrospectionResponseParams(); @@ -109,12 +109,12 @@ public function generateHttpResponse(ResponseInterface $response) /** * Add custom fields to your Introspection response here, then set your introspection - * reponse in AuthorizationServer::setIntrospectionResponseType() to pull in your version of + * response in AuthorizationServer::setIntrospectionResponseType() to pull in your version of * this class rather than the default. * * @return array */ - protected function getExtraParams() + protected function getExtraParams(): array { return []; } diff --git a/src/ResponseTypes/BearerTokenIntrospectionResponse.php b/src/ResponseTypes/Introspection/BearerTokenResponse.php similarity index 75% rename from src/ResponseTypes/BearerTokenIntrospectionResponse.php rename to src/ResponseTypes/Introspection/BearerTokenResponse.php index 5a8a71307..d82100f53 100644 --- a/src/ResponseTypes/BearerTokenIntrospectionResponse.php +++ b/src/ResponseTypes/Introspection/BearerTokenResponse.php @@ -1,6 +1,8 @@ jwtConfiguration = Configuration::forSymmetricSigner( - new Sha256(), InMemory::plainText('') - ); + $this->jwtConfiguration = Configuration::forSymmetricSigner(new Sha256(), InMemory::empty()); } /** @@ -36,7 +36,7 @@ private function initJwtConfiguration() * * @return array */ - protected function validIntrospectionResponse() + protected function validIntrospectionResponse(): array { $token = $this->getTokenFromRequest(); @@ -57,26 +57,26 @@ protected function validIntrospectionResponse() /** * Gets the token from the request body. * - * @return Token + * @return UnencryptedToken|Token */ protected function getTokenFromRequest() { - $jwt = $this->request->getParsedBody()['token'] ?? null; + $jwt = $this->request->getParsedBody()['token'] ?? ''; return $this->jwtConfiguration->parser() ->parse($jwt); } /** - * Gets single claim from the JWT token. + * Gets a single claim from the JWT token. * - * @param UnencryptedToken $token + * @param UnencryptedToken|Token\Plain $token * @param string $claim * @param mixed|null $default * * @return mixed */ - protected function getClaimFromToken(UnencryptedToken $token, string $claim, $default = null) + protected function getClaimFromToken($token, string $claim, $default = null) { return $token->claims()->get($claim, $default); } diff --git a/src/ResponseTypes/Introspection/ResponseTypeInterface.php b/src/ResponseTypes/Introspection/ResponseTypeInterface.php new file mode 100644 index 000000000..a5eda0f9b --- /dev/null +++ b/src/ResponseTypes/Introspection/ResponseTypeInterface.php @@ -0,0 +1,17 @@ +expectException(OAuthServerException::class); + + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + null, + $this->getMockBuilder(AuthorizationValidatorInterface::class)->getMock() + ); + + // invalid request method + $request = new ServerRequest([], [], '/', 'GET'); + + try { + $introspectionServer->respondToIntrospectionRequest($request, new Response); + } catch (OAuthServerException $e) { + $this->assertEquals('access_denied', $e->getErrorType()); + $this->assertEquals(401, $e->getHttpStatusCode()); + $this->assertStringContainsString('Invalid request method', $e->getHint()); + throw $e; + } + } + + public function testIfUnauthorizedRequestThrowsAuthorizationException() + { + $this->expectException(OAuthServerException::class); + + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + null + ); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('test'); + $accessToken->setUserIdentifier(123); + // expired token + $accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); + $accessToken->setClient($client); + $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/Stubs/private.key')); + + $token = (string) $accessToken; + + $request = new ServerRequest( + [], + [], + '/', + 'POST', + 'php://input', + ['Authorization' => 'Bearer ' . $token] + ); + + try { + $introspectionServer->respondToIntrospectionRequest($request, new Response); + } catch (OAuthServerException $e) { + $this->assertEquals('access_denied', $e->getErrorType()); + $this->assertEquals(401, $e->getHttpStatusCode()); + throw $e; + } + } + + public function testIfServerRespondsWhenTokenIsMissing() + { + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + null, + $this->getMockBuilder(AuthorizationValidatorInterface::class)->getMock() + ); + + $request = new ServerRequest([], [], '/', 'POST', 'php://input'); + + $response = $introspectionServer->respondToIntrospectionRequest($request, new Response); + $response->getBody()->rewind(); + + $responseData = json_decode($response->getBody()->getContents(), true); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals([ + 'active' => false + ], $responseData); + } + + public function testIfServerRespondsWhenTokenIsValid() + { + $introspectionValidator = $this->getMockBuilder(IntrospectionValidatorInterface::class)->getMock(); + $introspectionValidator->method('validateIntrospection')->willReturn(true); + + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + $introspectionValidator, + $this->getMockBuilder(AuthorizationValidatorInterface::class)->getMock() + ); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $jwtConfiguration = Configuration::forSymmetricSigner(new Sha256(), InMemory::file('file://' . __DIR__ . '/Stubs/private.key')); + $token = $jwtConfiguration + ->builder() + ->permittedFor('clientName') + ->identifiedBy('test') + ->issuedAt((new DateTimeImmutable('2016-01-01'))) + ->canOnlyBeUsedAfter((new DateTimeImmutable('2016-01-01'))) + ->expiresAt((new DateTimeImmutable('2017-01-01'))) + ->relatedTo('123') + ->withClaim('scopes', ['a', 'b', 'c']) + ->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey()) + ->toString(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn(['token' => $token]); + + $response = $introspectionServer->respondToIntrospectionRequest($requestMock, new Response); + $response->getBody()->rewind(); + $responseData = json_decode($response->getBody()->getContents(), true); + + $this->assertEquals([ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => [ + 'a', 'b', 'c' + ], + 'client_id' => [ + 'clientName' + ], + 'exp' => [ + 'date' => '2017-01-01 00:00:00.000000', + 'timezone_type' => 1, + 'timezone' => '+00:00' + ], + 'iat' => [ + 'date' => '2016-01-01 00:00:00.000000', + 'timezone_type' => 1, + 'timezone' => '+00:00' + ], + 'sub' => '123', + 'jti' => 'test', + ], $responseData); + } +} diff --git a/tests/IntrospectionValidators/BearerTokenValidatorTest.php b/tests/IntrospectionValidators/BearerTokenValidatorTest.php index f8ad1e175..db2f6f686 100644 --- a/tests/IntrospectionValidators/BearerTokenValidatorTest.php +++ b/tests/IntrospectionValidators/BearerTokenValidatorTest.php @@ -7,7 +7,9 @@ use Laminas\Diactoros\ServerRequest; 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\Exception\OAuthServerException; use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use PHPUnit\Framework\TestCase; @@ -17,11 +19,32 @@ class BearerTokenValidatorTest extends TestCase { + public function testThrowExceptionWhenRequestMethodIsNotPost() + { + $this->expectException(OAuthServerException::class); + + $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setMethodsExcept([ + 'validateIntrospection' + ]) + ->disableOriginalConstructor() + ->getMock(); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('GET'); + + $validator->validateIntrospection($requestMock); + } + public function testReturnsFalseWhenNoTokenPassed() { $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setMethodsExcept([ + 'validateIntrospection' + ]) ->disableOriginalConstructor() - ->setMethods(['getTokenFromRequest']) ->getMock(); $validator->method('getTokenFromRequest')->will( @@ -31,6 +54,8 @@ public function testReturnsFalseWhenNoTokenPassed() $requestMock = $this->getMockBuilder(ServerRequestInterface::class) ->getMock(); + $requestMock->method('getMethod')->willReturn('POST'); + $this->assertFalse($validator->validateIntrospection($requestMock)); } @@ -59,9 +84,15 @@ public function testReturnsFalseWhenTokenIsRevoked() ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); - $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $validJwt->toString() + ]); - $this->assertFalse($bearerTokenValidator->validateIntrospection($request)); + $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); } public function testReturnsFalseWhenTokenIsExpired() @@ -86,12 +117,18 @@ public function testReturnsFalseWhenTokenIsExpired() ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); - $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); - $this->assertFalse($bearerTokenValidator->validateIntrospection($request)); + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $validJwt->toString() + ]); + + $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); } - public function testReturnsFalseWhenTokenIsInvalid() + public function testReturnsFalseWhenTokenIsIssuedByDifferentPrivateKey() { $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) ->getMock(); @@ -103,7 +140,7 @@ public function testReturnsFalseWhenTokenIsInvalid() $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); $jwtConfiguration->setAccessible(true); - $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + $invalidJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() ->permittedFor('client-id') ->identifiedBy('token-id') ->issuedAt(new DateTimeImmutable()) @@ -113,9 +150,15 @@ public function testReturnsFalseWhenTokenIsInvalid() ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key.crlf')); - $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $invalidJwt->toString() + ]); - $this->assertFalse($bearerTokenValidator->validateIntrospection($request)); + $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); } public function testReturnsTrueWhenTokenIsValid() @@ -123,6 +166,9 @@ public function testReturnsTrueWhenTokenIsValid() $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) ->getMock(); + $accessTokenRepositoryMock->method('isAccessTokenRevoked') + ->willReturn(false); + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); @@ -140,8 +186,14 @@ public function testReturnsTrueWhenTokenIsValid() ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); - $request = (new ServerRequest())->withParsedBody(['token' => $validJwt->toString()]); + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $validJwt->toString() + ]); - $this->assertTrue($bearerTokenValidator->validateIntrospection($request)); + $this->assertTrue($bearerTokenValidator->validateIntrospection($requestMock)); } } diff --git a/tests/IntrospectorTest.php b/tests/IntrospectorTest.php deleted file mode 100644 index 172b5292a..000000000 --- a/tests/IntrospectorTest.php +++ /dev/null @@ -1,100 +0,0 @@ -getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - new CryptKey('file://' . __DIR__ . '/Stubs/private.key') - ); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getMethod')->willReturn('GET'); - $this->expectException(OAuthServerException::class); - - try { - $introspectionResponse = $introspector->validateIntrospectionRequest($requestMock); - } catch (OAuthServerException $e) { - $this->assertEquals('access_denied', $e->getErrorType()); - $this->assertEquals(401, $e->getHttpStatusCode()); - - throw $e; - } - } - - public function testPostRequest() - { - $introspector = new Introspector( - $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - new CryptKey('file://' . __DIR__ . '/Stubs/private.key') - ); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $requestMock->method('getMethod')->willReturn('POST'); - $this->assertNull($introspector->validateIntrospectionRequest($requestMock)); - } - - public function testRespondToInvalidRequest() - { - $validator = $this->getMockBuilder(IntrospectionValidatorInterface::class)->getMock(); - $validator->method('validateIntrospection')->willReturn(false); - - $introspector = new Introspector( - $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $validator - ); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); - - $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertEquals( - [ - 'active' => false, - ], - $introspectionResponse->getIntrospectionResponseParams() - ); - } - - public function testRespondToValidRequest() - { - $validator = $this->getMockBuilder(IntrospectionValidatorInterface::class)->getMock(); - $validator->method('validateIntrospection')->willReturn(true); - - $introspector = new Introspector( - $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - new CryptKey('file://' . __DIR__ . '/Stubs/private.key'), - $validator - ); - - $requestMock = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $introspectionResponse = $introspector->respondToIntrospectionRequest($requestMock, new IntrospectionResponse); - - $this->assertInstanceOf(IntrospectionResponse::class, $introspectionResponse); - $this->assertEquals( - [ - 'active' => true, - ], - $introspectionResponse->getIntrospectionResponseParams() - ); - } -} diff --git a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php index 28ef0a1de..712f7f8f2 100644 --- a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php +++ b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php @@ -2,17 +2,18 @@ namespace LeagueTests\ResponseTypes; +use Laminas\Diactoros\Response; use Lcobucci\JWT\Token; -use League\OAuth2\Server\ResponseTypes\BearerTokenIntrospectionResponse; +use Lcobucci\JWT\UnencryptedToken; +use League\OAuth2\Server\ResponseTypes\Introspection\BearerTokenResponse; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response; class BearerTokenIntrospectionResponseTest extends TestCase { public function testInvalidIntrospectionResponse() { - $responseType = new BearerTokenIntrospectionResponse(); + $responseType = new BearerTokenResponse(); $response = $responseType->generateHttpResponse(new Response()); $this->assertInstanceOf(ResponseInterface::class, $response); @@ -27,14 +28,14 @@ public function testInvalidIntrospectionResponse() public function testValidIntrospectionResponse() { - $responseType = $this->getMockBuilder(BearerTokenIntrospectionResponse::class) + $responseType = $this->getMockBuilder(BearerTokenResponse::class) ->onlyMethods([ 'getTokenFromRequest', 'getClaimFromToken' ]) ->getMock(); - $tokenMock = $this->getMockBuilder(Token::class) + $tokenMock = $this->getMockBuilder(UnencryptedToken::class) ->getMock(); $responseType->method('getTokenFromRequest')->willReturn($tokenMock); @@ -62,7 +63,7 @@ public function testValidIntrospectionResponse() public function testValidIntrospectionResponseWithExtraParams() { - $responseType = $this->getMockBuilder(BearerTokenIntrospectionResponse::class) + $responseType = $this->getMockBuilder(BearerTokenResponse::class) ->onlyMethods([ 'getTokenFromRequest', 'getClaimFromToken', @@ -70,7 +71,7 @@ public function testValidIntrospectionResponseWithExtraParams() ]) ->getMock(); - $tokenMock = $this->getMockBuilder(Token::class) + $tokenMock = $this->getMockBuilder(UnencryptedToken::class) ->getMock(); $responseType->method('getTokenFromRequest') From 69aca7d3d0db84d3eb7da9c95366db9242107567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=B3l?= Date: Sat, 11 Dec 2021 21:35:48 +0100 Subject: [PATCH 33/34] Small method declaration changes --- src/IntrospectionValidators/BearerTokenValidator.php | 4 ++-- src/ResponseTypes/Introspection/AbstractResponseType.php | 8 ++++---- src/ResponseTypes/Introspection/BearerTokenResponse.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php index f19e3b862..545835675 100644 --- a/src/IntrospectionValidators/BearerTokenValidator.php +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -48,7 +48,7 @@ public function __construct(AccessTokenRepositoryInterface $accessTokenRepositor * * @param CryptKey $key */ - public function setPublicKey(CryptKey $key) + public function setPublicKey(CryptKey $key): void { $this->publicKey = $key; @@ -118,7 +118,7 @@ public function getTokenFromRequest(ServerRequestInterface $request): Token * * @throws OAuthServerException */ - private function validateIntrospectionRequest(ServerRequestInterface $request) + private function validateIntrospectionRequest(ServerRequestInterface $request): void { if ($request->getMethod() !== 'POST') { throw OAuthServerException::accessDenied('Invalid request method'); diff --git a/src/ResponseTypes/Introspection/AbstractResponseType.php b/src/ResponseTypes/Introspection/AbstractResponseType.php index 25efc4c7f..21547c47a 100644 --- a/src/ResponseTypes/Introspection/AbstractResponseType.php +++ b/src/ResponseTypes/Introspection/AbstractResponseType.php @@ -22,7 +22,7 @@ abstract class AbstractResponseType implements ResponseTypeInterface * * @param bool $bool */ - public function setValidity(bool $bool) + public function setValidity(bool $bool): void { $this->valid = $bool; } @@ -32,7 +32,7 @@ public function setValidity(bool $bool) * * @param ServerRequestInterface $request */ - public function setRequest(ServerRequestInterface $request) + public function setRequest(ServerRequestInterface $request): void { $this->request = $request; } @@ -108,8 +108,8 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter } /** - * Add custom fields to your Introspection response here, then set your introspection - * response in AuthorizationServer::setIntrospectionResponseType() to pull in your version of + * Add custom fields to your Introspection response here, then add your introspection + * response to IntrospectionServer constructor to pull in your version of * this class rather than the default. * * @return array diff --git a/src/ResponseTypes/Introspection/BearerTokenResponse.php b/src/ResponseTypes/Introspection/BearerTokenResponse.php index d82100f53..656c11b52 100644 --- a/src/ResponseTypes/Introspection/BearerTokenResponse.php +++ b/src/ResponseTypes/Introspection/BearerTokenResponse.php @@ -26,7 +26,7 @@ public function __construct() /** * Initialise the JWT configuration. */ - private function initJwtConfiguration() + private function initJwtConfiguration(): void { $this->jwtConfiguration = Configuration::forSymmetricSigner(new Sha256(), InMemory::empty()); } From 759eea8baea1bf02b8c6c8a3fee1833bfa7e96c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=B3l?= Date: Sat, 11 Dec 2021 22:27:07 +0100 Subject: [PATCH 34/34] Apply StyleCI suggestions --- src/IntrospectionServer.php | 8 +++---- .../BearerTokenValidator.php | 1 - .../Introspection/AbstractResponseType.php | 4 ++-- .../Introspection/BearerTokenResponse.php | 7 +++---- tests/IntrospectionServerTest.php | 21 +++++++------------ .../BearerTokenValidatorTest.php | 14 ++++++------- .../BearerTokenIntrospectionResponseTest.php | 15 +++++++------ 7 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/IntrospectionServer.php b/src/IntrospectionServer.php index 684ceabd6..512dbd033 100644 --- a/src/IntrospectionServer.php +++ b/src/IntrospectionServer.php @@ -9,8 +9,8 @@ use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator as IntrospectionBearerTokenValidator; use League\OAuth2\Server\IntrospectionValidators\IntrospectionValidatorInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; -use League\OAuth2\Server\ResponseTypes\Introspection\BearerTokenResponse; use League\OAuth2\Server\ResponseTypes\Introspection\AbstractResponseType; +use League\OAuth2\Server\ResponseTypes\Introspection\BearerTokenResponse; use League\OAuth2\Server\ResponseTypes\Introspection\ResponseTypeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -43,11 +43,11 @@ class IntrospectionServer protected $introspectionValidator; public function __construct( - AccessTokenRepositoryInterface $accessTokenRepository, + AccessTokenRepositoryInterface $accessTokenRepository, $publicKey, IntrospectionValidatorInterface $introspectionValidator = null, AuthorizationValidatorInterface $authorizationValidator = null, - ResponseTypeInterface $responseType = null + ResponseTypeInterface $responseType = null ) { $this->accessTokenRepository = $accessTokenRepository; @@ -102,7 +102,7 @@ protected function getAuthorizationValidator(): AuthorizationValidatorInterface * Return an introspection response. * * @param ServerRequestInterface $request - * @param ResponseInterface $response + * @param ResponseInterface $response * * @return ResponseInterface * diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php index 545835675..a27241106 100644 --- a/src/IntrospectionValidators/BearerTokenValidator.php +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -9,7 +9,6 @@ use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; -use Lcobucci\JWT\Validation\Constraint\LooseValidAt; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\StrictValidAt; use Lcobucci\JWT\Validation\Constraint\ValidAt; diff --git a/src/ResponseTypes/Introspection/AbstractResponseType.php b/src/ResponseTypes/Introspection/AbstractResponseType.php index 21547c47a..57ab3c657 100644 --- a/src/ResponseTypes/Introspection/AbstractResponseType.php +++ b/src/ResponseTypes/Introspection/AbstractResponseType.php @@ -48,7 +48,7 @@ protected function validIntrospectionResponse(): array 'active' => true, ]; - return array_merge($this->getExtraParams(), $responseParams); + return \array_merge($this->getExtraParams(), $responseParams); } /** @@ -102,7 +102,7 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter ->withHeader('cache-control', 'no-store') ->withHeader('content-type', 'application/json; charset=UTF-8'); - $response->getBody()->write(json_encode($responseParams)); + $response->getBody()->write(\json_encode($responseParams)); return $response; } diff --git a/src/ResponseTypes/Introspection/BearerTokenResponse.php b/src/ResponseTypes/Introspection/BearerTokenResponse.php index 656c11b52..97a5e187d 100644 --- a/src/ResponseTypes/Introspection/BearerTokenResponse.php +++ b/src/ResponseTypes/Introspection/BearerTokenResponse.php @@ -22,7 +22,6 @@ public function __construct() $this->initJwtConfiguration(); } - /** * Initialise the JWT configuration. */ @@ -51,7 +50,7 @@ protected function validIntrospectionResponse(): array 'jti' => $this->getClaimFromToken($token, 'jti'), ]; - return array_merge($this->getExtraParams(), $responseParams); + return \array_merge($this->getExtraParams(), $responseParams); } /** @@ -71,8 +70,8 @@ protected function getTokenFromRequest() * Gets a single claim from the JWT token. * * @param UnencryptedToken|Token\Plain $token - * @param string $claim - * @param mixed|null $default + * @param string $claim + * @param mixed|null $default * * @return mixed */ diff --git a/tests/IntrospectionServerTest.php b/tests/IntrospectionServerTest.php index 11c164dc5..7719257a8 100644 --- a/tests/IntrospectionServerTest.php +++ b/tests/IntrospectionServerTest.php @@ -4,11 +4,8 @@ use DateInterval; use DateTimeImmutable; -use Defuse\Crypto\Key; use Laminas\Diactoros\Response; use Laminas\Diactoros\ServerRequest; -use Laminas\Diactoros\Stream; -use Laminas\Diactoros\StreamFactory; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; @@ -18,19 +15,17 @@ use League\OAuth2\Server\IntrospectionServer; use League\OAuth2\Server\IntrospectionValidators\IntrospectionValidatorInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; -use League\OAuth2\Server\ResponseTypes\Introspection\AbstractResponseType; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\ClientEntity; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\StreamInterface; class IntrospectionServerTest extends TestCase { public function setUp(): void { // Make sure the keys have the correct permissions. - chmod(__DIR__ . '/Stubs/public.key', 0600); + \chmod(__DIR__ . '/Stubs/public.key', 0600); } public function testIfGetRequestThrowsInvalidMethodException() @@ -112,11 +107,11 @@ public function testIfServerRespondsWhenTokenIsMissing() $response = $introspectionServer->respondToIntrospectionRequest($request, new Response); $response->getBody()->rewind(); - $responseData = json_decode($response->getBody()->getContents(), true); + $responseData = \json_decode($response->getBody()->getContents(), true); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals([ - 'active' => false + 'active' => false, ], $responseData); } @@ -153,26 +148,26 @@ public function testIfServerRespondsWhenTokenIsValid() $response = $introspectionServer->respondToIntrospectionRequest($requestMock, new Response); $response->getBody()->rewind(); - $responseData = json_decode($response->getBody()->getContents(), true); + $responseData = \json_decode($response->getBody()->getContents(), true); $this->assertEquals([ 'active' => true, 'token_type' => 'access_token', 'scope' => [ - 'a', 'b', 'c' + 'a', 'b', 'c', ], 'client_id' => [ - 'clientName' + 'clientName', ], 'exp' => [ 'date' => '2017-01-01 00:00:00.000000', 'timezone_type' => 1, - 'timezone' => '+00:00' + 'timezone' => '+00:00', ], 'iat' => [ 'date' => '2016-01-01 00:00:00.000000', 'timezone_type' => 1, - 'timezone' => '+00:00' + 'timezone' => '+00:00', ], 'sub' => '123', 'jti' => 'test', diff --git a/tests/IntrospectionValidators/BearerTokenValidatorTest.php b/tests/IntrospectionValidators/BearerTokenValidatorTest.php index db2f6f686..f5f055cc4 100644 --- a/tests/IntrospectionValidators/BearerTokenValidatorTest.php +++ b/tests/IntrospectionValidators/BearerTokenValidatorTest.php @@ -4,10 +4,8 @@ use DateInterval; use DateTimeImmutable; -use Laminas\Diactoros\ServerRequest; 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\Exception\OAuthServerException; use League\OAuth2\Server\IntrospectionValidators\BearerTokenValidator; @@ -25,7 +23,7 @@ public function testThrowExceptionWhenRequestMethodIsNotPost() $validator = $this->getMockBuilder(BearerTokenValidator::class) ->setMethodsExcept([ - 'validateIntrospection' + 'validateIntrospection', ]) ->disableOriginalConstructor() ->getMock(); @@ -42,7 +40,7 @@ public function testReturnsFalseWhenNoTokenPassed() { $validator = $this->getMockBuilder(BearerTokenValidator::class) ->setMethodsExcept([ - 'validateIntrospection' + 'validateIntrospection', ]) ->disableOriginalConstructor() ->getMock(); @@ -89,7 +87,7 @@ public function testReturnsFalseWhenTokenIsRevoked() $requestMock->method('getMethod')->willReturn('POST'); $requestMock->method('getParsedBody')->willReturn([ - 'token' => $validJwt->toString() + 'token' => $validJwt->toString(), ]); $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); @@ -122,7 +120,7 @@ public function testReturnsFalseWhenTokenIsExpired() $requestMock->method('getMethod')->willReturn('POST'); $requestMock->method('getParsedBody')->willReturn([ - 'token' => $validJwt->toString() + 'token' => $validJwt->toString(), ]); $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); @@ -155,7 +153,7 @@ public function testReturnsFalseWhenTokenIsIssuedByDifferentPrivateKey() $requestMock->method('getMethod')->willReturn('POST'); $requestMock->method('getParsedBody')->willReturn([ - 'token' => $invalidJwt->toString() + 'token' => $invalidJwt->toString(), ]); $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); @@ -191,7 +189,7 @@ public function testReturnsTrueWhenTokenIsValid() $requestMock->method('getMethod')->willReturn('POST'); $requestMock->method('getParsedBody')->willReturn([ - 'token' => $validJwt->toString() + 'token' => $validJwt->toString(), ]); $this->assertTrue($bearerTokenValidator->validateIntrospection($requestMock)); diff --git a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php index 712f7f8f2..fec277766 100644 --- a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php +++ b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php @@ -3,7 +3,6 @@ namespace LeagueTests\ResponseTypes; use Laminas\Diactoros\Response; -use Lcobucci\JWT\Token; use Lcobucci\JWT\UnencryptedToken; use League\OAuth2\Server\ResponseTypes\Introspection\BearerTokenResponse; use PHPUnit\Framework\TestCase; @@ -20,9 +19,9 @@ public function testInvalidIntrospectionResponse() $this->assertCorrectIntrospectionHeaders($response); $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents(), true); + $json = \json_decode($response->getBody()->getContents(), true); $this->assertEquals([ - 'active' => false + 'active' => false, ], $json); } @@ -31,7 +30,7 @@ public function testValidIntrospectionResponse() $responseType = $this->getMockBuilder(BearerTokenResponse::class) ->onlyMethods([ 'getTokenFromRequest', - 'getClaimFromToken' + 'getClaimFromToken', ]) ->getMock(); @@ -48,7 +47,7 @@ public function testValidIntrospectionResponse() $this->assertCorrectIntrospectionHeaders($response); $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents(), true); + $json = \json_decode($response->getBody()->getContents(), true); $this->assertEquals([ 'active' => true, 'token_type' => 'access_token', @@ -67,7 +66,7 @@ public function testValidIntrospectionResponseWithExtraParams() ->onlyMethods([ 'getTokenFromRequest', 'getClaimFromToken', - 'getExtraParams' + 'getExtraParams', ]) ->getMock(); @@ -90,7 +89,7 @@ public function testValidIntrospectionResponseWithExtraParams() $this->assertCorrectIntrospectionHeaders($response); $response->getBody()->rewind(); - $json = json_decode($response->getBody()->getContents(), true); + $json = \json_decode($response->getBody()->getContents(), true); $this->assertEquals([ 'active' => true, 'token_type' => 'access_token', @@ -100,7 +99,7 @@ public function testValidIntrospectionResponseWithExtraParams() 'exp' => 'value', 'sub' => 'value', 'jti' => 'value', - 'extra' => 'param' + 'extra' => 'param', ], $json); }