Completed
Pull Request — master (#573)
by ismail
34:07
created

AuthCodeGrant::getIdentifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * @author      Alex Bilbie <[email protected]>
4
 * @copyright   Copyright (c) Alex Bilbie
5
 * @license     http://mit-license.org/
6
 *
7
 * @link        https://github.com/thephpleague/oauth2-server
8
 */
9
10
namespace League\OAuth2\Server\Grant;
11
12
use DateInterval;
13
use League\OAuth2\Server\Entities\ClientEntityInterface;
14
use League\OAuth2\Server\Entities\ScopeEntityInterface;
15
use League\OAuth2\Server\Entities\UserEntityInterface;
16
use League\OAuth2\Server\Exception\OAuthServerException;
17
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
18
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
19
use League\OAuth2\Server\RequestEvent;
20
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
21
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
22
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
23
use Psr\Http\Message\ServerRequestInterface;
24
25
class AuthCodeGrant extends AbstractAuthorizeGrant
26
{
27
    /**
28
     * @var \DateInterval
29
     */
30
    private $authCodeTTL;
31
32
    /**
33
     * @var bool
34
     */
35
    private $enableCodeExchangeProof = false;
36
37
    /**
38
     * @param \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface     $authCodeRepository
39
     * @param \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface $refreshTokenRepository
40
     * @param \DateInterval                                                      $authCodeTTL
41
     */
42
    public function __construct(
43
        AuthCodeRepositoryInterface $authCodeRepository,
44
        RefreshTokenRepositoryInterface $refreshTokenRepository,
45
        \DateInterval $authCodeTTL
46
    ) {
47
        $this->setAuthCodeRepository($authCodeRepository);
48
        $this->setRefreshTokenRepository($refreshTokenRepository);
49
        $this->authCodeTTL = $authCodeTTL;
50
        $this->refreshTokenTTL = new \DateInterval('P1M');
51
    }
52
53
    public function enableCodeExchangeProof()
54
    {
55
        $this->enableCodeExchangeProof = true;
56
    }
57
58
    /**
59
     * Respond to an access token request.
60
     *
61
     * @param \Psr\Http\Message\ServerRequestInterface                  $request
62
     * @param \League\OAuth2\Server\ResponseTypes\ResponseTypeInterface $responseType
63
     * @param \DateInterval                                             $accessTokenTTL
64
     *
65
     * @throws \League\OAuth2\Server\Exception\OAuthServerException
66
     *
67
     * @return \League\OAuth2\Server\ResponseTypes\ResponseTypeInterface
68
     */
69
    public function respondToAccessTokenRequest(
70
        ServerRequestInterface $request,
71
        ResponseTypeInterface $responseType,
72
        DateInterval $accessTokenTTL
73
    ) {
74
        // Validate request
75
        $client = $this->validateClient($request);
76
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
77
78
        if ($encryptedAuthCode === null) {
79
            throw OAuthServerException::invalidRequest('code');
80
        }
81
82
        // Validate the authorization code
83
        try {
84
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
85
            if (time() > $authCodePayload->expire_time) {
86
                throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
87
            }
88
89
            if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
90
                throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
91
            }
92
93
            if ($authCodePayload->client_id !== $client->getIdentifier()) {
94
                throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
95
            }
96
97
            // The redirect URI is required in this request
98
            $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
99
            if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
100
                throw OAuthServerException::invalidRequest('redirect_uri');
101
            }
102
103
            if ($authCodePayload->redirect_uri !== $redirectUri) {
104
                throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
105
            }
106
107
            $scopes = [];
108
            foreach ($authCodePayload->scopes as $scopeId) {
109
                $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
110
111
                if (!$scope instanceof ScopeEntityInterface) {
112
                    // @codeCoverageIgnoreStart
113
                    throw OAuthServerException::invalidScope($scopeId);
114
                    // @codeCoverageIgnoreEnd
115
                }
116
117
                $scopes[] = $scope;
118
            }
119
120
            // Finalize the requested scopes
121
            $scopes = $this->scopeRepository->finalizeScopes(
122
                $scopes,
123
                $this->getIdentifier(),
124
                $client,
125
                $authCodePayload->user_id
126
            );
127
        } catch (\LogicException  $e) {
128
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
129
        }
130
131
        // Validate code challenge
132
        if ($this->enableCodeExchangeProof === true) {
133
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
134
            if ($codeVerifier === null) {
135
                throw OAuthServerException::invalidRequest('code_verifier');
136
            }
137
138
            switch ($authCodePayload->code_challenge_method) {
139
                case 'plain':
140
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
141
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
142
                    }
143
144
                    break;
145
                case 'S256':
146
                    if (
147
                        hash_equals(
148
                            urlencode(base64_encode(hash('sha256', $codeVerifier))),
149
                            $authCodePayload->code_challenge
150
                        ) === false
151
                    ) {
152
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
153
                    }
154
                    // @codeCoverageIgnoreStart
155
                    break;
156
                default:
157
                    throw OAuthServerException::serverError(
158
                        sprintf(
159
                            'Unsupported code challenge method `%s`',
160
                            $authCodePayload->code_challenge_method
161
                        )
162
                    );
163
                // @codeCoverageIgnoreEnd
164
            }
165
        }
