source: vendor/google/auth/src/OAuth2.php@ f9c482b

Last change on this file since f9c482b was f9c482b, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload new project files

  • Property mode set to 100644
File size: 48.6 KB
RevLine 
[f9c482b]1<?php
2/*
3 * Copyright 2015 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Auth;
19
20use Firebase\JWT\JWT;
21use Firebase\JWT\Key;
22use Google\Auth\HttpHandler\HttpClientCache;
23use Google\Auth\HttpHandler\HttpHandlerFactory;
24use GuzzleHttp\Psr7\Query;
25use GuzzleHttp\Psr7\Request;
26use GuzzleHttp\Psr7\Utils;
27use InvalidArgumentException;
28use Psr\Http\Message\RequestInterface;
29use Psr\Http\Message\ResponseInterface;
30use Psr\Http\Message\UriInterface;
31
32/**
33 * OAuth2 supports authentication by OAuth2 2-legged flows.
34 *
35 * It primary supports
36 * - service account authorization
37 * - authorization where a user already has an access token
38 */
39class OAuth2 implements FetchAuthTokenInterface
40{
41 const DEFAULT_EXPIRY_SECONDS = 3600; // 1 hour
42 const DEFAULT_SKEW_SECONDS = 60; // 1 minute
43 const JWT_URN = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
44 const STS_URN = 'urn:ietf:params:oauth:grant-type:token-exchange';
45 private const STS_REQUESTED_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
46
47 /**
48 * TODO: determine known methods from the keys of JWT::methods.
49 *
50 * @var array<string>
51 */
52 public static $knownSigningAlgorithms = [
53 'HS256',
54 'HS512',
55 'HS384',
56 'RS256',
57 ];
58
59 /**
60 * The well known grant types.
61 *
62 * @var array<string>
63 */
64 public static $knownGrantTypes = [
65 'authorization_code',
66 'refresh_token',
67 'password',
68 'client_credentials',
69 ];
70
71 /**
72 * - authorizationUri
73 * The authorization server's HTTP endpoint capable of
74 * authenticating the end-user and obtaining authorization.
75 *
76 * @var ?UriInterface
77 */
78 private $authorizationUri;
79
80 /**
81 * - tokenCredentialUri
82 * The authorization server's HTTP endpoint capable of issuing
83 * tokens and refreshing expired tokens.
84 *
85 * @var UriInterface
86 */
87 private $tokenCredentialUri;
88
89 /**
90 * The redirection URI used in the initial request.
91 *
92 * @var ?string
93 */
94 private $redirectUri;
95
96 /**
97 * A unique identifier issued to the client to identify itself to the
98 * authorization server.
99 *
100 * @var string
101 */
102 private $clientId;
103
104 /**
105 * A shared symmetric secret issued by the authorization server, which is
106 * used to authenticate the client.
107 *
108 * @var string
109 */
110 private $clientSecret;
111
112 /**
113 * The resource owner's username.
114 *
115 * @var ?string
116 */
117 private $username;
118
119 /**
120 * The resource owner's password.
121 *
122 * @var ?string
123 */
124 private $password;
125
126 /**
127 * The scope of the access request, expressed either as an Array or as a
128 * space-delimited string.
129 *
130 * @var ?array<string>
131 */
132 private $scope;
133
134 /**
135 * An arbitrary string designed to allow the client to maintain state.
136 *
137 * @var string
138 */
139 private $state;
140
141 /**
142 * The authorization code issued to this client.
143 *
144 * Only used by the authorization code access grant type.
145 *
146 * @var ?string
147 */
148 private $code;
149
150 /**
151 * The issuer ID when using assertion profile.
152 *
153 * @var ?string
154 */
155 private $issuer;
156
157 /**
158 * The target audience for assertions.
159 *
160 * @var string
161 */
162 private $audience;
163
164 /**
165 * The target sub when issuing assertions.
166 *
167 * @var string
168 */
169 private $sub;
170
171 /**
172 * The number of seconds assertions are valid for.
173 *
174 * @var int
175 */
176 private $expiry;
177
178 /**
179 * The signing key when using assertion profile.
180 *
181 * @var ?string
182 */
183 private $signingKey;
184
185 /**
186 * The signing key id when using assertion profile. Param kid in jwt header
187 *
188 * @var string
189 */
190 private $signingKeyId;
191
192 /**
193 * The signing algorithm when using an assertion profile.
194 *
195 * @var ?string
196 */
197 private $signingAlgorithm;
198
199 /**
200 * The refresh token associated with the access token to be refreshed.
201 *
202 * @var ?string
203 */
204 private $refreshToken;
205
206 /**
207 * The current access token.
208 *
209 * @var string
210 */
211 private $accessToken;
212
213 /**
214 * The current ID token.
215 *
216 * @var string
217 */
218 private $idToken;
219
220 /**
221 * The scopes granted to the current access token
222 *
223 * @var string
224 */
225 private $grantedScope;
226
227 /**
228 * The lifetime in seconds of the current access token.
229 *
230 * @var ?int
231 */
232 private $expiresIn;
233
234 /**
235 * The expiration time of the access token as a number of seconds since the
236 * unix epoch.
237 *
238 * @var ?int
239 */
240 private $expiresAt;
241
242 /**
243 * The issue time of the access token as a number of seconds since the unix
244 * epoch.
245 *
246 * @var ?int
247 */
248 private $issuedAt;
249
250 /**
251 * The current grant type.
252 *
253 * @var ?string
254 */
255 private $grantType;
256
257 /**
258 * When using an extension grant type, this is the set of parameters used by
259 * that extension.
260 *
261 * @var array<mixed>
262 */
263 private $extensionParams;
264
265 /**
266 * When using the toJwt function, these claims will be added to the JWT
267 * payload.
268 *
269 * @var array<mixed>
270 */
271 private $additionalClaims;
272
273 /**
274 * The code verifier for PKCE for OAuth 2.0. When set, the authorization
275 * URI will contain the Code Challenge and Code Challenge Method querystring
276 * parameters, and the token URI will contain the Code Verifier parameter.
277 *
278 * @see https://datatracker.ietf.org/doc/html/rfc7636
279 * @var ?string
280 */
281 private $codeVerifier;
282
283 /**
284 * For STS requests.
285 * A URI that indicates the target service or resource where the client
286 * intends to use the requested security token.
287 */
288 private ?string $resource;
289
290 /**
291 * For STS requests.
292 * A fetcher for the "subject_token", which is a security token that
293 * represents the identity of the party on behalf of whom the request is
294 * being made.
295 */
296 private ?ExternalAccountCredentialSourceInterface $subjectTokenFetcher;
297
298 /**
299 * For STS requests.
300 * An identifier, that indicates the type of the security token in the
301 * subjectToken parameter.
302 */
303 private ?string $subjectTokenType;
304
305 /**
306 * For STS requests.
307 * A security token that represents the identity of the acting party.
308 */
309 private ?string $actorToken;
310
311 /**
312 * For STS requests.
313 * An identifier that indicates the type of the security token in the
314 * actorToken parameter.
315 */
316 private ?string $actorTokenType;
317
318 /**
319 * From STS response.
320 * An identifier for the representation of the issued security token.
321 */
322 private ?string $issuedTokenType = null;
323
324 /**
325 * From STS response.
326 * An identifier for the representation of the issued security token.
327 *
328 * @var array<mixed>
329 */
330 private array $additionalOptions;
331
332 /**
333 * Create a new OAuthCredentials.
334 *
335 * The configuration array accepts various options
336 *
337 * - authorizationUri
338 * The authorization server's HTTP endpoint capable of
339 * authenticating the end-user and obtaining authorization.
340 *
341 * - tokenCredentialUri
342 * The authorization server's HTTP endpoint capable of issuing
343 * tokens and refreshing expired tokens.
344 *
345 * - clientId
346 * A unique identifier issued to the client to identify itself to the
347 * authorization server.
348 *
349 * - clientSecret
350 * A shared symmetric secret issued by the authorization server,
351 * which is used to authenticate the client.
352 *
353 * - scope
354 * The scope of the access request, expressed either as an Array
355 * or as a space-delimited String.
356 *
357 * - state
358 * An arbitrary string designed to allow the client to maintain state.
359 *
360 * - redirectUri
361 * The redirection URI used in the initial request.
362 *
363 * - username
364 * The resource owner's username.
365 *
366 * - password
367 * The resource owner's password.
368 *
369 * - issuer
370 * Issuer ID when using assertion profile
371 *
372 * - audience
373 * Target audience for assertions
374 *
375 * - expiry
376 * Number of seconds assertions are valid for
377 *
378 * - signingKey
379 * Signing key when using assertion profile
380 *
381 * - signingKeyId
382 * Signing key id when using assertion profile
383 *
384 * - refreshToken
385 * The refresh token associated with the access token
386 * to be refreshed.
387 *
388 * - accessToken
389 * The current access token for this client.
390 *
391 * - idToken
392 * The current ID token for this client.
393 *
394 * - extensionParams
395 * When using an extension grant type, this is the set of parameters used
396 * by that extension.
397 *
398 * - codeVerifier
399 * The code verifier for PKCE for OAuth 2.0.
400 *
401 * - resource
402 * The target service or resource where the client ntends to use the
403 * requested security token.
404 *
405 * - subjectTokenFetcher
406 * A fetcher for the "subject_token", which is a security token that
407 * represents the identity of the party on behalf of whom the request is
408 * being made.
409 *
410 * - subjectTokenType
411 * An identifier that indicates the type of the security token in the
412 * subjectToken parameter.
413 *
414 * - actorToken
415 * A security token that represents the identity of the acting party.
416 *
417 * - actorTokenType
418 * An identifier for the representation of the issued security token.
419 *
420 * @param array<mixed> $config Configuration array
421 */
422 public function __construct(array $config)
423 {
424 $opts = array_merge([
425 'expiry' => self::DEFAULT_EXPIRY_SECONDS,
426 'extensionParams' => [],
427 'authorizationUri' => null,
428 'redirectUri' => null,
429 'tokenCredentialUri' => null,
430 'state' => null,
431 'username' => null,
432 'password' => null,
433 'clientId' => null,
434 'clientSecret' => null,
435 'issuer' => null,
436 'sub' => null,
437 'audience' => null,
438 'signingKey' => null,
439 'signingKeyId' => null,
440 'signingAlgorithm' => null,
441 'scope' => null,
442 'additionalClaims' => [],
443 'codeVerifier' => null,
444 'resource' => null,
445 'subjectTokenFetcher' => null,
446 'subjectTokenType' => null,
447 'actorToken' => null,
448 'actorTokenType' => null,
449 'additionalOptions' => [],
450 ], $config);
451
452 $this->setAuthorizationUri($opts['authorizationUri']);
453 $this->setRedirectUri($opts['redirectUri']);
454 $this->setTokenCredentialUri($opts['tokenCredentialUri']);
455 $this->setState($opts['state']);
456 $this->setUsername($opts['username']);
457 $this->setPassword($opts['password']);
458 $this->setClientId($opts['clientId']);
459 $this->setClientSecret($opts['clientSecret']);
460 $this->setIssuer($opts['issuer']);
461 $this->setSub($opts['sub']);
462 $this->setExpiry($opts['expiry']);
463 $this->setAudience($opts['audience']);
464 $this->setSigningKey($opts['signingKey']);
465 $this->setSigningKeyId($opts['signingKeyId']);
466 $this->setSigningAlgorithm($opts['signingAlgorithm']);
467 $this->setScope($opts['scope']);
468 $this->setExtensionParams($opts['extensionParams']);
469 $this->setAdditionalClaims($opts['additionalClaims']);
470 $this->setCodeVerifier($opts['codeVerifier']);
471
472 // for STS
473 $this->resource = $opts['resource'];
474 $this->subjectTokenFetcher = $opts['subjectTokenFetcher'];
475 $this->subjectTokenType = $opts['subjectTokenType'];
476 $this->actorToken = $opts['actorToken'];
477 $this->actorTokenType = $opts['actorTokenType'];
478 $this->additionalOptions = $opts['additionalOptions'];
479
480 $this->updateToken($opts);
481 }
482
483 /**
484 * Verifies the idToken if present.
485 *
486 * - if none is present, return null
487 * - if present, but invalid, raises DomainException.
488 * - otherwise returns the payload in the idtoken as a PHP object.
489 *
490 * The behavior of this method varies depending on the version of
491 * `firebase/php-jwt` you are using. In versions 6.0 and above, you cannot
492 * provide multiple $allowed_algs, and instead must provide an array of Key
493 * objects as the $publicKey.
494 *
495 * @param string|Key|Key[] $publicKey The public key to use to authenticate the token
496 * @param string|array<string> $allowed_algs algorithm or array of supported verification algorithms.
497 * Providing more than one algorithm will throw an exception.
498 * @throws \DomainException if the token is missing an audience.
499 * @throws \DomainException if the audience does not match the one set in
500 * the OAuth2 class instance.
501 * @throws \UnexpectedValueException If the token is invalid
502 * @throws \InvalidArgumentException If more than one value for allowed_algs is supplied
503 * @throws \Firebase\JWT\SignatureInvalidException If the signature is invalid.
504 * @throws \Firebase\JWT\BeforeValidException If the token is not yet valid.
505 * @throws \Firebase\JWT\ExpiredException If the token has expired.
506 * @return null|object
507 */
508 public function verifyIdToken($publicKey = null, $allowed_algs = [])
509 {
510 $idToken = $this->getIdToken();
511 if (is_null($idToken)) {
512 return null;
513 }
514
515 $resp = $this->jwtDecode($idToken, $publicKey, $allowed_algs);
516 if (!property_exists($resp, 'aud')) {
517 throw new \DomainException('No audience found the id token');
518 }
519 if ($resp->aud != $this->getAudience()) {
520 throw new \DomainException('Wrong audience present in the id token');
521 }
522
523 return $resp;
524 }
525
526 /**
527 * Obtains the encoded jwt from the instance data.
528 *
529 * @param array<mixed> $config array optional configuration parameters
530 * @return string
531 */
532 public function toJwt(array $config = [])
533 {
534 if (is_null($this->getSigningKey())) {
535 throw new \DomainException('No signing key available');
536 }
537 if (is_null($this->getSigningAlgorithm())) {
538 throw new \DomainException('No signing algorithm specified');
539 }
540 $now = time();
541
542 $opts = array_merge([
543 'skew' => self::DEFAULT_SKEW_SECONDS,
544 ], $config);
545
546 $assertion = [
547 'iss' => $this->getIssuer(),
548 'exp' => ($now + $this->getExpiry()),
549 'iat' => ($now - $opts['skew']),
550 ];
551 foreach ($assertion as $k => $v) {
552 if (is_null($v)) {
553 throw new \DomainException($k . ' should not be null');
554 }
555 }
556 if (!(is_null($this->getAudience()))) {
557 $assertion['aud'] = $this->getAudience();
558 }
559
560 if (!(is_null($this->getScope()))) {
561 $assertion['scope'] = $this->getScope();
562 }
563
564 if (empty($assertion['scope']) && empty($assertion['aud'])) {
565 throw new \DomainException('one of scope or aud should not be null');
566 }
567
568 if (!(is_null($this->getSub()))) {
569 $assertion['sub'] = $this->getSub();
570 }
571 $assertion += $this->getAdditionalClaims();
572
573 return JWT::encode(
574 $assertion,
575 $this->getSigningKey(),
576 $this->getSigningAlgorithm(),
577 $this->getSigningKeyId()
578 );
579 }
580
581 /**
582 * Generates a request for token credentials.
583 *
584 * @param callable|null $httpHandler callback which delivers psr7 request
585 * @param array<mixed> $headers [optional] Additional headers to pass to
586 * the token endpoint request.
587 * @return RequestInterface the authorization Url.
588 */
589 public function generateCredentialsRequest(?callable $httpHandler = null, $headers = [])
590 {
591 $uri = $this->getTokenCredentialUri();
592 if (is_null($uri)) {
593 throw new \DomainException('No token credential URI was set.');
594 }
595
596 $grantType = $this->getGrantType();
597 $params = ['grant_type' => $grantType];
598 switch ($grantType) {
599 case 'authorization_code':
600 $params['code'] = $this->getCode();
601 $params['redirect_uri'] = $this->getRedirectUri();
602 if ($this->codeVerifier) {
603 $params['code_verifier'] = $this->codeVerifier;
604 }
605 $this->addClientCredentials($params);
606 break;
607 case 'password':
608 $params['username'] = $this->getUsername();
609 $params['password'] = $this->getPassword();
610 $this->addClientCredentials($params);
611 break;
612 case 'refresh_token':
613 $params['refresh_token'] = $this->getRefreshToken();
614 if (isset($this->getAdditionalClaims()['target_audience'])) {
615 $params['target_audience'] = $this->getAdditionalClaims()['target_audience'];
616 }
617 $this->addClientCredentials($params);
618 break;
619 case self::JWT_URN:
620 $params['assertion'] = $this->toJwt();
621 break;
622 case self::STS_URN:
623 $token = $this->subjectTokenFetcher->fetchSubjectToken($httpHandler);
624 $params['subject_token'] = $token;
625 $params['subject_token_type'] = $this->subjectTokenType;
626 $params += array_filter([
627 'resource' => $this->resource,
628 'audience' => $this->audience,
629 'scope' => $this->getScope(),
630 'requested_token_type' => self::STS_REQUESTED_TOKEN_TYPE,
631 'actor_token' => $this->actorToken,
632 'actor_token_type' => $this->actorTokenType,
633 ]);
634 if ($this->additionalOptions) {
635 $params['options'] = json_encode($this->additionalOptions);
636 }
637 break;
638 default:
639 if (!is_null($this->getRedirectUri())) {
640 # Grant type was supposed to be 'authorization_code', as there
641 # is a redirect URI.
642 throw new \DomainException('Missing authorization code');
643 }
644 unset($params['grant_type']);
645 if (!is_null($grantType)) {
646 $params['grant_type'] = $grantType;
647 }
648 $params = array_merge($params, $this->getExtensionParams());
649 }
650
651 $headers = [
652 'Cache-Control' => 'no-store',
653 'Content-Type' => 'application/x-www-form-urlencoded',
654 ] + $headers;
655
656 return new Request(
657 'POST',
658 $uri,
659 $headers,
660 Query::build($params)
661 );
662 }
663
664 /**
665 * Fetches the auth tokens based on the current state.
666 *
667 * @param callable|null $httpHandler callback which delivers psr7 request
668 * @param array<mixed> $headers [optional] If present, add these headers to the token
669 * endpoint request.
670 * @return array<mixed> the response
671 */
672 public function fetchAuthToken(?callable $httpHandler = null, $headers = [])
673 {
674 if (is_null($httpHandler)) {
675 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
676 }
677
678 $response = $httpHandler($this->generateCredentialsRequest($httpHandler, $headers));
679 $credentials = $this->parseTokenResponse($response);
680 $this->updateToken($credentials);
681 if (isset($credentials['scope'])) {
682 $this->setGrantedScope($credentials['scope']);
683 }
684
685 return $credentials;
686 }
687
688 /**
689 * @deprecated
690 *
691 * Obtains a key that can used to cache the results of #fetchAuthToken.
692 *
693 * The key is derived from the scopes.
694 *
695 * @return ?string a key that may be used to cache the auth token.
696 */
697 public function getCacheKey()
698 {
699 if (is_array($this->scope)) {
700 return implode(':', $this->scope);
701 }
702
703 if ($this->audience) {
704 return $this->audience;
705 }
706
707 // If scope has not set, return null to indicate no caching.
708 return null;
709 }
710
711 /**
712 * Gets this instance's SubjectTokenFetcher
713 *
714 * @return null|ExternalAccountCredentialSourceInterface
715 */
716 public function getSubjectTokenFetcher(): ?ExternalAccountCredentialSourceInterface
717 {
718 return $this->subjectTokenFetcher;
719 }
720
721 /**
722 * Parses the fetched tokens.
723 *
724 * @param ResponseInterface $resp the response.
725 * @return array<mixed> the tokens parsed from the response body.
726 * @throws \Exception
727 */
728 public function parseTokenResponse(ResponseInterface $resp)
729 {
730 $body = (string) $resp->getBody();
731 if ($resp->hasHeader('Content-Type') &&
732 $resp->getHeaderLine('Content-Type') == 'application/x-www-form-urlencoded'
733 ) {
734 $res = [];
735 parse_str($body, $res);
736
737 return $res;
738 }
739
740 // Assume it's JSON; if it's not throw an exception
741 if (null === $res = json_decode($body, true)) {
742 throw new \Exception('Invalid JSON response');
743 }
744
745 return $res;
746 }
747
748 /**
749 * Updates an OAuth 2.0 client.
750 *
751 * Example:
752 * ```
753 * $oauth->updateToken([
754 * 'refresh_token' => 'n4E9O119d',
755 * 'access_token' => 'FJQbwq9',
756 * 'expires_in' => 3600
757 * ]);
758 * ```
759 *
760 * @param array<mixed> $config
761 * The configuration parameters related to the token.
762 *
763 * - refresh_token
764 * The refresh token associated with the access token
765 * to be refreshed.
766 *
767 * - access_token
768 * The current access token for this client.
769 *
770 * - id_token
771 * The current ID token for this client.
772 *
773 * - expires_in
774 * The time in seconds until access token expiration.
775 *
776 * - expires_at
777 * The time as an integer number of seconds since the Epoch
778 *
779 * - issued_at
780 * The timestamp that the token was issued at.
781 * @return void
782 */
783 public function updateToken(array $config)
784 {
785 $opts = array_merge([
786 'extensionParams' => [],
787 'access_token' => null,
788 'id_token' => null,
789 'expires_in' => null,
790 'expires_at' => null,
791 'issued_at' => null,
792 'scope' => null,
793 ], $config);
794
795 $this->setExpiresAt($opts['expires_at']);
796 $this->setExpiresIn($opts['expires_in']);
797 // By default, the token is issued at `Time.now` when `expiresIn` is set,
798 // but this can be used to supply a more precise time.
799 if (!is_null($opts['issued_at'])) {
800 $this->setIssuedAt($opts['issued_at']);
801 }
802
803 $this->setAccessToken($opts['access_token']);
804 $this->setIdToken($opts['id_token']);
805
806 // The refresh token should only be updated if a value is explicitly
807 // passed in, as some access token responses do not include a refresh
808 // token.
809 if (array_key_exists('refresh_token', $opts)) {
810 $this->setRefreshToken($opts['refresh_token']);
811 }
812
813 // Required for STS response. An identifier for the representation of
814 // the issued security token.
815 if (array_key_exists('issued_token_type', $opts)) {
816 $this->issuedTokenType = $opts['issued_token_type'];
817 }
818 }
819
820 /**
821 * Builds the authorization Uri that the user should be redirected to.
822 *
823 * @param array<mixed> $config configuration options that customize the return url.
824 * @return UriInterface the authorization Url.
825 * @throws InvalidArgumentException
826 */
827 public function buildFullAuthorizationUri(array $config = [])
828 {
829 if (is_null($this->getAuthorizationUri())) {
830 throw new InvalidArgumentException(
831 'requires an authorizationUri to have been set'
832 );
833 }
834
835 $params = array_merge([
836 'response_type' => 'code',
837 'access_type' => 'offline',
838 'client_id' => $this->clientId,
839 'redirect_uri' => $this->redirectUri,
840 'state' => $this->state,
841 'scope' => $this->getScope(),
842 ], $config);
843
844 // Validate the auth_params
845 if (is_null($params['client_id'])) {
846 throw new InvalidArgumentException(
847 'missing the required client identifier'
848 );
849 }
850 if (is_null($params['redirect_uri'])) {
851 throw new InvalidArgumentException('missing the required redirect URI');
852 }
853 if (!empty($params['prompt']) && !empty($params['approval_prompt'])) {
854 throw new InvalidArgumentException(
855 'prompt and approval_prompt are mutually exclusive'
856 );
857 }
858 if ($this->codeVerifier) {
859 $params['code_challenge'] = $this->getCodeChallenge($this->codeVerifier);
860 $params['code_challenge_method'] = $this->getCodeChallengeMethod();
861 }
862
863 // Construct the uri object; return it if it is valid.
864 $result = clone $this->authorizationUri;
865 $existingParams = Query::parse($result->getQuery());
866
867 $result = $result->withQuery(
868 Query::build(array_merge($existingParams, $params))
869 );
870
871 if ($result->getScheme() != 'https') {
872 throw new InvalidArgumentException(
873 'Authorization endpoint must be protected by TLS'
874 );
875 }
876
877 return $result;
878 }
879
880 /**
881 * @return string|null
882 */
883 public function getCodeVerifier(): ?string
884 {
885 return $this->codeVerifier;
886 }
887
888 /**
889 * A cryptographically random string that is used to correlate the
890 * authorization request to the token request.
891 *
892 * The code verifier for PKCE for OAuth 2.0. When set, the authorization
893 * URI will contain the Code Challenge and Code Challenge Method querystring
894 * parameters, and the token URI will contain the Code Verifier parameter.
895 *
896 * @see https://datatracker.ietf.org/doc/html/rfc7636
897 *
898 * @param string|null $codeVerifier
899 */
900 public function setCodeVerifier(?string $codeVerifier): void
901 {
902 $this->codeVerifier = $codeVerifier;
903 }
904
905 /**
906 * Generates a random 128-character string for the "code_verifier" parameter
907 * in PKCE for OAuth 2.0. This is a cryptographically random string that is
908 * determined using random_int, hashed using "hash" and sha256, and base64
909 * encoded.
910 *
911 * When this method is called, the code verifier is set on the object.
912 *
913 * @return string
914 */
915 public function generateCodeVerifier(): string
916 {
917 return $this->codeVerifier = $this->generateRandomString(128);
918 }
919
920 private function getCodeChallenge(string $randomString): string
921 {
922 return rtrim(strtr(base64_encode(hash('sha256', $randomString, true)), '+/', '-_'), '=');
923 }
924
925 private function getCodeChallengeMethod(): string
926 {
927 return 'S256';
928 }
929
930 private function generateRandomString(int $length): string
931 {
932 $validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~';
933 $validCharsLen = strlen($validChars);
934 $str = '';
935 $i = 0;
936 while ($i++ < $length) {
937 $str .= $validChars[random_int(0, $validCharsLen - 1)];
938 }
939 return $str;
940 }
941
942 /**
943 * Sets the authorization server's HTTP endpoint capable of authenticating
944 * the end-user and obtaining authorization.
945 *
946 * @param string $uri
947 * @return void
948 */
949 public function setAuthorizationUri($uri)
950 {
951 $this->authorizationUri = $this->coerceUri($uri);
952 }
953
954 /**
955 * Gets the authorization server's HTTP endpoint capable of authenticating
956 * the end-user and obtaining authorization.
957 *
958 * @return ?UriInterface
959 */
960 public function getAuthorizationUri()
961 {
962 return $this->authorizationUri;
963 }
964
965 /**
966 * Gets the authorization server's HTTP endpoint capable of issuing tokens
967 * and refreshing expired tokens.
968 *
969 * @return ?UriInterface
970 */
971 public function getTokenCredentialUri()
972 {
973 return $this->tokenCredentialUri;
974 }
975
976 /**
977 * Sets the authorization server's HTTP endpoint capable of issuing tokens
978 * and refreshing expired tokens.
979 *
980 * @param string $uri
981 * @return void
982 */
983 public function setTokenCredentialUri($uri)
984 {
985 $this->tokenCredentialUri = $this->coerceUri($uri);
986 }
987
988 /**
989 * Gets the redirection URI used in the initial request.
990 *
991 * @return ?string
992 */
993 public function getRedirectUri()
994 {
995 return $this->redirectUri;
996 }
997
998 /**
999 * Sets the redirection URI used in the initial request.
1000 *
1001 * @param ?string $uri
1002 * @return void
1003 */
1004 public function setRedirectUri($uri)
1005 {
1006 if (is_null($uri)) {
1007 $this->redirectUri = null;
1008
1009 return;
1010 }
1011 // redirect URI must be absolute
1012 if (!$this->isAbsoluteUri($uri)) {
1013 // "postmessage" is a reserved URI string in Google-land
1014 // @see https://developers.google.com/identity/sign-in/web/server-side-flow
1015 if ('postmessage' !== (string) $uri) {
1016 throw new InvalidArgumentException(
1017 'Redirect URI must be absolute'
1018 );
1019 }
1020 }
1021 $this->redirectUri = (string) $uri;
1022 }
1023
1024 /**
1025 * Gets the scope of the access requests as a space-delimited String.
1026 *
1027 * @return ?string
1028 */
1029 public function getScope()
1030 {
1031 if (is_null($this->scope)) {
1032 return $this->scope;
1033 }
1034
1035 return implode(' ', $this->scope);
1036 }
1037
1038 /**
1039 * Gets the subject token type
1040 *
1041 * @return ?string
1042 */
1043 public function getSubjectTokenType(): ?string
1044 {
1045 return $this->subjectTokenType;
1046 }
1047
1048 /**
1049 * Sets the scope of the access request, expressed either as an Array or as
1050 * a space-delimited String.
1051 *
1052 * @param string|array<string>|null $scope
1053 * @return void
1054 * @throws InvalidArgumentException
1055 */
1056 public function setScope($scope)
1057 {
1058 if (is_null($scope)) {
1059 $this->scope = null;
1060 } elseif (is_string($scope)) {
1061 $this->scope = explode(' ', $scope);
1062 } elseif (is_array($scope)) {
1063 foreach ($scope as $s) {
1064 $pos = strpos($s, ' ');
1065 if ($pos !== false) {
1066 throw new InvalidArgumentException(
1067 'array scope values should not contain spaces'
1068 );
1069 }
1070 }
1071 $this->scope = $scope;
1072 } else {
1073 throw new InvalidArgumentException(
1074 'scopes should be a string or array of strings'
1075 );
1076 }
1077 }
1078
1079 /**
1080 * Gets the current grant type.
1081 *
1082 * @return ?string
1083 */
1084 public function getGrantType()
1085 {
1086 if (!is_null($this->grantType)) {
1087 return $this->grantType;
1088 }
1089
1090 // Returns the inferred grant type, based on the current object instance
1091 // state.
1092 if (!is_null($this->code)) {
1093 return 'authorization_code';
1094 }
1095
1096 if (!is_null($this->refreshToken)) {
1097 return 'refresh_token';
1098 }
1099
1100 if (!is_null($this->username) && !is_null($this->password)) {
1101 return 'password';
1102 }
1103
1104 if (!is_null($this->issuer) && !is_null($this->signingKey)) {
1105 return self::JWT_URN;
1106 }
1107
1108 if (!is_null($this->subjectTokenFetcher) && !is_null($this->subjectTokenType)) {
1109 return self::STS_URN;
1110 }
1111
1112 return null;
1113 }
1114
1115 /**
1116 * Sets the current grant type.
1117 *
1118 * @param string $grantType
1119 * @return void
1120 * @throws InvalidArgumentException
1121 */
1122 public function setGrantType($grantType)
1123 {
1124 if (in_array($grantType, self::$knownGrantTypes)) {
1125 $this->grantType = $grantType;
1126 } else {
1127 // validate URI
1128 if (!$this->isAbsoluteUri($grantType)) {
1129 throw new InvalidArgumentException(
1130 'invalid grant type'
1131 );
1132 }
1133 $this->grantType = (string) $grantType;
1134 }
1135 }
1136
1137 /**
1138 * Gets an arbitrary string designed to allow the client to maintain state.
1139 *
1140 * @return string
1141 */
1142 public function getState()
1143 {
1144 return $this->state;
1145 }
1146
1147 /**
1148 * Sets an arbitrary string designed to allow the client to maintain state.
1149 *
1150 * @param string $state
1151 * @return void
1152 */
1153 public function setState($state)
1154 {
1155 $this->state = $state;
1156 }
1157
1158 /**
1159 * Gets the authorization code issued to this client.
1160 *
1161 * @return string
1162 */
1163 public function getCode()
1164 {
1165 return $this->code;
1166 }
1167
1168 /**
1169 * Sets the authorization code issued to this client.
1170 *
1171 * @param string $code
1172 * @return void
1173 */
1174 public function setCode($code)
1175 {
1176 $this->code = $code;
1177 }
1178
1179 /**
1180 * Gets the resource owner's username.
1181 *
1182 * @return string
1183 */
1184 public function getUsername()
1185 {
1186 return $this->username;
1187 }
1188
1189 /**
1190 * Sets the resource owner's username.
1191 *
1192 * @param string $username
1193 * @return void
1194 */
1195 public function setUsername($username)
1196 {
1197 $this->username = $username;
1198 }
1199
1200 /**
1201 * Gets the resource owner's password.
1202 *
1203 * @return string
1204 */
1205 public function getPassword()
1206 {
1207 return $this->password;
1208 }
1209
1210 /**
1211 * Sets the resource owner's password.
1212 *
1213 * @param string $password
1214 * @return void
1215 */
1216 public function setPassword($password)
1217 {
1218 $this->password = $password;
1219 }
1220
1221 /**
1222 * Sets a unique identifier issued to the client to identify itself to the
1223 * authorization server.
1224 *
1225 * @return string
1226 */
1227 public function getClientId()
1228 {
1229 return $this->clientId;
1230 }
1231
1232 /**
1233 * Sets a unique identifier issued to the client to identify itself to the
1234 * authorization server.
1235 *
1236 * @param string $clientId
1237 * @return void
1238 */
1239 public function setClientId($clientId)
1240 {
1241 $this->clientId = $clientId;
1242 }
1243
1244 /**
1245 * Gets a shared symmetric secret issued by the authorization server, which
1246 * is used to authenticate the client.
1247 *
1248 * @return string
1249 */
1250 public function getClientSecret()
1251 {
1252 return $this->clientSecret;
1253 }
1254
1255 /**
1256 * Sets a shared symmetric secret issued by the authorization server, which
1257 * is used to authenticate the client.
1258 *
1259 * @param string $clientSecret
1260 * @return void
1261 */
1262 public function setClientSecret($clientSecret)
1263 {
1264 $this->clientSecret = $clientSecret;
1265 }
1266
1267 /**
1268 * Gets the Issuer ID when using assertion profile.
1269 *
1270 * @return ?string
1271 */
1272 public function getIssuer()
1273 {
1274 return $this->issuer;
1275 }
1276
1277 /**
1278 * Sets the Issuer ID when using assertion profile.
1279 *
1280 * @param string $issuer
1281 * @return void
1282 */
1283 public function setIssuer($issuer)
1284 {
1285 $this->issuer = $issuer;
1286 }
1287
1288 /**
1289 * Gets the target sub when issuing assertions.
1290 *
1291 * @return ?string
1292 */
1293 public function getSub()
1294 {
1295 return $this->sub;
1296 }
1297
1298 /**
1299 * Sets the target sub when issuing assertions.
1300 *
1301 * @param string $sub
1302 * @return void
1303 */
1304 public function setSub($sub)
1305 {
1306 $this->sub = $sub;
1307 }
1308
1309 /**
1310 * Gets the target audience when issuing assertions.
1311 *
1312 * @return ?string
1313 */
1314 public function getAudience()
1315 {
1316 return $this->audience;
1317 }
1318
1319 /**
1320 * Sets the target audience when issuing assertions.
1321 *
1322 * @param string $audience
1323 * @return void
1324 */
1325 public function setAudience($audience)
1326 {
1327 $this->audience = $audience;
1328 }
1329
1330 /**
1331 * Gets the signing key when using an assertion profile.
1332 *
1333 * @return ?string
1334 */
1335 public function getSigningKey()
1336 {
1337 return $this->signingKey;
1338 }
1339
1340 /**
1341 * Sets the signing key when using an assertion profile.
1342 *
1343 * @param string $signingKey
1344 * @return void
1345 */
1346 public function setSigningKey($signingKey)
1347 {
1348 $this->signingKey = $signingKey;
1349 }
1350
1351 /**
1352 * Gets the signing key id when using an assertion profile.
1353 *
1354 * @return ?string
1355 */
1356 public function getSigningKeyId()
1357 {
1358 return $this->signingKeyId;
1359 }
1360
1361 /**
1362 * Sets the signing key id when using an assertion profile.
1363 *
1364 * @param string $signingKeyId
1365 * @return void
1366 */
1367 public function setSigningKeyId($signingKeyId)
1368 {
1369 $this->signingKeyId = $signingKeyId;
1370 }
1371
1372 /**
1373 * Gets the signing algorithm when using an assertion profile.
1374 *
1375 * @return ?string
1376 */
1377 public function getSigningAlgorithm()
1378 {
1379 return $this->signingAlgorithm;
1380 }
1381
1382 /**
1383 * Sets the signing algorithm when using an assertion profile.
1384 *
1385 * @param ?string $signingAlgorithm
1386 * @return void
1387 */
1388 public function setSigningAlgorithm($signingAlgorithm)
1389 {
1390 if (is_null($signingAlgorithm)) {
1391 $this->signingAlgorithm = null;
1392 } elseif (!in_array($signingAlgorithm, self::$knownSigningAlgorithms)) {
1393 throw new InvalidArgumentException('unknown signing algorithm');
1394 } else {
1395 $this->signingAlgorithm = $signingAlgorithm;
1396 }
1397 }
1398
1399 /**
1400 * Gets the set of parameters used by extension when using an extension
1401 * grant type.
1402 *
1403 * @return array<mixed>
1404 */
1405 public function getExtensionParams()
1406 {
1407 return $this->extensionParams;
1408 }
1409
1410 /**
1411 * Sets the set of parameters used by extension when using an extension
1412 * grant type.
1413 *
1414 * @param array<mixed> $extensionParams
1415 * @return void
1416 */
1417 public function setExtensionParams($extensionParams)
1418 {
1419 $this->extensionParams = $extensionParams;
1420 }
1421
1422 /**
1423 * Gets the number of seconds assertions are valid for.
1424 *
1425 * @return int
1426 */
1427 public function getExpiry()
1428 {
1429 return $this->expiry;
1430 }
1431
1432 /**
1433 * Sets the number of seconds assertions are valid for.
1434 *
1435 * @param int $expiry
1436 * @return void
1437 */
1438 public function setExpiry($expiry)
1439 {
1440 $this->expiry = $expiry;
1441 }
1442
1443 /**
1444 * Gets the lifetime of the access token in seconds.
1445 *
1446 * @return int
1447 */
1448 public function getExpiresIn()
1449 {
1450 return $this->expiresIn;
1451 }
1452
1453 /**
1454 * Sets the lifetime of the access token in seconds.
1455 *
1456 * @param ?int $expiresIn
1457 * @return void
1458 */
1459 public function setExpiresIn($expiresIn)
1460 {
1461 if (is_null($expiresIn)) {
1462 $this->expiresIn = null;
1463 $this->issuedAt = null;
1464 } else {
1465 $this->issuedAt = time();
1466 $this->expiresIn = (int) $expiresIn;
1467 }
1468 }
1469
1470 /**
1471 * Gets the time the current access token expires at.
1472 *
1473 * @return ?int
1474 */
1475 public function getExpiresAt()
1476 {
1477 if (!is_null($this->expiresAt)) {
1478 return $this->expiresAt;
1479 }
1480
1481 if (!is_null($this->issuedAt) && !is_null($this->expiresIn)) {
1482 return $this->issuedAt + $this->expiresIn;
1483 }
1484
1485 return null;
1486 }
1487
1488 /**
1489 * Returns true if the acccess token has expired.
1490 *
1491 * @return bool
1492 */
1493 public function isExpired()
1494 {
1495 $expiration = $this->getExpiresAt();
1496 $now = time();
1497
1498 return !is_null($expiration) && $now >= $expiration;
1499 }
1500
1501 /**
1502 * Sets the time the current access token expires at.
1503 *
1504 * @param int $expiresAt
1505 * @return void
1506 */
1507 public function setExpiresAt($expiresAt)
1508 {
1509 $this->expiresAt = $expiresAt;
1510 }
1511
1512 /**
1513 * Gets the time the current access token was issued at.
1514 *
1515 * @return ?int
1516 */
1517 public function getIssuedAt()
1518 {
1519 return $this->issuedAt;
1520 }
1521
1522 /**
1523 * Sets the time the current access token was issued at.
1524 *
1525 * @param int $issuedAt
1526 * @return void
1527 */
1528 public function setIssuedAt($issuedAt)
1529 {
1530 $this->issuedAt = $issuedAt;
1531 }
1532
1533 /**
1534 * Gets the current access token.
1535 *
1536 * @return ?string
1537 */
1538 public function getAccessToken()
1539 {
1540 return $this->accessToken;
1541 }
1542
1543 /**
1544 * Sets the current access token.
1545 *
1546 * @param string $accessToken
1547 * @return void
1548 */
1549 public function setAccessToken($accessToken)
1550 {
1551 $this->accessToken = $accessToken;
1552 }
1553
1554 /**
1555 * Gets the current ID token.
1556 *
1557 * @return ?string
1558 */
1559 public function getIdToken()
1560 {
1561 return $this->idToken;
1562 }
1563
1564 /**
1565 * Sets the current ID token.
1566 *
1567 * @param string $idToken
1568 * @return void
1569 */
1570 public function setIdToken($idToken)
1571 {
1572 $this->idToken = $idToken;
1573 }
1574
1575 /**
1576 * Get the granted space-separated scopes (if they exist) for the last
1577 * fetched token.
1578 *
1579 * @return string|null
1580 */
1581 public function getGrantedScope()
1582 {
1583 return $this->grantedScope;
1584 }
1585
1586 /**
1587 * Sets the current ID token.
1588 *
1589 * @param string $grantedScope
1590 * @return void
1591 */
1592 public function setGrantedScope($grantedScope)
1593 {
1594 $this->grantedScope = $grantedScope;
1595 }
1596
1597 /**
1598 * Gets the refresh token associated with the current access token.
1599 *
1600 * @return ?string
1601 */
1602 public function getRefreshToken()
1603 {
1604 return $this->refreshToken;
1605 }
1606
1607 /**
1608 * Sets the refresh token associated with the current access token.
1609 *
1610 * @param string $refreshToken
1611 * @return void
1612 */
1613 public function setRefreshToken($refreshToken)
1614 {
1615 $this->refreshToken = $refreshToken;
1616 }
1617
1618 /**
1619 * Sets additional claims to be included in the JWT token
1620 *
1621 * @param array<mixed> $additionalClaims
1622 * @return void
1623 */
1624 public function setAdditionalClaims(array $additionalClaims)
1625 {
1626 $this->additionalClaims = $additionalClaims;
1627 }
1628
1629 /**
1630 * Gets the additional claims to be included in the JWT token.
1631 *
1632 * @return array<mixed>
1633 */
1634 public function getAdditionalClaims()
1635 {
1636 return $this->additionalClaims;
1637 }
1638
1639 /**
1640 * Gets the additional claims to be included in the JWT token.
1641 *
1642 * @return ?string
1643 */
1644 public function getIssuedTokenType()
1645 {
1646 return $this->issuedTokenType;
1647 }
1648
1649 /**
1650 * The expiration of the last received token.
1651 *
1652 * @return array<mixed>|null
1653 */
1654 public function getLastReceivedToken()
1655 {
1656 if ($token = $this->getAccessToken()) {
1657 // the bare necessity of an auth token
1658 $authToken = [
1659 'access_token' => $token,
1660 'expires_at' => $this->getExpiresAt(),
1661 ];
1662 } elseif ($idToken = $this->getIdToken()) {
1663 $authToken = [
1664 'id_token' => $idToken,
1665 'expires_at' => $this->getExpiresAt(),
1666 ];
1667 } else {
1668 return null;
1669 }
1670
1671 if ($expiresIn = $this->getExpiresIn()) {
1672 $authToken['expires_in'] = $expiresIn;
1673 }
1674 if ($issuedAt = $this->getIssuedAt()) {
1675 $authToken['issued_at'] = $issuedAt;
1676 }
1677 if ($refreshToken = $this->getRefreshToken()) {
1678 $authToken['refresh_token'] = $refreshToken;
1679 }
1680
1681 return $authToken;
1682 }
1683
1684 /**
1685 * Get the client ID.
1686 *
1687 * Alias of {@see Google\Auth\OAuth2::getClientId()}.
1688 *
1689 * @param callable|null $httpHandler
1690 * @return string
1691 * @access private
1692 */
1693 public function getClientName(?callable $httpHandler = null)
1694 {
1695 return $this->getClientId();
1696 }
1697
1698 /**
1699 * @todo handle uri as array
1700 *
1701 * @param ?string $uri
1702 * @return null|UriInterface
1703 */
1704 private function coerceUri($uri)
1705 {
1706 if (is_null($uri)) {
1707 return null;
1708 }
1709
1710 return Utils::uriFor($uri);
1711 }
1712
1713 /**
1714 * @param string $idToken
1715 * @param Key|Key[]|string|string[] $publicKey
1716 * @param string|string[] $allowedAlgs
1717 * @return object
1718 */
1719 private function jwtDecode($idToken, $publicKey, $allowedAlgs)
1720 {
1721 $keys = $this->getFirebaseJwtKeys($publicKey, $allowedAlgs);
1722
1723 // Default exception if none are caught. We are using the same exception
1724 // class and message from firebase/php-jwt to preserve backwards
1725 // compatibility.
1726 $e = new \InvalidArgumentException('Key may not be empty');
1727 foreach ($keys as $key) {
1728 try {
1729 return JWT::decode($idToken, $key);
1730 } catch (\Exception $e) {
1731 // try next alg
1732 }
1733 }
1734 throw $e;
1735 }
1736
1737 /**
1738 * @param Key|Key[]|string|string[] $publicKey
1739 * @param string|string[] $allowedAlgs
1740 * @return Key[]
1741 */
1742 private function getFirebaseJwtKeys($publicKey, $allowedAlgs)
1743 {
1744 // If $publicKey is instance of Key, return it
1745 if ($publicKey instanceof Key) {
1746 return [$publicKey];
1747 }
1748
1749 // If $allowedAlgs is empty, $publicKey must be Key or Key[].
1750 if (empty($allowedAlgs)) {
1751 $keys = [];
1752 foreach ((array) $publicKey as $kid => $pubKey) {
1753 if (!$pubKey instanceof Key) {
1754 throw new \InvalidArgumentException(sprintf(
1755 'When allowed algorithms is empty, the public key must'
1756 . 'be an instance of %s or an array of %s objects',
1757 Key::class,
1758 Key::class
1759 ));
1760 }
1761 $keys[$kid] = $pubKey;
1762 }
1763 return $keys;
1764 }
1765
1766 $allowedAlg = null;
1767 if (is_string($allowedAlgs)) {
1768 $allowedAlg = $allowedAlgs;
1769 } elseif (is_array($allowedAlgs)) {
1770 if (count($allowedAlgs) > 1) {
1771 throw new \InvalidArgumentException(
1772 'To have multiple allowed algorithms, You must provide an'
1773 . ' array of Firebase\JWT\Key objects.'
1774 . ' See https://github.com/firebase/php-jwt for more information.'
1775 );
1776 }
1777 $allowedAlg = array_pop($allowedAlgs);
1778 } else {
1779 throw new \InvalidArgumentException('allowed algorithms must be a string or array.');
1780 }
1781
1782 if (is_array($publicKey)) {
1783 // When publicKey is greater than 1, create keys with the single alg.
1784 $keys = [];
1785 foreach ($publicKey as $kid => $pubKey) {
1786 if ($pubKey instanceof Key) {
1787 $keys[$kid] = $pubKey;
1788 } else {
1789 $keys[$kid] = new Key($pubKey, $allowedAlg);
1790 }
1791 }
1792 return $keys;
1793 }
1794
1795 return [new Key($publicKey, $allowedAlg)];
1796 }
1797
1798 /**
1799 * Determines if the URI is absolute based on its scheme and host or path
1800 * (RFC 3986).
1801 *
1802 * @param string $uri
1803 * @return bool
1804 */
1805 private function isAbsoluteUri($uri)
1806 {
1807 $uri = $this->coerceUri($uri);
1808
1809 return $uri->getScheme() && ($uri->getHost() || $uri->getPath());
1810 }
1811
1812 /**
1813 * @param array<mixed> $params
1814 * @return array<mixed>
1815 */
1816 private function addClientCredentials(&$params)
1817 {
1818 $clientId = $this->getClientId();
1819 $clientSecret = $this->getClientSecret();
1820
1821 if ($clientId && $clientSecret) {
1822 $params['client_id'] = $clientId;
1823 $params['client_secret'] = $clientSecret;
1824 }
1825
1826 return $params;
1827 }
1828}
Note: See TracBrowser for help on using the repository browser.