Completed
Pull Request — master (#648)
by Lukáš
39:52
created

AuthCodeGrant   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 317
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 317
rs 8.3999

7 Methods

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