Completed
Push — master ( 683c35...fec6f1 )
by Alexandre
02:03
created

TokenRevocationEndpoint::getStorages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: Alexandre
5
 * Date: 10/06/2018
6
 * Time: 17:53
7
 */
8
9
namespace OAuth2\Endpoints;
10
11
12
use GuzzleHttp\Psr7\Response;
13
use OAuth2\ClientAuthentication\ClientAuthenticationMethodManager;
14
use OAuth2\Credentials\AccessTokenInterface;
15
use OAuth2\Credentials\RefreshTokenInterface;
16
use OAuth2\Credentials\TokenInterface;
17
use OAuth2\Exceptions\OAuthException;
18
use OAuth2\Storages\StorageManager;
19
use OAuth2\Storages\TokenStorageInterface;
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
23
24
/**
25
 * Class TokenRevocationEndpoint
26
 * @package OAuth2\Endpoints
27
 *
28
 * @see https://tools.ietf.org/html/rfc7009#section-2
29
 * Implementations MUST support the revocation of refresh tokens and
30
 * SHOULD support the revocation of access tokens (see Implementation
31
 * Note).
32
 *
33
 * The client requests the revocation of a particular token by making an
34
 * HTTP POST request to the token revocation endpoint URL.  This URL
35
 * MUST conform to the rules given in [RFC6749], Section 3.1.  Clients
36
 * MUST verify that the URL is an HTTPS URL.
37
 *
38
 * The means to obtain the location of the revocation endpoint is out of
39
 * the scope of this specification.  For example, the client developer
40
 * may consult the server's documentation or automatic discovery may be
41
 * used.  As this endpoint is handling security credentials, the
42
 * endpoint location needs to be obtained from a trustworthy source.
43
 *
44
 * Since requests to the token revocation endpoint result in the
45
 * transmission of plaintext credentials in the HTTP request, URLs for
46
 * token revocation endpoints MUST be HTTPS URLs.  The authorization
47
 * server MUST use Transport Layer Security (TLS) [RFC5246] in a version
48
 * compliant with [RFC6749], Section 1.6.  Implementations MAY also
49
 * support additional transport-layer security mechanisms that meet
50
 * their security requirements.
51
 *
52
 * If the host of the token revocation endpoint can also be reached over
53
 * HTTP, then the server SHOULD also offer a revocation service at the
54
 * corresponding HTTP URI, but it MUST NOT publish this URI as a token
55
 * revocation endpoint.  This ensures that tokens accidentally sent over
56
 * HTTP will be revoked.
57
 */
