Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ The following RFCs are implemented:

* [RFC6749 "OAuth 2.0"](https://tools.ietf.org/html/rfc6749)
* [RFC6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage"](https://tools.ietf.org/html/rfc6750)
* [RFC7009 "OAuth 2.0 Token Revocation"](https://tools.ietf.org/html/rfc7009)
* [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)
* [RFC8628 "OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628)

This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](https://twitter.com/alexbilbie).
Expand Down
282 changes: 282 additions & 0 deletions src/AbstractHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
<?php

declare(strict_types=1);

namespace League\OAuth2\Server;

use Exception;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\EventEmitting\EmitterAwareInterface;
use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\GrantTypeInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use Psr\Http\Message\ServerRequestInterface;

use function base64_decode;
use function explode;
use function json_decode;
use function substr;
use function time;
use function trim;

abstract class AbstractHandler implements EmitterAwareInterface
{
use EmitterAwarePolyfill;
use CryptTrait;

protected ClientRepositoryInterface $clientRepository;

protected AccessTokenRepositoryInterface $accessTokenRepository;

protected RefreshTokenRepositoryInterface $refreshTokenRepository;
Comment on lines +27 to +34
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From AbstractGrant class.


public function setClientRepository(ClientRepositoryInterface $clientRepository): void
{
$this->clientRepository = $clientRepository;
}

public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void
{
$this->accessTokenRepository = $accessTokenRepository;
}

public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void
{
$this->refreshTokenRepository = $refreshTokenRepository;
}
Comment on lines +36 to +49
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From AbstractGrant class.


/**
* Validate the client.
*
* @throws OAuthServerException
*/
protected function validateClient(ServerRequestInterface $request): ClientEntityInterface
{
[$clientId, $clientSecret] = $this->getClientCredentials($request);

$client = $this->getClientEntityOrFail($clientId, $request);

if ($client->isConfidential()) {
if ($clientSecret === '') {
throw OAuthServerException::invalidRequest('client_secret');
}

if (
$this->clientRepository->validateClient(
$clientId,
$clientSecret,
$this instanceof GrantTypeInterface ? $this->getIdentifier() : null
Copy link
Contributor Author

@hafezdivandari hafezdivandari Feb 16, 2025

Choose a reason for hiding this comment

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

This line is the only change on this method, since getIdentifier() method is defined on GrantTypeInterface only.

) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));

throw OAuthServerException::invalidClient($request);
}
}

return $client;
}
Comment on lines +51 to +81
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Wrapper around ClientRepository::getClientEntity() that ensures we emit
* an event and throw an exception if the repo doesn't return a client
* entity.
*
* This is a bit of defensive coding because the interface contract
* doesn't actually enforce non-null returns/exception-on-no-client so
* getClientEntity might return null. By contrast, this method will
* always either return a ClientEntityInterface or throw.
*
* @throws OAuthServerException
*/
protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface
{
$client = $this->clientRepository->getClientEntity($clientId);

if ($client instanceof ClientEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}

return $client;
}
Comment on lines +83 to +105
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Gets the client credentials from the request from the request body or
* the Http Basic Authorization header
*
* @return array{0:non-empty-string,1:string}
*
* @throws OAuthServerException
*/
protected function getClientCredentials(ServerRequestInterface $request): array
{
[$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request);

$clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);

if ($clientId === null) {
throw OAuthServerException::invalidRequest('client_id');
}

$clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);

return [$clientId, $clientSecret ?? ''];
}
Comment on lines +107 to +128
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Parse request parameter.
*
* @param array<array-key, mixed> $request
*
* @return non-empty-string|null
*
* @throws OAuthServerException
*/
private static function parseParam(string $parameter, array $request, ?string $default = null): ?string
{
$value = $request[$parameter] ?? '';

if (is_scalar($value)) {
$value = trim((string) $value);
} else {
throw OAuthServerException::invalidRequest($parameter);
}

if ($value === '') {
$value = $default === null ? null : trim($default);

if ($value === '') {
$value = null;
}
}

return $value;
}
Comment on lines +130 to +158
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Retrieve request parameter.
*
* @return non-empty-string|null
*
* @throws OAuthServerException
*/
protected function getRequestParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
{
return self::parseParam($parameter, (array) $request->getParsedBody(), $default);
}
Comment on lines +160 to +170
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Retrieve HTTP Basic Auth credentials with the Authorization header
* of a request. First index of the returned array is the username,
* second is the password (so list() will work). If the header does
* not exist, or is otherwise an invalid HTTP Basic header, return
* [null, null].
*
* @return array{0:non-empty-string,1:string}|array{0:null,1:null}
*/
protected function getBasicAuthCredentials(ServerRequestInterface $request): array
{
if (!$request->hasHeader('Authorization')) {
return [null, null];
}

$header = $request->getHeader('Authorization')[0];
if (stripos($header, 'Basic ') !== 0) {
return [null, null];
}

$decoded = base64_decode(substr($header, 6), true);

if ($decoded === false) {
return [null, null];
}

if (str_contains($decoded, ':') === false) {
return [null, null]; // HTTP Basic header without colon isn't valid
}

[$username, $password] = explode(':', $decoded, 2);

if ($username === '') {
return [null, null];
}

return [$username, $password];
}
Comment on lines +172 to +209
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Retrieve query string parameter.
*
* @return non-empty-string|null
*
* @throws OAuthServerException
*/
protected function getQueryStringParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
{
return self::parseParam($parameter, $request->getQueryParams(), $default);
}
Comment on lines +211 to +221
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Retrieve cookie parameter.
*
* @return non-empty-string|null
*
* @throws OAuthServerException
*/
protected function getCookieParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
{
return self::parseParam($parameter, $request->getCookieParams(), $default);
}
Comment on lines +223 to +233
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Retrieve server parameter.
*
* @return non-empty-string|null
*
* @throws OAuthServerException
*/
protected function getServerParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
{
return self::parseParam($parameter, $request->getServerParams(), $default);
}
Comment on lines +235 to +245
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from AbstractGrant class.


/**
* Validate the given encrypted refresh token.
*
* @throws OAuthServerException
*
* @return array<non-empty-string, mixed>
*/
protected function validateEncryptedRefreshToken(
ServerRequestInterface $request,
string $encryptedRefreshToken,
string $clientId
): array {
try {
$refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
}

$refreshTokenData = json_decode($refreshToken, true);

if ($refreshTokenData['client_id'] !== $clientId) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
}

if ($refreshTokenData['expire_time'] < time()) {
throw OAuthServerException::invalidRefreshToken('Token has expired');
}

if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) {
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
}

