Skip to content

Commit 9aeed31

Browse files
committed
:octocat: RFC-7009 Token Revocation
1 parent 852edf6 commit 9aeed31

18 files changed

+182
-224
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ A transparent, framework-agnostic, easily extensible PHP [PSR-18](https://www.ph
4141
- [Client Credentials Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)
4242
- [Token refresh](https://datatracker.ietf.org/doc/html/rfc6749#section-1.5)
4343
- [CSRF Token](https://datatracker.ietf.org/doc/html/rfc6749#section-10.12) ("state" parameter)
44+
- [RFC-7009: Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)
4445
- [RFC-7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange)
4546
- [RFC-9126: PAR](https://datatracker.ietf.org/doc/html/rfc9126) (Pushed Authorization Requests)
4647
- Proprietary, OAuth-like authorization flows (e.g. [Last.fm](https://www.last.fm/api/authentication))

docs/Basics/Overview.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fully [PSR-7](https://www.php-fig.org/psr/psr-7/)/[PSR-17](https://www.php-fig.o
1313
- [Client Credentials Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)
1414
- [Token refresh](https://datatracker.ietf.org/doc/html/rfc6749#section-1.5)
1515
- [CSRF Token](https://datatracker.ietf.org/doc/html/rfc6749#section-10.12) ("state" parameter)
16+
- [RFC-7009: Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)
1617
- [RFC-7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange)
1718
- [RFC-9126: PAR](https://datatracker.ietf.org/doc/html/rfc9126) (Pushed Authorization Requests)
1819
- Proprietary, OAuth-like authorization flows (e.g. [Last.fm](https://www.last.fm/api/authentication))

docs/Development/Additional-functionality.md

+23-29
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,16 @@ class MyOAuth2Provider extends OAuth2Provider implements PAR{
266266

267267
## `TokenInvalidate`
268268

269-
This is interface is *not* implemented in the abstract providers, as it may differ drastically between services or is not supported at all.
269+
The `TokenInvalidate` adds support for *"Token Revocation""* as described in [RFC-7009](https://datatracker.ietf.org/doc/html/rfc7009).
270270
The method `TokenInvalidate::invalidateAccessToken()` takes an `AccessToken` as optional parameter, in which case this token should be invalidated,
271271
otherwise the token for the current user should be fetched from the storage and be used in the invalidation request.
272+
An optional ["token type hint"](https://datatracker.ietf.org/doc/html/rfc7009#section-2.1) can be given with the `$type` parameter (defaults to `access_token`).
272273

273-
The more common implementation looks as follows: the access token along with client-id is sent with a `POST` request as url-encoded
274-
form-data in the body, and the server responds with either a HTTP 200 and (often) an empty body or a HTTP 204.
275-
On a successful response, the token should be deleted from the storage.
274+
The more common implementation looks as follows: the access token along with type hint (and sometimes other parameters) is sent
275+
with a `POST` request as url-encoded form-data in the body, and the server responds with either an HTTP 200 and (often) an empty
276+
body or an HTTP 204. On a successful response, the token should be deleted from the storage.
277+
278+
The implementation in `OAuth2Provider` is divided in parts that can be overridden separately:
276279

277280
```php
278281
class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{
@@ -281,37 +284,28 @@ class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{
281284
* ...
282285
*/
283286

284-
public function invalidateAccessToken(AccessToken|null $token = null):bool{
285-
$tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name));
286-
287-
// the body may vary between services
288-
$bodyParams = [
289-
'client_id' => $this->options->key,
290-
'token' => $tokenToInvalidate->accessToken,
291-
];
287+
protected function sendTokenInvalidateRequest(string $url, array $body){
292288

293-
// prepare the request
294289
$request = $this->requestFactory
295-
->createRequest('POST', $this->revokeURL)
290+
->createRequest('POST', $url)
296291
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
297292
;
298293

299-
// encode the body according to the content-type given in the request header
300-
$request = $this->setRequestBody($bodyParams, $request);
294+
// an additional basic auth header is set
295+
$request = $this->addBasicAuthHeader($request);
296+
$request = $this->setRequestBody($body, $request);
301297

302-
// bypass the host check and request authorization
303-
$response = $this->http->sendRequest($request);
304-
305-
if($response->getStatusCode() === 200){
306-
// delete the token on success (only if it wasn't given via param)
307-
if($token === null){
308-
$this->storage->clearAccessToken($this->name);
309-
}
310-
311-
return true;
312-
}
298+
return $this->http->sendRequest($request);
299+
}
313300

314-
return false;
301+
protected function getInvalidateAccessTokenBodyParams(AccessToken $token, string $type):array{
302+
return [
303+
// here, client_id and client_secret are set additionally
304+
'client_id' => $this->options->key,
305+
'client_secret' => $this->options->secret,
306+
'token' => $token->accessToken,
307+
'token_type_hint' => $type,
308+
];
315309
}
316310

317311
}
@@ -328,7 +322,7 @@ class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{
328322
* ...
329323
*/
330324

331-
public function invalidateAccessToken(AccessToken|null $token = null):bool{
325+
public function invalidateAccessToken(AccessToken|null $token = null, string|null $type = null):bool{
332326

333327
// a token was given
334328
if($token !== null){

docs/index.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
.. php-qrcode documentation master file, created by sphinx-quickstart on Sun Jul 9 21:45:56 2023.
22
markdown-rst converter: https://pandoc.org/try/
33
4-
================
5-
PHP-OAuth Manual
6-
================
4+
===========================
5+
chillerlan PHP-OAuth Manual
6+
===========================
77

88
User manual for `chillerlan/php-oauth <https://github.com/chillerlan/php-oauth/>`__ [|version|]. Updated on |today|.
99

src/Core/OAuth2Provider.php

+77-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
use chillerlan\OAuth\Providers\ProviderException;
1818
use Psr\Http\Message\{RequestInterface, ResponseInterface, UriInterface};
1919
use Throwable;
20-
use function array_merge, date, explode, hash, hash_equals, implode, in_array, is_array, random_int, sodium_bin2base64, sprintf;
20+
use function array_merge, date, explode, hash, hash_equals, implode, in_array, is_array, random_int,
21+
sodium_bin2base64, sprintf, str_contains, strtolower, trim;
2122
use const PHP_QUERY_RFC1738, PHP_VERSION_ID, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING;
2223

2324
/**
@@ -407,6 +408,81 @@ protected function getRefreshAccessTokenRequestBodyParams(string $refreshToken):
407408
}
408409

409410

411+
/*
412+
* TokenInvalidate
413+
*/
414+
415+
/**
416+
* @implements \chillerlan\OAuth\Core\TokenInvalidate::invalidateAccessToken()
417+
* @throws \chillerlan\OAuth\Providers\ProviderException
418+
*/
419+
public function invalidateAccessToken(AccessToken $token = null, string|null $type = null):bool{
420+
$type = strtolower(trim($type ?? 'access_token'));
421+
422+
// @link https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
423+
if(!in_array($type, ['access_token', 'refresh_token'])){
424+
throw new ProviderException(sprintf('invalid token type "%s"', $type));
425+
}
426+
427+
$tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name));
428+
$body = $this->getInvalidateAccessTokenBodyParams($tokenToInvalidate, $type);
429+
$response = $this->sendTokenInvalidateRequest($this->revokeURL, $body);
430+
431+
// some endpoints may return 204, others 200 with empty body
432+
if(in_array($response->getStatusCode(), [200, 204], true)){
433+
434+
// if the token was given via parameter it cannot be deleted from storage
435+
if($token === null){
436+
$this->storage->clearAccessToken($this->name);
437+
}
438+
439+
return true;
440+
}
441+
442+
// ok, let's see if we got a response body
443+
// @link https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1
444+
if(str_contains($response->getHeaderLine('content-type'), 'json')){
445+
$json = MessageUtil::decodeJSON($response);
446+
447+
if(isset($json['error'])){
448+
throw new ProviderException($json['error']);
449+
}
450+
}
451+
452+
return false;
453+
}
454+
455+
/**
456+
* Prepares and sends a request to the token invalidation endpoint
457+
*
458+
* @see \chillerlan\OAuth\Core\OAuth2Provider::invalidateAccessToken()
459+
*/
460+
protected function sendTokenInvalidateRequest(string $url, array $body):ResponseInterface{
461+
462+
$request = $this->requestFactory
463+
->createRequest('POST', $url)
464+
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
465+
;
466+
467+
// some enpoints may require a basic auth header here
468+
$request = $this->setRequestBody($body, $request);
469+
470+
return $this->http->sendRequest($request);
471+
}
472+
473+
/**
474+
* Prepares the body for a token revocation request
475+
*
476+
* @see \chillerlan\OAuth\Core\OAuth2Provider::invalidateAccessToken()
477+
*/
478+
protected function getInvalidateAccessTokenBodyParams(AccessToken $token, string $type):array{
479+
return [
480+
'token' => $token->accessToken,
481+
'token_type_hint' => $type,
482+
];
483+
}
484+
485+
410486
/*
411487
* CSRFToken
412488
*/

src/Core/TokenInvalidate.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
namespace chillerlan\OAuth\Core;
1313

1414
/**
15-
* Indicates whether the provider is capable of invalidating access tokens
15+
* Indicates whether the provider is capable of invalidating access tokens (RFC-7009 or proprietary)
16+
*
17+
* @link https://datatracker.ietf.org/doc/html/rfc7009
1618
*/
1719
interface TokenInvalidate{
1820

@@ -29,6 +31,6 @@ interface TokenInvalidate{
2931
*
3032
* @throws \chillerlan\OAuth\Providers\ProviderException
3133
*/
32-
public function invalidateAccessToken(AccessToken|null $token = null):bool;
34+
public function invalidateAccessToken(AccessToken|null $token = null, string|null $type = null):bool;
3335

3436
}

src/Providers/BigCartel.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function me():AuthenticatedUser{
5656
/**
5757
* @inheritDoc
5858
*/
59-
public function invalidateAccessToken(AccessToken|null $token = null):bool{
59+
public function invalidateAccessToken(AccessToken|null $token = null, string|null $type = null):bool{
6060
$tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name));
6161

6262
$request = $this->requestFactory

src/Providers/DeviantArt.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public function me():AuthenticatedUser{
8080
/**
8181
* @inheritDoc
8282
*/
83-
public function invalidateAccessToken(AccessToken|null $token = null):bool{
83+
public function invalidateAccessToken(AccessToken|null $token = null, string|null $type = null):bool{
8484

8585
if($token !== null){
8686
// to revoke a token different from the one of the currently authenticated user,

src/Providers/Discord.php

+13-32
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ class Discord extends OAuth2Provider implements ClientCredentials, CSRFToken, To
6464
protected string|null $apiDocs = 'https://discord.com/developers/';
6565
protected string|null $applicationURL = 'https://discordapp.com/developers/applications/';
6666

67+
/**
68+
* @inheritDoc
69+
* @link https://github.com/discord/discord-api-docs/issues/2259#issuecomment-927180184
70+
*/
71+
protected function getInvalidateAccessTokenBodyParams(AccessToken $token, string $type):array{
72+
return [
73+
'client_id' => $this->options->key,
74+
'client_secret' => $this->options->secret,
75+
'token' => $token->accessToken,
76+
'token_type_hint' => $type,
77+
];
78+
}
79+
6780
/**
6881
* @inheritDoc
6982
* @codeCoverageIgnore
@@ -84,36 +97,4 @@ public function me():AuthenticatedUser{
8497
return new AuthenticatedUser($userdata);
8598
}
8699

87-
/**
88-
* @inheritDoc
89-
*/
90-
public function invalidateAccessToken(AccessToken $token = null):bool{
91-
$tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name));
92-
93-
$bodyParams = [
94-
'client_id' => $this->options->key,
95-
'client_secret' => $this->options->secret,
96-
'token' => $tokenToInvalidate->accessToken,
97-
];
98-
99-
$request = $this->requestFactory
100-
->createRequest('POST', $this->revokeURL)
101-
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
102-
;
103-
104-
$request = $this->setRequestBody($bodyParams, $request);
105-
$response = $this->http->sendRequest($request);
106-
107-
if($response->getStatusCode() === 200){
108-
109-
if($token === null){
110-
$this->storage->clearAccessToken($this->name);
111-
}
112-
113-
return true;
114-
}
115-
116-
return false;
117-
}
118-
119100
}

src/Providers/MusicBrainz.php

+12-32
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ protected function getRefreshAccessTokenRequestBodyParams(string $refreshToken):
6565
];
6666
}
6767

68+
/**
69+
* @inheritDoc
70+
*/
71+
protected function getInvalidateAccessTokenBodyParams(AccessToken $token, string $type):array{
72+
return [
73+
'client_id' => $this->options->key,
74+
'client_secret' => $this->options->secret,
75+
'token' => $token->accessToken,
76+
'token_type_hint' => $type,
77+
];
78+
}
79+
6880
/**
6981
* @inheritDoc
7082
*/
@@ -108,36 +120,4 @@ public function me():AuthenticatedUser{
108120
return new AuthenticatedUser($userdata);
109121
}
110122

111-
/**
112-
* @inheritDoc
113-
*/
114-
public function invalidateAccessToken(AccessToken|null $token = null):bool{
115-
$tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name));
116-
117-
$request = $this->requestFactory
118-
->createRequest('POST', $this->revokeURL)
119-
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
120-
;
121-
122-
$bodyParams = [
123-
'client_id' => $this->options->key,
124-
'client_secret' => $this->options->secret,
125-
'token' => $tokenToInvalidate->accessToken,
126-
];
127-
128-
$request = $this->setRequestBody($bodyParams, $request);
129-
$response = $this->http->sendRequest($request);
130-
131-
if($response->getStatusCode() === 200){
132-
133-
if($token === null){
134-
$this->storage->clearAccessToken($this->name);
135-
}
136-
137-
return true;
138-
}
139-
140-
return false;
141-
}
142-
143123
}

src/Providers/NPROne.php

+1-32
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace chillerlan\OAuth\Providers;
1515

16-
use chillerlan\OAuth\Core\{AccessToken, AuthenticatedUser, CSRFToken, OAuth2Provider, TokenInvalidate, TokenRefresh, UserInfo};
16+
use chillerlan\OAuth\Core\{AuthenticatedUser, CSRFToken, OAuth2Provider, TokenInvalidate, TokenRefresh, UserInfo};
1717
use function in_array, sprintf, strtolower;
1818

1919
/**
@@ -76,35 +76,4 @@ public function me():AuthenticatedUser{
7676
return new AuthenticatedUser($userdata);
7777
}
7878

79-
/**
80-
* @inheritDoc
81-
*/
82-
public function invalidateAccessToken(AccessToken|null $token = null):bool{
83-
$tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name));
84-
85-
$bodyParams = [
86-
'token' => $tokenToInvalidate->accessToken,
87-
'token_type_hint' => 'access_token',
88-
];
89-
90-
$request = $this->requestFactory
91-
->createRequest('POST', $this->revokeURL)
92-
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
93-
;
94-
95-
$request = $this->setRequestBody($bodyParams, $request);
96-
$response = $this->http->sendRequest($request);
97-
98-
if($response->getStatusCode() === 200){
99-
100-
if($token === null){
101-
$this->storage->clearAccessToken($this->name);
102-
}
103-
104-
return true;
105-
}
106-
107-
return false;
108-
}
109-
11079
}

0 commit comments

Comments
 (0)