From 9446f0e292d03f9f5fbc731e18b71638a267acf8 Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 2 Mar 2018 22:53:46 +0100 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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