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

AuthCodeGrant::enableCodeChallengeVerifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
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
            $codeChallengeVerified = false;
144
            foreach ($this->codeChallengeVerifiers as $method => $verifier) {
145
                if ($method === $authCodePayload->code_challenge_method) {
146
                    if ($verifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
147
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
148
                    }
149
                    $codeChallengeVerified = true;
150
                    break;
151
                }
152
            }
153
154
            if ($codeChallengeVerified === false) {
155
                throw OAuthServerException::serverError(
156
                    sprintf(
157
                        'Unsupported code challenge method `%s`',
158
                        $authCodePayload->code_challenge_method
159
                    )
160
                );
161
            }
162
        }
163
164
        // Issue and persist access + refresh tokens
165
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
166
        $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 165 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...
167
168
        // Inject tokens into response type
169
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 165 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...
170
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 166 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...
171
172
        // Revoke used auth code
173
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
174
175
        return $responseType;
176
    }
177
178
    /**
179
     * Return the grant identifier that can be used in matching up requests.
180
     *
181
     * @return string
182
     */
183
    public function getIdentifier()
184
    {
185
        return 'authorization_code';
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
192
    {
193
        return (
194
            array_key_exists('response_type', $request->getQueryParams())
195
            && $request->getQueryParams()['response_type'] === 'code'
196
            && isset($request->getQueryParams()['client_id'])
197
        );
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function validateAuthorizationRequest(ServerRequestInterface $request)
204
    {
205
        $clientId = $this->getQueryStringParameter(
206
            'client_id',
207
            $request,
208
            $this->getServerParameter('PHP_AUTH_USER', $request)
209
        );
210
        if (is_null($clientId)) {
211
            throw OAuthServerException::invalidRequest('client_id');
212
        }
213
214
        $client = $this->clientRepository->getClientEntity(
215
            $clientId,
216
            $this->getIdentifier(),
217
            null,
218
            false
219
        );
220
221
        if ($client instanceof ClientEntityInterface === false) {
222
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
223
            throw OAuthServerException::invalidClient();
224
        }
225
226
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
227
        if ($redirectUri !== null) {
228
            if (
229
                is_string($client->getRedirectUri())
230
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
231
            ) {
232
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
233
                throw OAuthServerException::invalidClient();
234
            } elseif (
235
                is_array($client->getRedirectUri())
236
                && in_array($redirectUri, $client->getRedirectUri()) === false
237
            ) {
238
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
239
                throw OAuthServerException::invalidClient();
240
            }
241
        }
242
243
        $scopes = $this->validateScopes(
244
            $this->getQueryStringParameter('scope', $request),
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
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
266
            $enabledCodeChallengeMethods = array_keys($this->codeChallengeVerifiers);
267
            if (in_array($codeChallengeMethod, $enabledCodeChallengeMethods) === false) {
268
                throw OAuthServerException::invalidRequest(
269
                    'code_challenge_method',
270
                    'Code challenge method must be one of ' . implode(', ', array_map(function ($method) { return '`' . $method . '`'; }, $enabledCodeChallengeMethods))
271
                );
272
            }
273
274
            $authorizationRequest->setCodeChallenge($codeChallenge);
275
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
276
        }
277
278
        return $authorizationRequest;
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
285
    {
286
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
287
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
288
        }
289
290
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
291
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
292
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
293
                : $authorizationRequest->getClient()->getRedirectUri()
294
            : $authorizationRequest->getRedirectUri();
295
296
        // The user approved the client, redirect them back with an auth code
297
        if ($authorizationRequest->isAuthorizationApproved() === true) {
298
            $authCode = $this->issueAuthCode(
299
                $this->authCodeTTL,
300
                $authorizationRequest->getClient(),
301
                $authorizationRequest->getUser()->getIdentifier(),
302
                $authorizationRequest->getRedirectUri(),
303
                $authorizationRequest->getScopes()
304
            );
305
306
            $response = new RedirectResponse();
307
            $response->setRedirectUri(
308
                $this->makeRedirectUri(
309
                    $finalRedirectUri,
310
                    [
311
                        'code'  => $this->encrypt(
312
                            json_encode(
313
                                [
314
                                    'client_id'               => $authCode->getClient()->getIdentifier(),
315
                                    'redirect_uri'            => $authCode->getRedirectUri(),
316
                                    'auth_code_id'            => $authCode->getIdentifier(),
317
                                    'scopes'                  => $authCode->getScopes(),
318
                                    'user_id'                 => $authCode->getUserIdentifier(),
319
                                    'expire_time'             => (new \DateTime())->add($this->authCodeTTL)->format('U'),
320
                                    'code_challenge'          => $authorizationRequest->getCodeChallenge(),
321
                                    'code_challenge_method  ' => $authorizationRequest->getCodeChallengeMethod(),
322
                                ]
323
                            )
324
                        ),
325
                        'state' => $authorizationRequest->getState(),
326
                    ]
327
                )
328
            );
329
330
            return $response;
331
        }
332
333
        // The user denied the client, redirect them back with an error
334
        throw OAuthServerException::accessDenied(
335
            'The user denied the request',
336
            $this->makeRedirectUri(
337
                $finalRedirectUri,
338
                [
339
                    'state' => $authorizationRequest->getState(),
340
                ]
341
            )
342
        );
343
    }
344
}
345