58
class TokenRevocationEndpoint implements EndpointInterface
59
{
60
61
    /**
62
     * @var ClientAuthenticationMethodManager
63
     */
64
    private $clientAuthenticationMethodManager;
65
    /**
66
     * @var StorageManager
67
     */
68
    private $storageManager;
69
70
    /**
71
     * TokenRevocationEndpoint constructor.
72
     * @param ClientAuthenticationMethodManager $clientAuthenticationMethodManager
73
     * @param StorageManager $storageManager
74
     */
75
    public function __construct(ClientAuthenticationMethodManager $clientAuthenticationMethodManager,
76
                                StorageManager $storageManager)
77
    {
78
        $this->clientAuthenticationMethodManager = $clientAuthenticationMethodManager;
79
        $this->storageManager = $storageManager;
80
    }
81
82
    /**
83
     * @param ServerRequestInterface $request
84
     * @return ResponseInterface
85
     *
86
     * @see https://tools.ietf.org/html/rfc7009#section-2.1
87
     * The client constructs the request by including the following
88
     * parameters using the "application/x-www-form-urlencoded" format in
89
     * the HTTP request entity-body:
90
     *
91
     * token   REQUIRED.  The token that the client wants to get revoked.
92
     *
93
     * token_type_hint  OPTIONAL.  A hint about the type of the token
94
     * submitted for revocation.  Clients MAY pass this parameter in
95
     * order to help the authorization server to optimize the token
96
     * lookup.  If the server is unable to locate the token using
97
     * the given hint, it MUST extend its search across all of its
98
     * supported token types.  An authorization server MAY ignore
99
     * this parameter, particularly if it is able to detect the
100
     * token type automatically.  This specification defines two
101
     * such values:
102
     * access_token: An access token as defined in [RFC6749],
103
     * Section 1.4
104
     * refresh_token: A refresh token as defined in [RFC6749],
105
     * Section 1.5
106
     *
107
     * Specific implementations, profiles, and extensions of this
108
     * specification MAY define other values for this parameter
109
     * using the registry defined in Section 4.1.2.
110
     *
111
     * The client also includes its authentication credentials as described
112
     * in Section 2.3. of [RFC6749].
113
     *
114
     * For example, a client may request the revocation of a refresh token
115
     * with the following request:
116
     *
117
     * POST /revoke HTTP/1.1
118
     * Host: server.example.com
119
     * Content-Type: application/x-www-form-urlencoded
120
     * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
121
     *
122
     * token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
123
     */
124
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
125
    {
126
        if ($request->getMethod() !== 'POST') {
127
            return new Response(405);
128
        }
129
130
        $requestData = $request->getParsedBody();
131
        $token = $requestData['token'] ?? null;
132
        $tokenTypeHint = $requestData['token_type_hint'] ?? null;
133
134
        try {
135
            /**
136
             * @see https://tools.ietf.org/html/rfc7009#section-2.1
137
             * The authorization server first validates the client credentials (in
138
             * case of a confidential client) and then verifies whether the token
139
             * was issued to the client making the revocation request.  If this
140
             * validation fails, the request is refused and the client is informed
141
             * of the error by the authorization server as described below.
142
             */
143
            $client = $this->clientAuthenticationMethodManager->authenticate($request, $requestData);
144
145
            if (!$token) {
146
                throw new OAuthException('invalid_request',
147
                    'The token that the client wants to get revoked is required.',
148
                    'https://tools.ietf.org/html/rfc7009#section-2.1');
149
            }
150
151
            $token = $this->findToken($token, $tokenTypeHint);
152
153
            if ($token->getClientIdentifier() !== $client->getIdentifier()) {
154
                throw new OAuthException('unauthorized_client',
155
                    'The token that the client wants to get revoked was not issued to it.',
156
                    'https://tools.ietf.org/html/rfc7009#section-2.1');
157
            }
158
159
            /**
160
             * @see https://tools.ietf.org/html/rfc7009#section-2.1
161
             * In the next step, the authorization server invalidates the token.
162
             * The invalidation takes place immediately, and the token cannot be
163
             * used again after the revocation.  In practice, there could be a
164
             * propagation delay, for example, in which some servers know about the
165
             * invalidation while others do not.  Implementations should minimize
166
             * that window, and clients must not try to use the token after receipt
167
             * of an HTTP 200 response from the server.
168
             */
169
            $this->revokeToken($token);
170
        } catch (OAuthException $e) {
171
            $status = 400;
172
            $headers = ['Content-Type' => 'application/json'];
173
            if ($e->getError() === 'invalid_client') {
174
                $status = 401;
175
                if ($request->hasHeader('Authorization')) {
176
                    $headers['WWW-Authenticate'] = 'Basic';
177
                }
178
            }
179
            return new Response($status, $headers, $e->jsonSerialize());
180
        }
181
182
        /**
183
         * @see https://tools.ietf.org/html/rfc7009#section-2.2
184
         * The authorization server responds with HTTP status code 200 if the
185
         * token has been revoked successfully or if the client submitted an
186
         * invalid token.
187
         *
188
         * Note: invalid tokens do not cause an error response since the client
189
         * cannot handle such an error in a reasonable way.  Moreover, the
190
         * purpose of the revocation request, invalidating the particular token,
191
         * is already achieved.
192
         *
193
         * The content of the response body is ignored by the client as all
194
         * necessary information is conveyed in the response code.
195
         *
196
         * An invalid token type hint value is ignored by the authorization
197
         * server and does not influence the revocation response.
198
         */
199
        return new Response(200);
200
    }
201
202
    /**
203
     * @return TokenStorageInterface[]
204
     */
205
    protected function getStorages(): array
206
    {
207
        return [
208
            'access_token' => $this->storageManager->getAccessTokenStorage(),
209
            'refresh_token' => $this->storageManager->getRefreshTokenStorage()
210
        ];
211
    }
212
213
    protected function findToken(string $token, ?string $tokenTypeHint = null): ?TokenInterface
214
    {
215
        $storages = $this->getStorages();
216
217
        if ($tokenTypeHint && isset($storages[$tokenTypeHint])) {
218
            $storage = $storages[$tokenTypeHint];
219
            unset($storages[$tokenTypeHint]);
220
            if ($tokenFound = $storage->get($token)) {
221
                return $tokenFound;
222
            }
223
        }
224
225
        foreach ($storages as $storage) {
226
            if ($tokenFound = $storage->get($token)) {
227
                return $tokenFound;
228
            }
229
        }
230
231
        return null;
232
    }
233
234
    /**
235
     * @param TokenInterface $token
236
     *
237
     * @see https://tools.ietf.org/html/rfc7009#section-2.1
238
     * Depending on the authorization server's revocation policy, the
239
     * revocation of a particular token may cause the revocation of related
240
     * tokens and the underlying authorization grant.  If the particular
241
     * token is a refresh token and the authorization server supports the
242
     * revocation of access tokens, then the authorization server SHOULD
243
     * also invalidate all access tokens based on the same authorization
244
     * grant (see Implementation Note).  If the token passed to the request
245
     * is an access token, the server MAY revoke the respective refresh
246
     * token as well.
247
     */
248
    protected function revokeToken(TokenInterface $token)
249
    {
250
        $accessTokenStorage = $this->storageManager->getAccessTokenStorage();
251
        $refreshTokenStorage = $this->storageManager->getRefreshTokenStorage();
252
253
        if ($token instanceof AccessTokenInterface) {
254
            $accessTokenStorage->revoke($token);
255
            if ($refreshToken = $token->getRefreshToken()) {
256
                if ($refreshToken = $refreshTokenStorage->get($refreshToken)) {
257
                    $refreshTokenStorage->revoke($refreshToken);
258
                }
259
            }
260
        } else if ($token instanceof RefreshTokenInterface) {
261
            $refreshTokenStorage->revoke($token);
262
            foreach ($this->storageManager->getAccessTokenStorage()->getByRefreshToken($token) as $accessToken) {
263
                $accessTokenStorage->revoke($accessToken);
264
            }
265
        }
266
    }
267
}