Passed
Pull Request — master (#1470)
by
unknown
33:53
created

AbstractGrant::setClientRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * OAuth 2.0 Abstract grant.
5
 *
6
 * @author      Alex Bilbie <[email protected]>
7
 * @copyright   Copyright (c) Alex Bilbie
8
 * @license     http://mit-license.org/
9
 *
10
 * @link        https://github.com/thephpleague/oauth2-server
11
 */
12
13
declare(strict_types=1);
14
15
namespace League\OAuth2\Server\Grant;
16
17
use DateInterval;
18
use DateTimeImmutable;
19
use DomainException;
20
use Error;
21
use Exception;
22
use League\OAuth2\Server\CryptKeyInterface;
23
use League\OAuth2\Server\CryptTrait;
0 ignored issues
show
Bug introduced by
The type League\OAuth2\Server\CryptTrait was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
25
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
26
use League\OAuth2\Server\Entities\ClientEntityInterface;
27
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
28
use League\OAuth2\Server\Entities\ScopeEntityInterface;
29
use League\OAuth2\Server\Entities\UserEntityInterface;
30
use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill;
31
use League\OAuth2\Server\Exception\OAuthServerException;
32
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
33
use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
34
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
35
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
36
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
37
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
38
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
39
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
40
use League\OAuth2\Server\RequestEvent;
41
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;
42
use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse;
43
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
44
use LogicException;
45
use Psr\Http\Message\ServerRequestInterface;
46
use TypeError;
47
48
use function array_filter;
49
use function array_key_exists;
50
use function base64_decode;
51
use function bin2hex;
52
use function explode;
53
use function is_string;
54
use function random_bytes;
55
use function substr;
56
use function trim;
57
58
/**
59
 * Abstract grant class.
60
 */
61
abstract class AbstractGrant implements GrantTypeInterface
62
{
63
    use EmitterAwarePolyfill;
64
65
    protected const SCOPE_DELIMITER_STRING = ' ';
66
67
    protected const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10;
68
69
    protected ClientRepositoryInterface $clientRepository;
70
71
    protected AccessTokenRepositoryInterface $accessTokenRepository;
72
73
    protected ScopeRepositoryInterface $scopeRepository;
74
75
    protected AuthCodeRepositoryInterface $authCodeRepository;
76
77
    protected RefreshTokenRepositoryInterface $refreshTokenRepository;
78
79
    protected UserRepositoryInterface $userRepository;
80
81
    protected DateInterval $refreshTokenTTL;
82
83
    protected string $defaultScope;
84
85
    protected bool $revokeRefreshTokens = true;
86
87
    public function setClientRepository(ClientRepositoryInterface $clientRepository): void
88
    {
89 103
        $this->clientRepository = $clientRepository;
90
    }
91 103
92
    public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void
93
    {
94 55
        $this->accessTokenRepository = $accessTokenRepository;
95
    }
96 55
97
    public function setScopeRepository(ScopeRepositoryInterface $scopeRepository): void
98
    {
99 69
        $this->scopeRepository = $scopeRepository;
100
    }
101 69
102
    public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void
103
    {
104 93
        $this->refreshTokenRepository = $refreshTokenRepository;
105
    }
106 93
107
    public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository): void
108
    {
109 54
        $this->authCodeRepository = $authCodeRepository;
110
    }
111 54
112
    public function setUserRepository(UserRepositoryInterface $userRepository): void
113
    {
114 6
        $this->userRepository = $userRepository;
115
    }
116 6
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function setRefreshTokenTTL(DateInterval $refreshTokenTTL): void
121
    {
122 2
        $this->refreshTokenTTL = $refreshTokenTTL;
123
    }
124 2
125
    public function setDefaultScope(string $scope): void
126
    {
127
        $this->defaultScope = $scope;
128
    }
129
130 47
    public function revokeRefreshTokens(bool $willRevoke): void
131
    {
132 47
        $this->revokeRefreshTokens = $willRevoke;
133
    }
134
135 38
    /**
136
     * Validate the client.
137 38
     *
138
     * @throws OAuthServerException
139
     */
140 12
    protected function validateClient(ServerRequestInterface $request): ClientEntityInterface
141
    {
142 12
        [$clientId, $clientSecret] = $this->getClientCredentials($request);
143
144
        if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
145
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
146
147
            throw OAuthServerException::invalidClient($request);
148
        }
149
        $client = $this->getClientEntityOrFail($clientId, $request);
150 59
151
        // If a redirect URI is provided ensure it matches what is pre-registered
152 59
        $redirectUri = $this->getRequestParameter('redirect_uri', $request);
153
154 56
        if ($redirectUri !== null) {
155 11
            $this->validateRedirectUri($redirectUri, $client, $request);
156
        }
157 11
158
        return $client;
159 45
    }
