Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add introspection implementation #869

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
17 changes: 17 additions & 0 deletions src/AuthorizationServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -197,6 +198,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check that the request is a POST request as per the RFC, section 2.1

{
$introspector = new Introspector($this->accessTokenRepository, $this->privateKey, new Parser);
$introspectionResponse = $introspector->respondToIntrospectionRequest($request);

return $introspectionResponse->generateHttpResponse($response);
}

/**
* Get the token type that grants will return in the HTTP response.
*
Expand Down
1 change: 1 addition & 0 deletions src/Grant/AuthCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
165 changes: 165 additions & 0 deletions src/Introspector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php

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\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\ResponseTypes\IntrospectionResponse;
use Psr\Http\Message\ServerRequestInterface;

class Introspector
{
/**
* @var AccessTokenRepositoryInterface
*/
private $accessTokenRepository;

/**
* @var CryptKey
*/
private $privateKey;

/**
* @var Parser
*/
private $parser;

/**
* New Introspector instance.
*
* @param AccessTokenRepositoryInterface $accessTokenRepository
* @param CryptKey $privateKey
* @param Parser $parser
*/
public function __construct(
AccessTokenRepositoryInterface $accessTokenRepository,
CryptKey $privateKey,
Parser $parser
) {
$this->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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should use exceptions to control the flow of the program here. It is expected that we sometimes will fail these validation checks so I think this would be better handled with boolean checks

$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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use existing verifiers for this within the package?

{
$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');
}
}

/**
* 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;
}
}
41 changes: 41 additions & 0 deletions src/ResponseTypes/IntrospectionResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace League\OAuth2\Server\ResponseTypes;

use Psr\Http\Message\ResponseInterface;

class IntrospectionResponse extends AbstractResponseType
{
/**
* @var array
*/
private $introspectionData;

/**
* @param array $introspectionData
*/
public function setIntrospectionData(array $introspectionData)
{
$this->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;
}
}
Loading