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