return $refreshTokenData;
}
Comment on lines +247 to +281
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved from RefreshTokenGrant class.

}
33 changes: 26 additions & 7 deletions src/AuthorizationValidators/BearerTokenValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
use function preg_replace;
use function trim;

class BearerTokenValidator implements AuthorizationValidatorInterface
class BearerTokenValidator implements AuthorizationValidatorInterface, JwtValidatorInterface
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implements new JwtValidatorInterface interface that defines validateJwt() method.

{
use CryptTrait;

Expand Down Expand Up @@ -99,6 +99,21 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe
throw OAuthServerException::accessDenied('Missing "Bearer" token');
}

$claims = $this->validateJwt($request, $jwt);

// Return the request with additional attributes
return $request
->withAttribute('oauth_access_token_id', $claims['jti'] ?? null)
->withAttribute('oauth_client_id', $claims['aud'][0] ?? null)
->withAttribute('oauth_user_id', $claims['sub'] ?? null)
->withAttribute('oauth_scopes', $claims['scopes'] ?? null);
}

/**
* {@inheritdoc}
*/
public function validateJwt(ServerRequestInterface $request, string $jwt, ?string $clientId = null): array
{
Comment on lines +102 to +116
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extract the JWT validation logic from validateAuthorization method into a new validateJwt method.

try {
// Attempt to parse the JWT
$token = $this->jwtConfiguration->parser()->parse($jwt);
Expand All @@ -120,16 +135,20 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe

$claims = $token->claims();

// Check if token is linked to the client
if (
$clientId !== null &&
$claims->get('client_id') !== $clientId &&
!$token->isPermittedFor($clientId)
) {
throw OAuthServerException::accessDenied('Access token is not linked to client');
}

Comment on lines +138 to +146
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new validateJwt method optionally accepts $clientId argument. If a $clientId is provided, it verifies whether the token was issued to the given client.

// Check if token has been revoked
if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) {
throw OAuthServerException::accessDenied('Access token has been revoked');
}

// Return the request with additional attributes
return $request
->withAttribute('oauth_access_token_id', $claims->get('jti'))
->withAttribute('oauth_client_id', $claims->get('aud')[0])
->withAttribute('oauth_user_id', $claims->get('sub'))
->withAttribute('oauth_scopes', $claims->get('scopes'));
return $claims->all();
}
}
20 changes: 20 additions & 0 deletions src/AuthorizationValidators/JwtValidatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace League\OAuth2\Server\AuthorizationValidators;

use Psr\Http\Message\ServerRequestInterface;

interface JwtValidatorInterface
{
/**
* Parse and validate the given JWT.
*
* @param non-empty-string $jwt
* @param non-empty-string|null $clientId
*
* @return array<non-empty-string, mixed>
*/
public function validateJwt(ServerRequestInterface $request, string $jwt, ?string $clientId = null): array;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Define new JwtValidatorInterface interface that defines validateJwt method. This interface is implemented by BearerTokenValidator class.

}
14 changes: 14 additions & 0 deletions src/Exception/OAuthServerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,20 @@ public static function unauthorizedClient(?string $hint = null): static
);
}

/**
* Unsupported Token Type error.
*/
public static function unsupportedTokenType(?string $hint = null): static
{
return new static(
'The authorization server does not support the revocation of the presented token type.',
15,
'unsupported_token_type',
400,
$hint
);
}

/**
* Generate a HTTP response.
*/
Expand Down
Loading