Completed
Pull Request — master (#648)
by Lukáš
31:23
created

AuthCodeGrant::enableCodeChallengeVerifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
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 League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
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 CodeChallengeVerifierInterface[]
34
     */
35
    private $codeChallengeVerifiers = [];
36
37
    /**
38
     * @param AuthCodeRepositoryInterface     $authCodeRepository
39
     * @param 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
    /**
54
     * Enable a code challenge verifier on the grant.
55
     *
56
     * @param CodeChallengeVerifierInterface $codeChallengeVerifier
57
     */
58
    public function enableCodeChallengeVerifier(CodeChallengeVerifierInterface $codeChallengeVerifier)
59
    {
60
        $this->codeChallengeVerifiers[$codeChallengeVerifier->getMethod()] = $codeChallengeVerifier;
61
    }
62
63
    /**
64
     * Respond to an access token request.
65
     *
66
     * @param ServerRequestInterface $request
67
     * @param ResponseTypeInterface  $responseType
68
     * @param \DateInterval          $accessTokenTTL
69
     *
70
     * @throws OAuthServerException
71
     *
72
     * @return ResponseTypeInterface
73
     */
74
    public function respondToAccessTokenRequest(
75
        ServerRequestInterface $request,
76
        ResponseTypeInterface $responseType,
77
        \DateInterval $accessTokenTTL
78
    ) {
79
        // Validate request
80
        $client = $this->validateClient($request);
81
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
82
83
        if ($encryptedAuthCode === null) {
84
            throw OAuthServerException::invalidRequest('code');
85
        }
86
87
        // Validate the authorization code
88
        try {
89
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
90
            if (time() > $authCodePayload->expire_time) {
91
                throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
92
            }
93
94
            if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
95
                throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
96
            }
97
98
            if ($authCodePayload->client_id !== $client->getIdentifier()) {
99
                throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
100
            }
101
102
            // The redirect URI is required in this request
103
            $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
104
            if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
105
                throw OAuthServerException::invalidRequest('redirect_uri');
106
            }
107
108
            if ($authCodePayload->redirect_uri !== $redirectUri) {
109
                throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
110
            }
111
112
            $scopes = [];
113
            foreach ($authCodePayload->scopes as $scopeId) {
114
                $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
115
116
                if ($scope instanceof ScopeEntityInterface === false) {
117
                    // @codeCoverageIgnoreStart
118
                    throw OAuthServerException::invalidScope($scopeId);
119
                    // @codeCoverageIgnoreEnd
120
                }
121
122
                $scopes[] = $scope;
123
            }
124
125
            // Finalize the requested scopes
126
            $scopes = $this->scopeRepository->finalizeScopes(
127
                $scopes,
128
                $this->getIdentifier(),
129
                $client,
130
                $authCodePayload->user_id
131
            );
132
        } catch (\LogicException  $e) {
133
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
134
        }
135
136
        // Verify code challenge
137
        if (empty($this->codeChallengeVerifiers) === false) {
138
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
139
            if ($codeVerifier === null) {
140
                throw OAuthServerException::invalidRequest('code_verifier');
141
            }
142
143
            if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
144
                if ($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method]->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
145
                    throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
146
                }
147
            } else {
148
                // @codeCoverageIgnoreStart
149
                throw OAuthServerException::serverError(
150
                    sprintf(
151
                        'Unsupported code challenge method `%s`',
152
                        $authCodePayload->code_challenge_method
153
                    )
154
                );
155
                // @codeCoverageIgnoreEnd
156
            }
157
        }
158
159
        // Issue and persist access + refresh tokens
160
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
161
        $refreshToken = $this->issueRefreshToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 160 can be null; however, League\OAuth2\Server\Gra...nt::issueRefreshToken() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
162
163
        // Inject tokens into response type
164
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 160 can be null; however, League\OAuth2\Server\Res...rface::setAccessToken() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
165
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 161 can be null; however, League\OAuth2\Server\Res...face::setRefreshToken() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
166
167
        // Revoke used auth code
168
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
169
170
        return $responseType;
171
    }
172
173
    /**
174
     * Return the grant identifier that can be used in matching up requests.
175
     *
176
     * @return string
177
     */
178
    public function getIdentifier()
