diff --git a/README.md b/README.md index 5307b840a..bc50d3b31 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,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/examples/public/introspect.php b/examples/public/introspect.php new file mode 100644 index 000000000..b97090e02 --- /dev/null +++ b/examples/public/introspect.php @@ -0,0 +1,51 @@ + function () { + + // Setup the authorization server + $server = new IntrospectionServer( + new AccessTokenRepository(), + 'file://' . __DIR__ . '/../public.key' + ); + + return $server; + }, +]); + +$app->post( + '/introspect', + function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { + /* @var IntrospectionServer $server */ + $server = $app->getContainer()->get(IntrospectionServer::class); + + try { + // Try to respond to the introspection request + return $server->respondToIntrospectionRequest($request, new Response()); + } catch (OAuthServerException $exception) { + + // All instances of OAuthServerException can be converted to a PSR-7 response + return $exception->generateHttpResponse($response); + } catch (Exception $exception) { + + // Catch unexpected exceptions + $body = $response->getBody(); + $body->write($exception->getMessage()); + + return $response->withStatus(500)->withBody($body); + } + } +); + +$app->run(); diff --git a/src/IntrospectionServer.php b/src/IntrospectionServer.php new file mode 100644 index 000000000..512dbd033 --- /dev/null +++ b/src/IntrospectionServer.php @@ -0,0 +1,122 @@ +accessTokenRepository = $accessTokenRepository; + + if ($publicKey instanceof CryptKey === false) { + $publicKey = new CryptKey($publicKey); + } + + $this->publicKey = $publicKey; + $this->introspectionValidator = $introspectionValidator; + $this->authorizationValidator = $authorizationValidator; + + if ($responseType === null) { + $this->responseType = new BearerTokenResponse(); + } else { + $this->responseType = clone $responseType; + } + } + + /** + * Get the introspection validator + * + * @return IntrospectionValidatorInterface + */ + protected function getIntrospectionValidator(): IntrospectionValidatorInterface + { + if ($this->introspectionValidator instanceof IntrospectionValidatorInterface === false) { + $this->introspectionValidator = new IntrospectionBearerTokenValidator($this->accessTokenRepository); + + $this->introspectionValidator->setPublicKey($this->publicKey); + } + + return $this->introspectionValidator; + } + + /** + * Get the authorization validator + * + * @return AuthorizationValidatorInterface + */ + protected function getAuthorizationValidator(): AuthorizationValidatorInterface + { + if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) { + $this->authorizationValidator = new AuthorizationBearerTokenValidator($this->accessTokenRepository); + + $this->authorizationValidator->setPublicKey($this->publicKey); + } + + return $this->authorizationValidator; + } + + /** + * Return an introspection response. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * + * @throws Exception\OAuthServerException + */ + public function respondToIntrospectionRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->getAuthorizationValidator()->validateAuthorization($request); + + $this->responseType->setRequest($request); + $this->responseType->setValidity( + $this->getIntrospectionValidator()->validateIntrospection($request) + ); + + return $this->responseType->generateHttpResponse($response); + } +} diff --git a/src/IntrospectionValidators/BearerTokenValidator.php b/src/IntrospectionValidators/BearerTokenValidator.php new file mode 100644 index 000000000..a27241106 --- /dev/null +++ b/src/IntrospectionValidators/BearerTokenValidator.php @@ -0,0 +1,152 @@ +accessTokenRepository = $accessTokenRepository; + } + + /** + * Set the private key. + * + * @param CryptKey $key + */ + public function setPublicKey(CryptKey $key): void + { + $this->publicKey = $key; + + $this->initJwtConfiguration(); + } + + /** + * Initialise the JWT configuration. + */ + private function initJwtConfiguration(): void + { + $this->jwtConfiguration = Configuration::forSymmetricSigner(new Sha256(), InMemory::empty()); + + $this->jwtConfiguration->setValidationConstraints( + \class_exists(StrictValidAt::class) + ? new StrictValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))) + : new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))), + new SignedWith( + new Sha256(), + InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '') + ) + ); + } + + /** + * {@inheritdoc} + */ + public function validateIntrospection(ServerRequestInterface $request): bool + { + $this->validateIntrospectionRequest($request); + + try { + $token = $this->getTokenFromRequest($request); + } catch (InvalidArgumentException $e) { + return false; + } + + if ( + !$this->isTokenValid($token) || + $this->isTokenRevoked($token) + ) { + return false; + } + + return true; + } + + /** + * Gets the token from the request body. + * + * @param ServerRequestInterface $request + * + * @return Token + */ + public function getTokenFromRequest(ServerRequestInterface $request): Token + { + $jwt = $request->getParsedBody()['token'] ?? ''; + + return $this->jwtConfiguration->parser() + ->parse($jwt); + } + + /** + * Validate the introspection request. + * + * @param ServerRequestInterface $request + * + * @throws OAuthServerException + */ + private function validateIntrospectionRequest(ServerRequestInterface $request): void + { + if ($request->getMethod() !== 'POST') { + throw OAuthServerException::accessDenied('Invalid request method'); + } + } + + /** + * Check if the given token is revoked. + * + * @param Token $token + * + * @return bool + */ + private function isTokenRevoked(Token $token): bool + { + return $this->accessTokenRepository->isAccessTokenRevoked($token->claims()->get('jti')); + } + + /** + * Check if the given token is valid + * + * @param Token $token + * + * @return bool + */ + private function isTokenValid(Token $token): bool + { + $constraints = $this->jwtConfiguration->validationConstraints(); + + return $this->jwtConfiguration->validator()->validate($token, ...$constraints); + } +} diff --git a/src/IntrospectionValidators/IntrospectionValidatorInterface.php b/src/IntrospectionValidators/IntrospectionValidatorInterface.php new file mode 100644 index 000000000..6d58a437b --- /dev/null +++ b/src/IntrospectionValidators/IntrospectionValidatorInterface.php @@ -0,0 +1,20 @@ +valid = $bool; + } + + /** + * Set the request. + * + * @param ServerRequestInterface $request + */ + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + /** + * Return the valid introspection parameters. + * + * @return array + */ + protected function validIntrospectionResponse(): array + { + $responseParams = [ + 'active' => true, + ]; + + return \array_merge($this->getExtraParams(), $responseParams); + } + + /** + * Return the invalid introspection parameters. + * + * @return array + */ + protected function invalidIntrospectionResponse(): array + { + return [ + 'active' => false, + ]; + } + + /** + * Extract the introspection response. + * + * @return array + */ + public function getIntrospectionResponseParams(): array + { + return $this->isValid() ? + $this->validIntrospectionResponse() : + $this->invalidIntrospectionResponse(); + } + + /** + * Check if the response is valid. + * + * @return bool + */ + protected function isValid(): bool + { + return $this->valid === true; + } + + /** + * Generate a HTTP response. + * + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function generateHttpResponse(ResponseInterface $response): ResponseInterface + { + $responseParams = $this->getIntrospectionResponseParams(); + + $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 add your introspection + * response to IntrospectionServer constructor to pull in your version of + * this class rather than the default. + * + * @return array + */ + protected function getExtraParams(): array + { + return []; + } +} diff --git a/src/ResponseTypes/Introspection/BearerTokenResponse.php b/src/ResponseTypes/Introspection/BearerTokenResponse.php new file mode 100644 index 000000000..97a5e187d --- /dev/null +++ b/src/ResponseTypes/Introspection/BearerTokenResponse.php @@ -0,0 +1,82 @@ +initJwtConfiguration(); + } + + /** + * Initialise the JWT configuration. + */ + private function initJwtConfiguration(): void + { + $this->jwtConfiguration = Configuration::forSymmetricSigner(new Sha256(), InMemory::empty()); + } + + /** + * Add the token data to the response. + * + * @return array + */ + protected function validIntrospectionResponse(): array + { + $token = $this->getTokenFromRequest(); + + $responseParams = [ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => $this->getClaimFromToken($token, 'scopes', ''), + 'client_id' => $this->getClaimFromToken($token, 'aud'), + 'exp' => $this->getClaimFromToken($token, 'exp'), + 'iat' => $this->getClaimFromToken($token, 'iat'), + 'sub' => $this->getClaimFromToken($token, 'sub'), + 'jti' => $this->getClaimFromToken($token, 'jti'), + ]; + + return \array_merge($this->getExtraParams(), $responseParams); + } + + /** + * Gets the token from the request body. + * + * @return UnencryptedToken|Token + */ + protected function getTokenFromRequest() + { + $jwt = $this->request->getParsedBody()['token'] ?? ''; + + return $this->jwtConfiguration->parser() + ->parse($jwt); + } + + /** + * Gets a single claim from the JWT token. + * + * @param UnencryptedToken|Token\Plain $token + * @param string $claim + * @param mixed|null $default + * + * @return mixed + */ + protected function getClaimFromToken($token, string $claim, $default = null) + { + return $token->claims()->get($claim, $default); + } +} diff --git a/src/ResponseTypes/Introspection/ResponseTypeInterface.php b/src/ResponseTypes/Introspection/ResponseTypeInterface.php new file mode 100644 index 000000000..a5eda0f9b --- /dev/null +++ b/src/ResponseTypes/Introspection/ResponseTypeInterface.php @@ -0,0 +1,17 @@ +expectException(OAuthServerException::class); + + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + null, + $this->getMockBuilder(AuthorizationValidatorInterface::class)->getMock() + ); + + // invalid request method + $request = new ServerRequest([], [], '/', 'GET'); + + try { + $introspectionServer->respondToIntrospectionRequest($request, new Response); + } catch (OAuthServerException $e) { + $this->assertEquals('access_denied', $e->getErrorType()); + $this->assertEquals(401, $e->getHttpStatusCode()); + $this->assertStringContainsString('Invalid request method', $e->getHint()); + throw $e; + } + } + + public function testIfUnauthorizedRequestThrowsAuthorizationException() + { + $this->expectException(OAuthServerException::class); + + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + null + ); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('test'); + $accessToken->setUserIdentifier(123); + // expired token + $accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); + $accessToken->setClient($client); + $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/Stubs/private.key')); + + $token = (string) $accessToken; + + $request = new ServerRequest( + [], + [], + '/', + 'POST', + 'php://input', + ['Authorization' => 'Bearer ' . $token] + ); + + try { + $introspectionServer->respondToIntrospectionRequest($request, new Response); + } catch (OAuthServerException $e) { + $this->assertEquals('access_denied', $e->getErrorType()); + $this->assertEquals(401, $e->getHttpStatusCode()); + throw $e; + } + } + + public function testIfServerRespondsWhenTokenIsMissing() + { + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + null, + $this->getMockBuilder(AuthorizationValidatorInterface::class)->getMock() + ); + + $request = new ServerRequest([], [], '/', 'POST', 'php://input'); + + $response = $introspectionServer->respondToIntrospectionRequest($request, new Response); + $response->getBody()->rewind(); + + $responseData = \json_decode($response->getBody()->getContents(), true); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals([ + 'active' => false, + ], $responseData); + } + + public function testIfServerRespondsWhenTokenIsValid() + { + $introspectionValidator = $this->getMockBuilder(IntrospectionValidatorInterface::class)->getMock(); + $introspectionValidator->method('validateIntrospection')->willReturn(true); + + $introspectionServer = new IntrospectionServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + new CryptKey('file://' . __DIR__ . '/Stubs/public.key'), + $introspectionValidator, + $this->getMockBuilder(AuthorizationValidatorInterface::class)->getMock() + ); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $jwtConfiguration = Configuration::forSymmetricSigner(new Sha256(), InMemory::file('file://' . __DIR__ . '/Stubs/private.key')); + $token = $jwtConfiguration + ->builder() + ->permittedFor('clientName') + ->identifiedBy('test') + ->issuedAt((new DateTimeImmutable('2016-01-01'))) + ->canOnlyBeUsedAfter((new DateTimeImmutable('2016-01-01'))) + ->expiresAt((new DateTimeImmutable('2017-01-01'))) + ->relatedTo('123') + ->withClaim('scopes', ['a', 'b', 'c']) + ->getToken($jwtConfiguration->signer(), $jwtConfiguration->signingKey()) + ->toString(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn(['token' => $token]); + + $response = $introspectionServer->respondToIntrospectionRequest($requestMock, new Response); + $response->getBody()->rewind(); + $responseData = \json_decode($response->getBody()->getContents(), true); + + $this->assertEquals([ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => [ + 'a', 'b', 'c', + ], + 'client_id' => [ + 'clientName', + ], + 'exp' => [ + 'date' => '2017-01-01 00:00:00.000000', + 'timezone_type' => 1, + 'timezone' => '+00:00', + ], + 'iat' => [ + 'date' => '2016-01-01 00:00:00.000000', + 'timezone_type' => 1, + 'timezone' => '+00:00', + ], + 'sub' => '123', + 'jti' => 'test', + ], $responseData); + } +} diff --git a/tests/IntrospectionValidators/BearerTokenValidatorTest.php b/tests/IntrospectionValidators/BearerTokenValidatorTest.php new file mode 100644 index 000000000..f5f055cc4 --- /dev/null +++ b/tests/IntrospectionValidators/BearerTokenValidatorTest.php @@ -0,0 +1,197 @@ +expectException(OAuthServerException::class); + + $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setMethodsExcept([ + 'validateIntrospection', + ]) + ->disableOriginalConstructor() + ->getMock(); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('GET'); + + $validator->validateIntrospection($requestMock); + } + + public function testReturnsFalseWhenNoTokenPassed() + { + $validator = $this->getMockBuilder(BearerTokenValidator::class) + ->setMethodsExcept([ + 'validateIntrospection', + ]) + ->disableOriginalConstructor() + ->getMock(); + + $validator->method('getTokenFromRequest')->will( + $this->throwException(new InvalidArgumentException()) + ); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + + $this->assertFalse($validator->validateIntrospection($requestMock)); + } + + public function testReturnsFalseWhenTokenIsRevoked() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $accessTokenRepositoryMock->method('isAccessTokenRevoked') + ->willReturn(true); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $validJwt->toString(), + ]); + + $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); + } + + public function testReturnsFalseWhenTokenIsExpired() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->sub(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $validJwt->toString(), + ]); + + $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); + } + + public function testReturnsFalseWhenTokenIsIssuedByDifferentPrivateKey() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $invalidJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key.crlf')); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $invalidJwt->toString(), + ]); + + $this->assertFalse($bearerTokenValidator->validateIntrospection($requestMock)); + } + + public function testReturnsTrueWhenTokenIsValid() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class) + ->getMock(); + + $accessTokenRepositoryMock->method('isAccessTokenRevoked') + ->willReturn(false); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt(new DateTimeImmutable()) + ->canOnlyBeUsedAfter(new DateTimeImmutable()) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); + + $requestMock = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + + $requestMock->method('getMethod')->willReturn('POST'); + $requestMock->method('getParsedBody')->willReturn([ + 'token' => $validJwt->toString(), + ]); + + $this->assertTrue($bearerTokenValidator->validateIntrospection($requestMock)); + } +} diff --git a/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php new file mode 100644 index 000000000..fec277766 --- /dev/null +++ b/tests/ResponseTypes/BearerTokenIntrospectionResponseTest.php @@ -0,0 +1,113 @@ +generateHttpResponse(new Response()); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertCorrectIntrospectionHeaders($response); + + $response->getBody()->rewind(); + $json = \json_decode($response->getBody()->getContents(), true); + $this->assertEquals([ + 'active' => false, + ], $json); + } + + public function testValidIntrospectionResponse() + { + $responseType = $this->getMockBuilder(BearerTokenResponse::class) + ->onlyMethods([ + 'getTokenFromRequest', + 'getClaimFromToken', + ]) + ->getMock(); + + $tokenMock = $this->getMockBuilder(UnencryptedToken::class) + ->getMock(); + + $responseType->method('getTokenFromRequest')->willReturn($tokenMock); + $responseType->method('getClaimFromToken')->willReturn('value'); + + $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(), true); + $this->assertEquals([ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => 'value', + 'client_id' => 'value', + 'exp' => 'value', + 'iat' => 'value', + 'sub' => 'value', + 'jti' => 'value', + ], $json); + } + + public function testValidIntrospectionResponseWithExtraParams() + { + $responseType = $this->getMockBuilder(BearerTokenResponse::class) + ->onlyMethods([ + 'getTokenFromRequest', + 'getClaimFromToken', + 'getExtraParams', + ]) + ->getMock(); + + $tokenMock = $this->getMockBuilder(UnencryptedToken::class) + ->getMock(); + + $responseType->method('getTokenFromRequest') + ->willReturn($tokenMock); + + $responseType->method('getClaimFromToken') + ->willReturn('value'); + + $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(), true); + $this->assertEquals([ + 'active' => true, + 'token_type' => 'access_token', + 'scope' => 'value', + 'client_id' => 'value', + 'iat' => 'value', + 'exp' => 'value', + 'sub' => 'value', + 'jti' => 'value', + 'extra' => 'param', + ], $json); + } + + private function assertCorrectIntrospectionHeaders(ResponseInterface $response) + { + $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]); + } +}