160
161
    /**
162 45
     * Wrapper around ClientRepository::getClientEntity() that ensures we emit
163
     * an event and throw an exception if the repo doesn't return a client
164 45
     * entity.
165 17
     *
166
     * This is a bit of defensive coding because the interface contract
167
     * doesn't actually enforce non-null returns/exception-on-no-client so
168 45
     * getClientEntity might return null. By contrast, this method will
169
     * always either return a ClientEntityInterface or throw.
170
     *
171
     * @throws OAuthServerException
172
     */
173
    protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface
174
    {
175
        $client = $this->clientRepository->getClientEntity($clientId);
176
177
        if ($client instanceof ClientEntityInterface === false) {
178
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
179
            throw OAuthServerException::invalidClient($request);
180
        }
181
182
        return $client;
183 82
    }
184
185 82
    /**
186
     * Gets the client credentials from the request from the request body or
187 82
     * the Http Basic Authorization header
188 4
     *
189 4
     * @return array{0:non-empty-string,1:string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{0:non-empty-string,1:string} at position 4 could not be parsed: Unknown type name 'non-empty-string' at position 4 in array{0:non-empty-string,1:string}.
Loading history...
190
     *
191
     * @throws OAuthServerException
192 78
     */
193
    protected function getClientCredentials(ServerRequestInterface $request): array
194
    {
195
        [$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request);
196
197
        $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
198
199
        if ($clientId === null) {
200
            throw OAuthServerException::invalidRequest('client_id');
201
        }
202
203 70
        $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
204
205 70
        return [$clientId, $clientSecret ?? ''];
206
    }
207 70
208
    /**
209 70
     * Validate redirectUri from the request. If a redirect URI is provided
210 3
     * ensure it matches what is pre-registered
211
     *
212
     * @throws OAuthServerException
213 67
     */
214
    protected function validateRedirectUri(
215 66
        string $redirectUri,
216
        ClientEntityInterface $client,
217
        ServerRequestInterface $request
218
    ): void {
219
        $validator = new RedirectUriValidator($client->getRedirectUri());
220
221
        if (!$validator->validateRedirectUri($redirectUri)) {
222
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
223
            throw OAuthServerException::invalidClient($request);
224 34
        }
225
    }
226
227
    /**
228
     * Validate scopes in the request.
229 34
     *
230
     * @param null|string|string[] $scopes
231 34
     *
232 4
     * @throws OAuthServerException
233 4
     *
234
     * @return ScopeEntityInterface[]
235
     */
236
    public function validateScopes(string|array|null $scopes, ?string $redirectUri = null): array
237
    {
238
        if ($scopes === null) {
0 ignored issues
show
introduced by
The condition $scopes === null is always false.
Loading history...
239
            $scopes = [];
240
        } elseif (is_string($scopes)) {
0 ignored issues
show
introduced by
The condition is_string($scopes) is always false.
Loading history...
241
            $scopes = $this->convertScopesQueryStringToArray($scopes);
242
        }
243
244
        $validScopes = [];
245
246 52
        foreach ($scopes as $scopeItem) {
247
            $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
248 52
249
            if ($scope instanceof ScopeEntityInterface === false) {
250 52
                throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
251 36
            }
252
253
            $validScopes[$scopeItem] = $scope;
254 52
        }
255
256 52
        return array_values($validScopes);
257 52
    }
258
259 52
    /**
260 7
     * Converts a scopes query string to an array to easily iterate for validation.
261
     *
262
     * @return string[]
263 45
     */
264
    private function convertScopesQueryStringToArray(string $scopes): array
265
    {
266 45
        return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), static fn ($scope) => $scope !== '');
267
    }
268
269
    /**
270
     * Parse request parameter.
271
     *
272
     * @param array<array-key, mixed> $request
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, mixed> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, mixed>.
Loading history...
273
     *
274 36
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
275
     *
276 36
     * @throws OAuthServerException
277
     */
278
    private static function parseParam(string $parameter, array $request, ?string $default = null): ?string
279
    {
280
        $value = $request[$parameter] ?? '';
281
282
        if (is_scalar($value)) {
283
            $value = trim((string) $value);
284
        } else {
285
            throw OAuthServerException::invalidRequest($parameter);
286
        }
287
288 103
        if ($value === '') {
289
            $value = $default === null ? null : trim($default);
290 103
291
            if ($value === '') {
292 103
                $value = null;
293 103
            }
294
        }
295 2
296
        return $value;
297
    }