179
    {
180
        return 'authorization_code';
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
187
    {
188
        return (
189
            array_key_exists('response_type', $request->getQueryParams())
190
            && $request->getQueryParams()['response_type'] === 'code'
191
            && isset($request->getQueryParams()['client_id'])
192
        );
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function validateAuthorizationRequest(ServerRequestInterface $request)
199
    {
200
        $clientId = $this->getQueryStringParameter(
201
            'client_id',
202
            $request,
203
            $this->getServerParameter('PHP_AUTH_USER', $request)
204
        );
205
        if (is_null($clientId)) {
206
            throw OAuthServerException::invalidRequest('client_id');
207
        }
208
209
        $client = $this->clientRepository->getClientEntity(
210
            $clientId,
211
            $this->getIdentifier(),
212
            null,
213
            false
214
        );
215
216
        if ($client instanceof ClientEntityInterface === false) {
217
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
218
            throw OAuthServerException::invalidClient();
219
        }
220
221
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
222
        if ($redirectUri !== null) {
223
            if (
224
                is_string($client->getRedirectUri())
225
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
226
            ) {
227
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
228
                throw OAuthServerException::invalidClient();
229
            } elseif (
230
                is_array($client->getRedirectUri())
231
                && in_array($redirectUri, $client->getRedirectUri()) === false
232
            ) {
233
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
234
                throw OAuthServerException::invalidClient();
235
            }
236
        } elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
237
            || empty($client->getRedirectUri())
238
        ) {
239
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
240
            throw OAuthServerException::invalidClient();
241
        }
242
243
        $scopes = $this->validateScopes(
244
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
245
            is_array($client->getRedirectUri())
246
                ? $client->getRedirectUri()[0]
247
                : $client->getRedirectUri()
248
        );
249
250
        $stateParameter = $this->getQueryStringParameter('state', $request);
251
252
        $authorizationRequest = new AuthorizationRequest();
253
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
254
        $authorizationRequest->setClient($client);
255
        $authorizationRequest->setRedirectUri($redirectUri);
256
        $authorizationRequest->setState($stateParameter);
257
        $authorizationRequest->setScopes($scopes);
258
259
        if (empty($this->codeChallengeVerifiers) === false) {
260
            $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
261
            if ($codeChallenge === null) {
262
                throw OAuthServerException::invalidRequest('code_challenge');
263
            }
264
265
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
266
                throw OAuthServerException::invalidRequest(
267
                    'code_challenge',
268
                    'The code_challenge must be between 43 and 128 characters'
269
                );
270
            }
271
272
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
273
            if (array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
274
                throw OAuthServerException::invalidRequest(
275
                    'code_challenge_method',
276
                    'Code challenge method must be one of ' . implode(', ', array_map(
277
                        function ($method) { return '`' . $method . '`'; },
278
                        array_keys($this->codeChallengeVerifiers)
279
                    ))
280
                );
281
            }
282
283
            $authorizationRequest->setCodeChallenge($codeChallenge);
284
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
285
        }
286
287
        return $authorizationRequest;
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
294
    {
295
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
296
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
297
        }
298
299
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
300
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
301
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
302
                : $authorizationRequest->getClient()->getRedirectUri()
303
            : $authorizationRequest->getRedirectUri();
304
305
        // The user approved the client, redirect them back with an auth code
306
        if ($authorizationRequest->isAuthorizationApproved() === true) {
307
            $authCode = $this->issueAuthCode(
308
                $this->authCodeTTL,
309
                $authorizationRequest->getClient(),
310
                $authorizationRequest->getUser()->getIdentifier(),
311
                $authorizationRequest->getRedirectUri(),
312
                $authorizationRequest->getScopes()
313
            );
314
315
            $payload = [
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
            $response = new RedirectResponse();
327
            $response->setRedirectUri(
328
                $this->makeRedirectUri(
329
                    $finalRedirectUri,
330
                    [
331
                        'code'  => $this->encrypt(
332
                            json_encode(
333
                                $payload
334
                            )
335
                        ),
336
                        'state' => $authorizationRequest->getState(),
337
                    ]
338
                )
339
            );
340
341
            return $response;
342
        }
343
344
        // The user denied the client, redirect them back with an error
345
        throw OAuthServerException::accessDenied(
346
            'The user denied the request',
347
            $this->makeRedirectUri(
348
                $finalRedirectUri,
349
                [
350
                    'state' => $authorizationRequest->getState(),
351
                ]
352
            )
353
        );
354
    }
355
}
356