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