Skip to content

Commit fbc0289

Browse files
support token revocation and introspection
1 parent 07920aa commit fbc0289

13 files changed

+759
-246
lines changed

src/AbstractHandler.php

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\OAuth2\Server;
6+
7+
use Exception;
8+
use League\OAuth2\Server\Entities\ClientEntityInterface;
9+
use League\OAuth2\Server\EventEmitting\EmitterAwareInterface;
10+
use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill;
11+
use League\OAuth2\Server\Exception\OAuthServerException;
12+
use League\OAuth2\Server\Grant\GrantTypeInterface;
13+
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
14+
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
15+
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
16+
use Psr\Http\Message\ServerRequestInterface;
17+
18+
use function base64_decode;
19+
use function explode;
20+
use function json_decode;
21+
use function substr;
22+
use function time;
23+
use function trim;
24+
25+
abstract class AbstractHandler implements EmitterAwareInterface
26+
{
27+
use EmitterAwarePolyfill;
28+
use CryptTrait;
29+
30+
protected ClientRepositoryInterface $clientRepository;
31+
32+
protected AccessTokenRepositoryInterface $accessTokenRepository;
33+
34+
protected RefreshTokenRepositoryInterface $refreshTokenRepository;
35+
36+
public function setClientRepository(ClientRepositoryInterface $clientRepository): void
37+
{
38+
$this->clientRepository = $clientRepository;
39+
}
40+
41+
public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void
42+
{
43+
$this->accessTokenRepository = $accessTokenRepository;
44+
}
45+
46+
public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void
47+
{
48+
$this->refreshTokenRepository = $refreshTokenRepository;
49+
}
50+
51+
/**
52+
* Validate the client.
53+
*
54+
* @throws OAuthServerException
55+
*/
56+
protected function validateClient(ServerRequestInterface $request): ClientEntityInterface
57+
{
58+
[$clientId, $clientSecret] = $this->getClientCredentials($request);
59+
60+
$client = $this->getClientEntityOrFail($clientId, $request);
61+
62+
if ($client->isConfidential()) {
63+
if ($clientSecret === '') {
64+
throw OAuthServerException::invalidRequest('client_secret');
65+
}
66+
67+
if (
68+
$this->clientRepository->validateClient(
69+
$clientId,
70+
$clientSecret,
71+
$this instanceof GrantTypeInterface ? $this->getIdentifier() : null
72+
) === false
73+
) {
74+
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
75+
76+
throw OAuthServerException::invalidClient($request);
77+
}
78+
}
79+
80+
return $client;
81+
}
82+
83+
/**
84+
* Wrapper around ClientRepository::getClientEntity() that ensures we emit
85+
* an event and throw an exception if the repo doesn't return a client
86+
* entity.
87+
*
88+
* This is a bit of defensive coding because the interface contract
89+
* doesn't actually enforce non-null returns/exception-on-no-client so
90+
* getClientEntity might return null. By contrast, this method will
91+
* always either return a ClientEntityInterface or throw.
92+
*
93+
* @throws OAuthServerException
94+
*/
95+
protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface
96+
{
97+
$client = $this->clientRepository->getClientEntity($clientId);
98+
99+
if ($client instanceof ClientEntityInterface === false) {
100+
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
101+
throw OAuthServerException::invalidClient($request);
102+
}
103+
104+
return $client;
105+
}
106+
107+
/**
108+
* Gets the client credentials from the request from the request body or
109+
* the Http Basic Authorization header
110+
*
111+
* @return array{0:non-empty-string,1:string}
112+
*
113+
* @throws OAuthServerException
114+
*/
115+
protected function getClientCredentials(ServerRequestInterface $request): array
116+
{
117+
[$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request);
118+
119+
$clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
120+
121+
if ($clientId === null) {
122+
throw OAuthServerException::invalidRequest('client_id');
123+
}
124+
125+
$clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
126+
127+
return [$clientId, $clientSecret ?? ''];
128+
}
129+
/**
130+
* Parse request parameter.
131+
*
132+
* @param array<array-key, mixed> $request
133+
*
134+
* @return non-empty-string|null
135+
*
136+
* @throws OAuthServerException
137+
*/
138+
private static function parseParam(string $parameter, array $request, ?string $default = null): ?string
139+
{
140+
$value = $request[$parameter] ?? '';
141+
142+
if (is_scalar($value)) {
143+
$value = trim((string) $value);
144+
} else {
145+
throw OAuthServerException::invalidRequest($parameter);
146+
}
147+
148+
if ($value === '') {
149+
$value = $default === null ? null : trim($default);
150+
151+
if ($value === '') {
152+
$value = null;
153+
}
154+
}
155+
156+
return $value;
157+
}
158+
159+
/**
160+
* Retrieve request parameter.
161+
*
162+
* @return non-empty-string|null
163+
*
164+
* @throws OAuthServerException
165+
*/
166+
protected function getRequestParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
167+
{
168+
return self::parseParam($parameter, (array) $request->getParsedBody(), $default);
169+
}
170+
171+
/**
172+
* Retrieve HTTP Basic Auth credentials with the Authorization header
173+
* of a request. First index of the returned array is the username,
174+
* second is the password (so list() will work). If the header does
175+
* not exist, or is otherwise an invalid HTTP Basic header, return
176+
* [null, null].
177+
*
178+
* @return array{0:non-empty-string,1:string}|array{0:null,1:null}
179+
*/
180+
protected function getBasicAuthCredentials(ServerRequestInterface $request): array
181+
{
182+
if (!$request->hasHeader('Authorization')) {
183+
return [null, null];
184+
}
185+
186+
$header = $request->getHeader('Authorization')[0];
187+
if (stripos($header, 'Basic ') !== 0) {
188+
return [null, null];
189+
}
190+
191+
$decoded = base64_decode(substr($header, 6), true);
192+
193+
if ($decoded === false) {
194+
return [null, null];
195+
}
196+
197+
if (str_contains($decoded, ':') === false) {
198+
return [null, null]; // HTTP Basic header without colon isn't valid
199+
}
200+
201+
[$username, $password] = explode(':', $decoded, 2);
202+
203+
if ($username === '') {
204+
return [null, null];
205+
}
206+
207+
return [$username, $password];
208+
}
209+
210+
/**
211+
* Retrieve query string parameter.
212+
*
213+
* @return non-empty-string|null
214+
*
215+
* @throws OAuthServerException
216+
*/
217+
protected function getQueryStringParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
218+
{
219+
return self::parseParam($parameter, $request->getQueryParams(), $default);
220+
}
221+
222+
/**
223+
* Retrieve cookie parameter.
224+
*
225+
* @return non-empty-string|null
226+
*
227+
* @throws OAuthServerException
228+
*/
229+
protected function getCookieParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
230+
{
231+
return self::parseParam($parameter, $request->getCookieParams(), $default);
232+
}
233+
234+
/**
235+
* Retrieve server parameter.
236+
*
237+
* @return non-empty-string|null
238+
*
239+
* @throws OAuthServerException
240+
*/
241+
protected function getServerParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
242+
{
243+
return self::parseParam($parameter, $request->getServerParams(), $default);
244+
}
245+
246+
/**
247+
* Validate the given encrypted refresh token.
248+
*
249+
* @throws OAuthServerException
250+
*
251+
* @return array<string, mixed>
252+
*/
253+
protected function validateEncryptedRefreshToken(
254+
ServerRequestInterface $request,
255+
string $encryptedRefreshToken,
256+
string $clientId
257+
): array {
258+
try {
259+
$refreshToken = $this->decrypt($encryptedRefreshToken);
260+
} catch (Exception $e) {
261+
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
262+
}
263+
264+
$refreshTokenData = json_decode($refreshToken, true);
265+
266+
if ($refreshTokenData['client_id'] !== $clientId) {
267+
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
268+
throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
269+
}
270+
271+
if ($refreshTokenData['expire_time'] < time()) {
272+
throw OAuthServerException::invalidRefreshToken('Token has expired');
273+
}
274+
275+
if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) {
276+
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
277+
}
278+
279+
return $refreshTokenData;
280+
}
281+
}

