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

AuthCodeGrant::enableCodeExchangeProof()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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