298 103
299 97
    /**
300
     * Retrieve request parameter.
301 97
     *
302
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
303
     *
304
     * @throws OAuthServerException
305
     */
306 103
    protected function getRequestParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
307
    {
308
        return self::parseParam($parameter, (array) $request->getParsedBody(), $default);
309
    }
310
311
    /**
312
     * Retrieve HTTP Basic Auth credentials with the Authorization header
313
     * of a request. First index of the returned array is the username,
314
     * second is the password (so list() will work). If the header does
315
     * not exist, or is otherwise an invalid HTTP Basic header, return
316 78
     * [null, null].
317
     *
318 78
     * @return array{0:non-empty-string,1:string}|array{0:null,1:null}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{0:non-empty-string...g}|array{0:null,1:null} at position 4 could not be parsed: Unknown type name 'non-empty-string' at position 4 in array{0:non-empty-string,1:string}|array{0:null,1:null}.
Loading history...
319
     */
320
    protected function getBasicAuthCredentials(ServerRequestInterface $request): array
321
    {
322
        if (!$request->hasHeader('Authorization')) {
323
            return [null, null];
324
        }
325
326
        $header = $request->getHeader('Authorization')[0];
327
        if (stripos($header, 'Basic ') !== 0) {
328
            return [null, null];
329
        }
330 76
331
        $decoded = base64_decode(substr($header, 6), true);
332 76
333 66
        if ($decoded === false) {
334
            return [null, null];
335
        }
336 10
337 10
        if (str_contains($decoded, ':') === false) {
338 3
            return [null, null]; // HTTP Basic header without colon isn't valid
339
        }
340
341 7
        [$username, $password] = explode(':', $decoded, 2);
342
343 7
        if ($username === '') {
344 1
            return [null, null];
345
        }
346
347 6
        return [$username, $password];
348 2
    }
349
350
    /**
351 4
     * Retrieve query string parameter.
352
     *
353 4
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
354
     *
355
     * @throws OAuthServerException
356
     */
357 4
    protected function getQueryStringParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
358
    {
359
        return self::parseParam($parameter, $request->getQueryParams(), $default);
360
    }
361
362
    /**
363
     * Retrieve cookie parameter.
364
     *
365
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
366
     *
367 24
     * @throws OAuthServerException
368
     */
369 24
    protected function getCookieParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
370
    {
371
        return self::parseParam($parameter, $request->getCookieParams(), $default);
372
    }
373
374
    /**
375
     * Retrieve server parameter.
376
     *
377
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
378
     *
379 1
     * @throws OAuthServerException
380
     */
381 1
    protected function getServerParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
382
    {
383
        return self::parseParam($parameter, $request->getServerParams(), $default);
384
    }
385
386
    /**
387
     * Issue an access token.
388
     *
389
     * @param ScopeEntityInterface[] $scopes
390
     *
391 31
     * @throws OAuthServerException
392
     * @throws UniqueTokenIdentifierConstraintViolationException
393 31
     */
394
    protected function issueAccessToken(
395
        DateInterval $accessTokenTTL,
396
        ClientEntityInterface $client,
397
        ?UserEntityInterface $user,
398
        array $scopes = []
399
    ): AccessTokenEntityInterface {
400
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
401
402
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $user);
403
        $accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL));
404 28
405
        while ($maxGenerationAttempts-- > 0) {
406
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
407
            try {
408
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
409
410 28
                return $accessToken;
411
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
412 28
                if ($maxGenerationAttempts === 0) {
413 28
                    throw $e;
414 28
                }
415
            }
416 28
        }
417 28
418
        // This should never be hit. It is here to work around a PHPStan false error
419 28
        return $accessToken;
420
    }
421 26
422 3
    /**
423 2
     * Issue an auth code.
424 1
     *
425
     * @param ?UserEntityInterface   $user
426
     * @param ScopeEntityInterface[] $scopes
427
     *
428
     * @throws OAuthServerException
429
     * @throws UniqueTokenIdentifierConstraintViolationException
430
     */
431
    protected function issueAuthCode(
432
        DateInterval $authCodeTTL,
433
        ClientEntityInterface $client,
434
        ?UserEntityInterface $user,
435
        ?string $redirectUri,
436
        array $scopes = [],
437
        ?string $codeChallenge = null,
438
        ?string $codeChallengeMethod = null
439
    ): AuthCodeEntityInterface {
440
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
441
442 7
        $authCode = $this->authCodeRepository->getNewAuthCode();
443
        $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL));
