Completed
Pull Request — master (#648)
by Lukáš
41:28 queued 06:19
created

AuthCodeGrant   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 315
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 14

Importance

Changes 21
Bugs 8 Features 2
Metric Value
wmc 38
c 21
b 8
f 2
lcom 2
cbo 14
dl 0
loc 315
rs 8.3999

7 Methods

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