Skip to content

Commit 896ca45

Browse files
committed
Merge branch 'feature/token_revocation'
2 parents 4446231 + af4bb6a commit 896ca45

13 files changed

+374
-41
lines changed

Module.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use chervand\yii2\oauth2\server\components\ResponseTypes\MacTokenResponse;
1313
use chervand\yii2\oauth2\server\components\Server\AuthorizationServer;
1414
use chervand\yii2\oauth2\server\controllers\AuthorizeController;
15+
use chervand\yii2\oauth2\server\controllers\RevokeController;
1516
use chervand\yii2\oauth2\server\controllers\TokenController;
1617
use chervand\yii2\oauth2\server\models\Client;
1718
use League\OAuth2\Server\CryptKey;
@@ -48,6 +49,10 @@ class Module extends \yii\base\Module implements BootstrapInterface
4849
'class' => AuthorizeController::class,
4950
'as corsFilter' => Cors::class,
5051
],
52+
'revoke' => [
53+
'class' => RevokeController::class,
54+
'as corsFilter' => Cors::class,
55+
],
5156
'token' => [
5257
'class' => TokenController::class,
5358
'as corsFilter' => Cors::class,
@@ -121,7 +126,8 @@ public function bootstrap($app)
121126
],
122127
'rules' => ArrayHelper::merge([
123128
['controller' => $this->uniqueId . '/authorize'],
124-
['controller' => $this->uniqueId . '/token']
129+
['controller' => $this->uniqueId . '/revoke'],
130+
['controller' => $this->uniqueId . '/token'],
125131
], $this->urlManagerRules)
126132
]))->rules, false);
127133
}

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
`chervand/yii2-oauth2-server` is a `Yii 2.0 PHP Framework` integration of [`thephpleague/oauth2-server`](https://github.com/thephpleague/oauth2-server) library which implements a standards compliant [`OAuth 2.0 Server`](https://tools.ietf.org/html/rfc6749) for PHP. It supports all of the grants defined in the specification with usage of `JWT` `Bearer` tokens.
44

55
`chervand/yii2-oauth2-server` additionally provides [`MAC`](https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-05) tokens support, which is not supported by the original library, because `MAC` tokens specification is currently in draft and it was not updated since 2014, so it's a pretty experimental feature.
6+
7+
It also includes tokens revocation implementation based on [RFC7009](https://tools.ietf.org/html/rfc7009).
68

79
## Installation
810

@@ -62,14 +64,18 @@ return [
6264
$server->enableGrantType(new \League\OAuth2\Server\Grant\ImplicitGrant(
6365
new \DateInterval('PT1H')
6466
));
65-
$server->enableGrantType(new \League\OAuth2\Server\Grant\RefreshTokenGrant(
66-
$module->refreshTokenRepository
67-
));
6867
$server->enableGrantType(new \League\OAuth2\Server\Grant\PasswordGrant(
6968
$module->userRepository,
7069
$module->refreshTokenRepository
7170
));
7271
$server->enableGrantType(new \League\OAuth2\Server\Grant\ClientCredentialsGrant());
72+
$server->enableGrantType(new \League\OAuth2\Server\Grant\RefreshTokenGrant(
73+
$module->refreshTokenRepository
74+
));
75+
$server->enableGrantType(new \chervand\yii2\oauth2\server\components\Grant\RevokeGrant(
76+
$module->refreshTokenRepository,
77+
$module->publicKey
78+
));
7379
},
7480
],
7581
// ...

components/Grant/RevokeGrant.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
/**
3+
*
4+
*/
5+
6+
namespace chervand\yii2\oauth2\server\components\Grant;
7+
8+
use Lcobucci\JWT\Parser;
9+
use Lcobucci\JWT\Signer\Rsa\Sha256;
10+
use League\OAuth2\Server\CryptKey;
11+
use League\OAuth2\Server\Exception\OAuthServerException;
12+
use League\OAuth2\Server\Grant\AbstractGrant;
13+
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
14+
use League\OAuth2\Server\RequestEvent;
15+
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
16+
use Psr\Http\Message\ServerRequestInterface;
17+
18+
class RevokeGrant extends AbstractGrant
19+
{
20+
/**
21+
* @var \League\OAuth2\Server\CryptKey
22+
*/
23+
protected $publicKey;
24+
25+
26+
/**
27+
* RevokeGrant constructor.
28+
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
29+
* @param CryptKey $publicKey
30+
*/
31+
public function __construct(RefreshTokenRepositoryInterface $refreshTokenRepository, CryptKey $publicKey)
32+
{
33+
$this->setRefreshTokenRepository($refreshTokenRepository);
34+
$this->setPublicKey($publicKey);
35+
}
36+
37+
public function setPublicKey(CryptKey $key)
38+
{
39+
$this->publicKey = $key;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function getIdentifier()
46+
{
47+
return 'revoke';
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function respondToAccessTokenRequest(
54+
ServerRequestInterface $request,
55+
ResponseTypeInterface $responseType,
56+
\DateInterval $accessTokenTTL
57+
)
58+
{
59+
throw new \LogicException('This grant does not used this method');
60+
}
61+
62+
/**
63+
* "Note: invalid tokens do not cause an error response since the client
64+
* cannot handle such an error in a reasonable way. Moreover, the
65+
* purpose of the revocation request, invalidating the particular token,
66+
* is already achieved."
67+
*
68+
* @see https://tools.ietf.org/html/rfc7009#section-2.2
69+
*
70+
* @param ServerRequestInterface $request
71+
* @param ResponseTypeInterface $response
72+
* @return mixed
73+
* @throws OAuthServerException
74+
*/
75+
public function respondToRevokeTokenRequest(
76+
ServerRequestInterface $request,
77+
ResponseTypeInterface $response
78+
)
79+
{
80+
$client = $this->validateClient($request);
81+
$this->invalidateToken($request, $client->getIdentifier());
82+
83+
return $response;
84+
}
85+
86+
/**
87+
* "If the server is unable to locate the token using
88+
* the given hint, it MUST extend its search across all of its
89+
* supported token types."
90+
*
91+
* @see https://tools.ietf.org/html/rfc7009#section-2.1
92+
*
93+
* @param ServerRequestInterface $request
94+
* @param $clientId
95+
*/
96+
protected function invalidateToken(ServerRequestInterface $request, $clientId)
97+
{
98+
$tokenTypeHint = $this->getRequestParameter('token_type_hint', $request);
99+
100+
$callStack = $tokenTypeHint == 'refresh_token'
101+
? ['invalidateRefreshToken', 'invalidateAccessToken']
102+
: ['invalidateAccessToken', 'invalidateRefreshToken'];
103+
104+
foreach ($callStack as $function) {
105+
if (call_user_func([$this, $function], $request, $clientId) === true) {
106+
break;
107+
}
108+
}
109+
}
110+
111+
/**
112+
* @param ServerRequestInterface $request
113+
* @param $clientId
114+
* @return bool
115+
*/
116+
protected function invalidateAccessToken(ServerRequestInterface $request, $clientId)
117+
{
118+
$accessToken = $this->getRequestParameter('token', $request);
119+
if (is_null($accessToken)) {
120+
throw OAuthServerException::invalidRequest('token');
121+
}
122+
123+
try {
124+
$token = (new Parser())->parse($accessToken);
125+
} catch (\Exception $exception) {
126+
return false;
127+
}
128+
129+
if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
130+
throw OAuthServerException::accessDenied('Access token could not be verified');
131+
}
132+
133+
if ($token->getClaim('aud') !== $clientId) {
134+
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
135+
throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
136+
}
137+
138+
$this->accessTokenRepository->revokeAccessToken($token->getClaim('jti'));
139+
140+
return true;
141+
}
142+
143+
/**
144+
* @param ServerRequestInterface $request
145+
* @param $clientId
146+
* @return bool
147+
*/
148+
protected function invalidateRefreshToken(ServerRequestInterface $request, $clientId)
149+
{
150+
$encryptedRefreshToken = $this->getRequestParameter('token', $request);
151+
if (is_null($encryptedRefreshToken)) {
152+
throw OAuthServerException::invalidRequest('token');
153+
}
154+
155+
try {
156+
$refreshToken = $this->decrypt($encryptedRefreshToken);
157+
} catch (\Exception $exception) {
158+
return false;
159+
}
160+
161+
$refreshTokenData = json_decode($refreshToken, true);
162+
if ($refreshTokenData['client_id'] !== $clientId) {
163+
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
164+
throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
165+
}
166+
167+
$this->accessTokenRepository->revokeAccessToken($refreshTokenData['access_token_id']);
168+
$this->refreshTokenRepository->revokeRefreshToken($refreshTokenData['refresh_token_id']);
169+
170+
return true;
171+
}
172+
}

components/Repositories/RefreshTokenRepository.php

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,32 +33,29 @@ public function getNewRefreshToken()
3333
*/
3434
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity)
3535
{
36-
if (
37-
$refreshTokenEntity instanceof RefreshToken
38-
&& $refreshTokenEntity->save()
39-
) {
40-
return $refreshTokenEntity;
36+
if ($refreshTokenEntity instanceof RefreshToken) {
37+
$refreshTokenEntity->setAttribute(
38+
'expired_at',
39+
$refreshTokenEntity->getExpiryDateTime()->getTimestamp()
40+
);
41+
if ($refreshTokenEntity->save()) {
42+
return $refreshTokenEntity;
43+
}
4144
}
4245

4346
throw OAuthServerException::serverError('Refresh token failure');
4447
}
4548

4649
/**
4750
* {@inheritdoc}
48-
*
49-
* @throws OAuthServerException
5051
*/
5152
public function revokeRefreshToken($tokenId)
5253
{
53-
$updated = RefreshToken::updateAll(
54+
RefreshToken::updateAll(
5455
['status' => RefreshToken::STATUS_REVOKED],
5556
'identifier=:identifier',
5657
[':identifier' => $tokenId]
5758
);
58-
59-
if ($updated < 1) {
60-
throw OAuthServerException::invalidRefreshToken('Cannot revoke the refresh token');
61-
}
6259
}
6360

6461
/**
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace chervand\yii2\oauth2\server\components\ResponseTypes;
4+
5+
use League\OAuth2\Server\ResponseTypes\AbstractResponseType;
6+
use Psr\Http\Message\ResponseInterface;
7+
8+
/**
9+
* Class RevokeResponse
10+
* @package chervand\yii2\oauth2\server\components
11+
* @link https://tools.ietf.org/html/rfc7009
12+
*/
13+
class RevokeResponse extends AbstractResponseType
14+
{
15+
/**
16+
* {@inheritdoc}
17+
*/
18+
public function generateHttpResponse(ResponseInterface $response)
19+
{
20+
$response = $response
21+
->withStatus(200)
22+
->withHeader('pragma', 'no-cache')
23+
->withHeader('cache-control', 'no-store');
24+
25+
return $response;
26+
}
27+
}

components/Server/AuthorizationServer.php

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
namespace chervand\yii2\oauth2\server\components\Server;
44

55
use chervand\yii2\oauth2\server\components\Events\AuthorizationEvent;
6+
use chervand\yii2\oauth2\server\components\Grant\RevokeGrant;
7+
use chervand\yii2\oauth2\server\components\ResponseTypes\RevokeResponse;
68
use League\OAuth2\Server\Exception\OAuthServerException;
9+
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
710
use Psr\Http\Message\ResponseInterface;
811
use Psr\Http\Message\ServerRequestInterface;
912
use yii\base\Event;
@@ -13,27 +16,55 @@ class AuthorizationServer extends \League\OAuth2\Server\AuthorizationServer
1316
/**
1417
* {@inheritdoc}
1518
*/
16-
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseInterface $response)
19+
public function respondToAccessTokenRequest(
20+
ServerRequestInterface $request,
21+
ResponseInterface $response
22+
)
1723
{
18-
try {
19-
20-
$response = parent::respondToAccessTokenRequest($request, $response);
21-
22-
if ($response instanceof ResponseInterface) {
23-
Event::trigger(
24-
$this,
25-
AuthorizationEvent::USER_AUTHENTICATION_SUCCEED,
26-
new AuthorizationEvent([
27-
'request' => $request,
28-
'response' => $response,
29-
])
30-
);
31-
}
24+
$response = parent::respondToAccessTokenRequest($request, $response);
25+
26+
if ($response instanceof ResponseInterface) {
27+
Event::trigger(
28+
$this,
29+
AuthorizationEvent::USER_AUTHENTICATION_SUCCEED,
30+
new AuthorizationEvent([
31+
'request' => $request,
32+
'response' => $response,
33+
])
34+
);
35+
}
3236

33-
return $response;
37+
return $response;
38+
}
39+
40+
/**
41+
* @param ServerRequestInterface $request
42+
* @param ResponseInterface $response
43+
* @return ResponseInterface
44+
* @throws OAuthServerException
45+
*/
46+
public function respondToRevokeTokenRequest(
47+
ServerRequestInterface $request,
48+
ResponseInterface $response
49+
)
50+
{
51+
if (
52+
array_key_exists('revoke', $this->enabledGrantTypes) === true
53+
&& $this->enabledGrantTypes['revoke'] instanceof RevokeGrant
54+
) {
55+
56+
$this->responseType = new RevokeResponse();
57+
58+
/** @var RevokeGrant $revokeGrant */
59+
$revokeGrant = $this->enabledGrantTypes['revoke'];
60+
$revokeResponse = $revokeGrant->respondToRevokeTokenRequest($request, $this->getResponseType());
61+
62+
if ($revokeResponse instanceof ResponseTypeInterface) {
63+
return $revokeResponse->generateHttpResponse($response);
64+
}
3465

35-
} catch (OAuthServerException $exception) {
36-
throw $exception;
3766
}
67+
68+
throw OAuthServerException::unsupportedGrantType();
3869
}
3970
}

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"require": {
3232
"php": ">=5.6.0",
3333
"yiisoft/yii2": "^2.0",
34-
"league/oauth2-server": "^6.0",
34+
"league/oauth2-server": "^6.1",
3535
"guzzlehttp/guzzle": "^6.3"
3636
},
3737
"autoload": {

0 commit comments

Comments
 (0)