166
167
        // Issue and persist access + refresh tokens
168
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
169
        $refreshToken = $this->issueRefreshToken($accessToken);
170
171
        // Inject tokens into response type
172
        $responseType->setAccessToken($accessToken);
173
        $responseType->setRefreshToken($refreshToken);
174
175
        // Revoke used auth code
176
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
177
178
        return $responseType;
179
    }
180
181
    /**
182
     * Return the grant identifier that can be used in matching up requests.
183
     *
184
     * @return string
185
     */
186
    public function getIdentifier()
187
    {
188
        return 'authorization_code';
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
195
    {
196
        return (
197
            array_key_exists('response_type', $request->getQueryParams())
198
            && $request->getQueryParams()['response_type'] === 'code'
199
            && isset($request->getQueryParams()['client_id'])
200
        );
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function validateAuthorizationRequest(ServerRequestInterface $request)
207
    {
208
        $clientId = $this->getQueryStringParameter(
209
            'client_id',
210
            $request,
211
            $this->getServerParameter('PHP_AUTH_USER', $request)
212
        );
213
        if (is_null($clientId)) {
214
            throw OAuthServerException::invalidRequest('client_id');
215
        }
216
217
        $client = $this->clientRepository->getClientEntity(
218
            $clientId,
219
            $this->getIdentifier(),
220
            null,
221
            false
222
        );
223
224
        if ($client instanceof ClientEntityInterface === false) {
225
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
226
            throw OAuthServerException::invalidClient();
227
        }
228
229
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
230
        if ($redirectUri !== null) {
231
            if (
232
                is_string($client->getRedirectUri())
233
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
234
            ) {
235
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
236
                throw OAuthServerException::invalidClient();
237
            } elseif (
238
                is_array($client->getRedirectUri())
239
                && in_array($redirectUri, $client->getRedirectUri()) === false
240
            ) {
241
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
242
                throw OAuthServerException::invalidClient();
243
            }
244
        } elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
245
            || empty($client->getRedirectUri())
246
        ) {
247
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
248
            throw OAuthServerException::invalidClient();
249
        }
250
251
        $scopes = $this->validateScopes(
252
            $this->getQueryStringParameter('scope', $request),
253
            is_array($client->getRedirectUri())
254
                ? $client->getRedirectUri()[0]
255
                : $client->getRedirectUri()
256
        );
257
258
        $stateParameter = $this->getQueryStringParameter('state', $request);
259
260
        $authorizationRequest = new AuthorizationRequest();
261
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
262
        $authorizationRequest->setClient($client);
263
        $authorizationRequest->setRedirectUri($redirectUri);
264
        $authorizationRequest->setState($stateParameter);
265
        $authorizationRequest->setScopes($scopes);
266
267
        if ($this->enableCodeExchangeProof === true) {
268
            $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
269
            if ($codeChallenge === null) {
270
                throw OAuthServerException::invalidRequest('code_challenge');
271
            }
272
273
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
274
            if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) {
275
                throw OAuthServerException::invalidRequest(
276
                    'code_challenge_method',
277
                    'Code challenge method must be `plain` or `S256`'
278
                );
279
            }
280
281
            $authorizationRequest->setCodeChallenge($codeChallenge);
282
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
283
        }
284
285
        return $authorizationRequest;
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
292
    {
293
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
294
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
295
        }
296
297
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
298
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
299
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
300
                : $authorizationRequest->getClient()->getRedirectUri()
301
            : $authorizationRequest->getRedirectUri();
302
303
        // The user approved the client, redirect them back with an auth code
304
        if ($authorizationRequest->isAuthorizationApproved() === true) {
305
            $authCode = $this->issueAuthCode(
306
                $this->authCodeTTL,
307
                $authorizationRequest->getClient(),
308
                $authorizationRequest->getUser()->getIdentifier(),
309
                $authorizationRequest->getRedirectUri(),
310
                $authorizationRequest->getScopes()
311
            );
312
313
            $response = new RedirectResponse();
314
            $response->setRedirectUri(
315
                $this->makeRedirectUri(
316
                    $finalRedirectUri,
317
                    [
318
                        'code'  => $this->encrypt(
319
                            json_encode(
320
                                [
321
                                    'client_id'               => $authCode->getClient()->getIdentifier(),
322
                                    'redirect_uri'            => $authCode->getRedirectUri(),
323
                                    'auth_code_id'            => $authCode->getIdentifier(),
324
                                    'scopes'                  => $authCode->getScopes(),
325
                                    'user_id'                 => $authCode->getUserIdentifier(),
326
                                    'expire_time'             => (new \DateTime())->add($this->authCodeTTL)->format('U'),
327
                                    'code_challenge'          => $authorizationRequest->getCodeChallenge(),
328
                                    'code_challenge_method  ' => $authorizationRequest->getCodeChallengeMethod(),
329
                                ]
330
                            )
331
                        ),
332
                        'state' => $authorizationRequest->getState(),
333
                    ]
334
                )
335
            );
336
337
            return $response;
338
        }
339
340
        // The user denied the client, redirect them back with an error
341
        throw OAuthServerException::accessDenied(
342
            'The user denied the request',
343
            $finalRedirectUri
344
        );
345
    }
346
}
347