Completed
Push — master ( 4a4f4f...599c9a )
by Alex
31:44
created

AuthCodeGrant::canRespondToAuthorizationRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 3
eloc 5
nc 3
nop 1
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
        }
245
246
        $scopes = $this->validateScopes(
247
            $this->getQueryStringParameter('scope', $request),
248
            is_array($client->getRedirectUri())
249
                ? $client->getRedirectUri()[0]
250
                : $client->getRedirectUri()
251
        );
252
253
        $stateParameter = $this->getQueryStringParameter('state', $request);
254
255
        $authorizationRequest = new AuthorizationRequest();
256
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
257
        $authorizationRequest->setClient($client);
258
        $authorizationRequest->setRedirectUri($redirectUri);
259
        $authorizationRequest->setState($stateParameter);
260
        $authorizationRequest->setScopes($scopes);
261
262
        if ($this->enableCodeExchangeProof === true) {
263
            $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
264
            if ($codeChallenge === null) {
265
                throw OAuthServerException::invalidRequest('code_challenge');
266
            }
267
268
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
269
            if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) {
270
                throw OAuthServerException::invalidRequest(
271
                    'code_challenge_method',
272
                    'Code challenge method must be `plain` or `S256`'
273
                );
274
            }
275
276
            $authorizationRequest->setCodeChallenge($codeChallenge);
277
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
278
        }
279
280
        return $authorizationRequest;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
287
    {
288
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
289
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
290
        }
291
292
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
293
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
294
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
295
                : $authorizationRequest->getClient()->getRedirectUri()
296
            : $authorizationRequest->getRedirectUri();
297
298
        // The user approved the client, redirect them back with an auth code
299
        if ($authorizationRequest->isAuthorizationApproved() === true) {
300
            $authCode = $this->issueAuthCode(
301
                $this->authCodeTTL,
302
                $authorizationRequest->getClient(),
303
                $authorizationRequest->getUser()->getIdentifier(),
304
                $authorizationRequest->getRedirectUri(),
305
                $authorizationRequest->getScopes()
306
            );
307
308
            $response = new RedirectResponse();
309
            $response->setRedirectUri(
310
                $this->makeRedirectUri(
311
                    $finalRedirectUri,
312
                    [
313
                        'code'  => $this->encrypt(
314
                            json_encode(
315
                                [
316
                                    'client_id'               => $authCode->getClient()->getIdentifier(),
317
                                    'redirect_uri'            => $authCode->getRedirectUri(),
318
                                    'auth_code_id'            => $authCode->getIdentifier(),
319
                                    'scopes'                  => $authCode->getScopes(),
320
                                    'user_id'                 => $authCode->getUserIdentifier(),
321
                                    'expire_time'             => (new \DateTime())->add($this->authCodeTTL)->format('U'),
322
                                    'code_challenge'          => $authorizationRequest->getCodeChallenge(),
323
                                    'code_challenge_method  ' => $authorizationRequest->getCodeChallengeMethod(),
324
                                ]
325
                            )
326
                        ),
327
                        'state' => $authorizationRequest->getState(),
328
                    ]
329
                )
330
            );
331
332
            return $response;
333
        }
334
335
        // The user denied the client, redirect them back with an error
336
        throw OAuthServerException::accessDenied(
337
            'The user denied the request',
338
            $finalRedirectUri
339
        );
340
    }
341
}
342