src/AuthorizationValidators/BearerTokenValidator.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
use function preg_replace;
3535
use function trim;
3636

37-
class BearerTokenValidator implements AuthorizationValidatorInterface
37+
class BearerTokenValidator implements AuthorizationValidatorInterface, JwtValidatorInterface
3838
{
3939
use CryptTrait;
4040

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

102+
$claims = $this->validateJwt($request, $jwt);
103+
104+
// Return the request with additional attributes
105+
return $request
106+
->withAttribute('oauth_access_token_id', $claims['jti'] ?? null)
107+
->withAttribute('oauth_client_id', $claims['aud'][0] ?? null)
108+
->withAttribute('oauth_user_id', $claims['sub'] ?? null)
109+
->withAttribute('oauth_scopes', $claims['scopes'] ?? null);
110+
}
111+
112+
/**
113+
* {@inheritdoc}
114+
*/
115+
public function validateJwt(ServerRequestInterface $request, string $jwt, ?string $clientId = null): array
116+
{
102117
try {
103118
// Attempt to parse the JWT
104119
$token = $this->jwtConfiguration->parser()->parse($jwt);
@@ -120,16 +135,20 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe
120135

121136
$claims = $token->claims();
122137

138+
// Check if token is linked to the client
139+
if (
140+
$clientId !== null &&
141+
$claims->get('client_id') !== $clientId &&
142+
!$token->isPermittedFor($clientId)
143+
) {
144+
throw OAuthServerException::accessDenied('Access token is not linked to client');
145+
}
146+
123147
// Check if token has been revoked
124148
if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) {
125149
throw OAuthServerException::accessDenied('Access token has been revoked');
126150
}
127151

128-
// Return the request with additional attributes
129-
return $request
130-
->withAttribute('oauth_access_token_id', $claims->get('jti'))
131-
->withAttribute('oauth_client_id', $claims->get('aud')[0])
132-
->withAttribute('oauth_user_id', $claims->get('sub'))
133-
->withAttribute('oauth_scopes', $claims->get('scopes'));
152+
return $claims->all();
134153
}
135154
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\OAuth2\Server\AuthorizationValidators;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
9+
interface JwtValidatorInterface
10+
{
11+
/**
12+
* Parse and validate the given JWT.
13+
*
14+
* @return array<non-empty-string, mixed>
15+
*/
16+
public function validateJwt(ServerRequestInterface $request, string $jwt, ?string $clientId = null): array;
17+
}

src/Exception/OAuthServerException.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,20 @@ public static function unauthorizedClient(?string $hint = null): static
265265
);
266266
}
267267

268+
/**
269+
* Unsupported Token Type error.
270+
*/
271+
public static function unsupportedTokenType(?string $hint = null): static
272+
{
273+
return new static(
274+
'The authorization server does not support the revocation of the presented token type.',
275+
15,
276+
'unsupported_token_type',
277+
400,
278+
$hint
279+
);
280+
}
281+
268282
/**
269283
* Generate a HTTP response.
270284
*/

0 commit comments

Comments
 (0)