444
        $authCode->setClient($client);
445
        $authCode->setUser($user);
446
        $authCode->setCodeChallenge($codeChallenge);
447
        $authCode->setCodeChallengeMethod($codeChallengeMethod);
448
449 7
        if ($redirectUri !== null) {
450
            $authCode->setRedirectUri($redirectUri);
451 7
        }
452 7
453 7
        foreach ($scopes as $scope) {
454 7
            $authCode->addScope($scope);
455
        }
456 7
457 1
        while ($maxGenerationAttempts-- > 0) {
458
            $authCode->setIdentifier($this->generateUniqueIdentifier());
459
            try {
460 7
                $this->authCodeRepository->persistNewAuthCode($authCode);
461 1
462
                return $authCode;
463
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
464 7
                if ($maxGenerationAttempts === 0) {
465 7
                    throw $e;
466
                }
467 7
            }
468
        }
469 5
470 3
        // This should never be hit. It is here to work around a PHPStan false error
471 2
        return $authCode;
472 1
    }
473
474
    /**
475
     * @throws OAuthServerException
476
     * @throws UniqueTokenIdentifierConstraintViolationException
477
     */
478
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface
479
    {
480
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
481
482
        if ($refreshToken === null) {
483
            return null;
484
        }
485 22
486
        $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
487 22
        $refreshToken->setAccessToken($accessToken);
488
489 22
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
490 5
491
        while ($maxGenerationAttempts-- > 0) {
492
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
493 17
            try {
494 17
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
495
496 17
                return $refreshToken;
497
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
498 17
                if ($maxGenerationAttempts === 0) {
499 17
                    throw $e;
500
                }
501 17
            }
502
        }
503 15
504 3
        // This should never be hit. It is here to work around a PHPStan false error
505 2
        return $refreshToken;
506 1
    }
507
508
    /**
509
     * Generate a new unique identifier.
510
     *
511
     * @return non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
512
     *
513
     * @throws OAuthServerException
514
     */
515
    protected function generateUniqueIdentifier(int $length = 40): string
516
    {
517
        try {
518
            if ($length < 1) {
519
                throw new DomainException('Length must be a positive integer');
520
            }
521
522 42
            return bin2hex(random_bytes($length));
523
            // @codeCoverageIgnoreStart
524
        } catch (TypeError | Error $e) {
525 42
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
526
        } catch (Exception $e) {
527
            // If you get this message, the CSPRNG failed hard.
528
            throw OAuthServerException::serverError('Could not generate a random string', $e);
529 42
        }
530
        // @codeCoverageIgnoreEnd
531
    }
532
533
    /**
534
     * {@inheritdoc}
535
     */
536
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request): bool
537
    {
538
        $requestParameters = (array) $request->getParsedBody();
539
540
        return (
541
            array_key_exists('grant_type', $requestParameters)
542
            && $requestParameters['grant_type'] === $this->getIdentifier()
543 5
        );
544
    }
545 5
546
    /**
547 5
     * {@inheritdoc}
548 5
     */
549 5
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool
550 5
    {
551
        return false;
552
    }
553
554
    /**
555
     * {@inheritdoc}
556 1
     */
557
    public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface
558 1
    {
559
        throw new LogicException('This grant cannot validate an authorization request');
560
    }
561
562
    /**
563
     * {@inheritdoc}
564 1
     */
565
    public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface
566 1
    {
567
        throw new LogicException('This grant cannot complete an authorization request');
568
    }
569
570
    /**
571
     * {@inheritdoc}
572 1
     */
573
    public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request): bool
574 1
    {
575
        return false;
576
    }
577
578
    /**
579
     * {@inheritdoc}
580
     */
581
    public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse
582
    {
583
        throw new LogicException('This grant cannot validate a device authorization request');
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     */
589
    public function completeDeviceAuthorizationRequest(string $deviceCode, UserEntityInterface $user, bool $userApproved): void
590
    {
591
        throw new LogicException('This grant cannot complete a device authorization request');
592
    }
593
594
    /**
595
     * {@inheritdoc}
596
     */
597
    public function setIntervalVisibility(bool $intervalVisibility): void
598
    {
599
        throw new LogicException('This grant does not support the interval parameter');
600
    }
601
602
    /**
603
     * {@inheritdoc}
604
     */
605
    public function getIntervalVisibility(): bool
606
    {
607
        return false;
608
    }
609
610
    /**
611
     * {@inheritdoc}
612
     */
613
    public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void
614
    {
615
        throw new LogicException('This grant does not support the verification_uri_complete parameter');
616
    }
617
}
618