File modules/openid_connect/OpenIDConnectClient.class.php

Last commit: Sat Oct 31 03:48:03 2020 +0100	Jan Dankert	Some bad fixes for OIDC to work properly.
1 <?php 2 /** 3 * 4 * Copyright MITRE 2020 5 * 6 * OpenIDConnectClient for PHP5 7 * Author: Michael Jett <mjett@mitre.org> 8 * 9 * Licensed under the Apache License, Version 2.0 (the "License"); you may 10 * not use this file except in compliance with the License. You may obtain 11 * a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, software 16 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 17 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 18 * License for the specific language governing permissions and limitations 19 * under the License. 20 * 21 */ 22 23 namespace openid_connect; 24 25 /** 26 * 27 * JWT signature verification support by Jonathan Reed <jdreed@mit.edu> 28 * Licensed under the same license as the rest of this file. 29 * 30 * phpseclib is required to validate the signatures of some tokens. 31 * It can be downloaded from: http://phpseclib.sourceforge.net/ 32 */ 33 34 if (!class_exists('\phpseclib\Crypt\RSA') && !class_exists('Crypt_RSA')) { 35 user_error('Unable to find phpseclib Crypt/RSA.php. Ensure phpseclib is installed and in include_path before you include this file'); 36 } 37 38 /** 39 * A wrapper around base64_decode which decodes Base64URL-encoded data, 40 * which is not the same alphabet as base64. 41 * @param string $base64url 42 * @return bool|string 43 */ 44 function base64url_decode($base64url) { 45 return base64_decode(b64url2b64($base64url)); 46 } 47 48 /** 49 * Per RFC4648, "base64 encoding with URL-safe and filename-safe 50 * alphabet". This just replaces characters 62 and 63. None of the 51 * reference implementations seem to restore the padding if necessary, 52 * but we'll do it anyway. 53 * @param string $base64url 54 * @return string 55 */ 56 function b64url2b64($base64url) { 57 // "Shouldn't" be necessary, but why not 58 $padding = strlen($base64url) % 4; 59 if ($padding > 0) { 60 $base64url .= str_repeat('=', 4 - $padding); 61 } 62 return strtr($base64url, '-_', '+/'); 63 } 64 65 66 /** 67 * OpenIDConnect Exception Class 68 */ 69 class OpenIDConnectClientException extends \Exception 70 { 71 72 } 73 74 /** 75 * Require the CURL and JSON PHP extensions to be installed 76 */ 77 if (!function_exists('curl_init')) { 78 throw new OpenIDConnectClientException('OpenIDConnect needs the CURL PHP extension.'); 79 } 80 if (!function_exists('json_decode')) { 81 throw new OpenIDConnectClientException('OpenIDConnect needs the JSON PHP extension.'); 82 } 83 84 /** 85 * 86 * Please note this class stores nonces by default in $_SESSION['openid_connect_nonce'] 87 * 88 */ 89 class OpenIDConnectClient 90 { 91 92 /** 93 * @var string arbitrary id value 94 */ 95 private $clientID; 96 97 /** 98 * @var string arbitrary name value 99 */ 100 private $clientName; 101 102 /** 103 * @var string arbitrary secret value 104 */ 105 private $clientSecret; 106 107 /** 108 * @var array holds the provider configuration 109 */ 110 private $providerConfig = array(); 111 112 /** 113 * @var string http proxy if necessary 114 */ 115 private $httpProxy; 116 117 /** 118 * @var string full system path to the SSL certificate 119 */ 120 private $certPath; 121 122 /** 123 * @var bool Verify SSL peer on transactions 124 */ 125 private $verifyPeer = true; 126 127 /** 128 * @var bool Verify peer hostname on transactions 129 */ 130 private $verifyHost = true; 131 132 /** 133 * @var string if we acquire an access token it will be stored here 134 */ 135 protected $accessToken; 136 137 /** 138 * @var string if we acquire a refresh token it will be stored here 139 */ 140 private $refreshToken; 141 142 /** 143 * @var string if we acquire an id token it will be stored here 144 */ 145 protected $idToken; 146 147 /** 148 * @var string stores the token response 149 */ 150 private $tokenResponse; 151 152 /** 153 * @var array holds scopes 154 */ 155 private $scopes = array(); 156 157 /** 158 * @var int|null Response code from the server 159 */ 160 private $responseCode; 161 162 /** 163 * @var array holds response types 164 */ 165 private $responseTypes = array(); 166 167 /** 168 * @var array holds a cache of info returned from the user info endpoint 169 */ 170 private $userInfo = array(); 171 172 /** 173 * @var array holds authentication parameters 174 */ 175 private $authParams = array(); 176 177 /** 178 * @var array holds additional registration parameters for example post_logout_redirect_uris 179 */ 180 private $registrationParams = array(); 181 182 /** 183 * @var mixed holds well-known openid server properties 184 */ 185 private $wellKnown = false; 186 187 /** 188 * @var mixed holds well-known opendid configuration parameters, like policy for MS Azure AD B2C User Flow 189 * @see https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview 190 */ 191 private $wellKnownConfigParameters = array(); 192 193 /** 194 * @var int timeout (seconds) 195 */ 196 protected $timeOut = 60; 197 198 /** 199 * @var int leeway (seconds) 200 */ 201 private $leeway = 300; 202 203 /** 204 * @var array holds response types 205 */ 206 private $additionalJwks = array(); 207 208 /** 209 * @var array holds verified jwt claims 210 */ 211 protected $verifiedClaims = array(); 212 213 /** 214 * @var callable validator function for issuer claim 215 */ 216 private $issuerValidator; 217 218 /** 219 * @var bool Allow OAuth 2 implicit flow; see http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth 220 */ 221 private $allowImplicitFlow = false; 222 /** 223 * @var string 224 */ 225 private $redirectURL; 226 227 protected $enc_type = PHP_QUERY_RFC1738; 228 229 /** 230 * @param $provider_url string optional 231 * 232 * @param $client_id string optional 233 * @param $client_secret string optional 234 * @param null $issuer 235 */ 236 public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null) { 237 $this->setProviderURL($provider_url); 238 if ($issuer === null) { 239 $this->setIssuer($provider_url); 240 } else { 241 $this->setIssuer($issuer); 242 } 243 244 $this->clientID = $client_id; 245 $this->clientSecret = $client_secret; 246 247 $this->issuerValidator = function($iss){ 248 return ($iss === $this->getIssuer() || $iss === $this->getWellKnownIssuer() || $iss === $this->getWellKnownIssuer(true)); 249 }; 250 } 251 252 /** 253 * @param $provider_url 254 */ 255 public function setProviderURL($provider_url) { 256 $this->providerConfig['providerUrl'] = $provider_url; 257 } 258 259 /** 260 * @param $issuer 261 */ 262 public function setIssuer($issuer) { 263 $this->providerConfig['issuer'] = $issuer; 264 } 265 266 /** 267 * @param $response_types 268 */ 269 public function setResponseTypes($response_types) { 270 $this->responseTypes = array_merge($this->responseTypes, (array)$response_types); 271 } 272 273 /** 274 * @return bool 275 * @throws OpenIDConnectClientException 276 */ 277 public function authenticate() { 278 279 // Do a preemptive check to see if the provider has thrown an error from a previous redirect 280 if (isset($_REQUEST['error'])) { 281 $desc = isset($_REQUEST['error_description']) ? ' Description: ' . $_REQUEST['error_description'] : ''; 282 throw new OpenIDConnectClientException('Error: ' . $_REQUEST['error'] .$desc); 283 } 284 285 // If we have an authorization code then proceed to request a token 286 if (isset($_REQUEST['code'])) { 287 288 $code = $_REQUEST['code']; 289 $token_json = $this->requestTokens($code); 290 291 // Throw an error if the server returns one 292 if (isset($token_json->error)) { 293 if (isset($token_json->error_description)) { 294 throw new OpenIDConnectClientException($token_json->error_description); 295 } 296 throw new OpenIDConnectClientException('Got response: ' . $token_json->error); 297 } 298 299 // Do an OpenID Connect session check 300 if ($_REQUEST['state'] !== $this->getState()) { 301 throw new OpenIDConnectClientException('Unable to determine state'); 302 } 303 304 // Cleanup state 305 $this->unsetState(); 306 307 if (!property_exists($token_json, 'id_token')) { 308 throw new OpenIDConnectClientException('User did not authorize openid scope.'); 309 } 310 311 $claims = $this->decodeJWT($token_json->id_token, 1); 312 313 // Verify the signature 314 if ($this->canVerifySignatures()) { 315 if (!$this->getProviderConfigValue('jwks_uri')) { 316 throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); 317 } 318 if (!$this->verifyJWTsignature($token_json->id_token)) { 319 throw new OpenIDConnectClientException ('Unable to verify signature'); 320 } 321 } else { 322 user_error('Warning: JWT signature verification unavailable.'); 323 } 324 325 // Save the id token 326 $this->idToken = $token_json->id_token; 327 328 // Save the access token 329 $this->accessToken = $token_json->access_token; 330 331 // If this is a valid claim 332 if ($this->verifyJWTclaims($claims, $token_json->access_token)) { 333 334 // Clean up the session a little 335 $this->unsetNonce(); 336 337 // Save the full response 338 $this->tokenResponse = $token_json; 339 340 // Save the verified claims 341 $this->verifiedClaims = $claims; 342 343 // Save the refresh token, if we got one 344 if (isset($token_json->refresh_token)) { 345 $this->refreshToken = $token_json->refresh_token; 346 } 347 348 // Success! 349 return true; 350 351 } 352 353 throw new OpenIDConnectClientException ('Unable to verify JWT claims'); 354 } 355 356 if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { 357 // if we have no code but an id_token use that 358 $id_token = $_REQUEST['id_token']; 359 360 $accessToken = null; 361 if (isset($_REQUEST['access_token'])) { 362 $accessToken = $_REQUEST['access_token']; 363 } 364 365 // Do an OpenID Connect session check 366 if ($_REQUEST['state'] !== $this->getState()) { 367 throw new OpenIDConnectClientException('Unable to determine state'); 368 } 369 370 // Cleanup state 371 $this->unsetState(); 372 373 $claims = $this->decodeJWT($id_token, 1); 374 375 // Verify the signature 376 if ($this->canVerifySignatures()) { 377 if (!$this->getProviderConfigValue('jwks_uri')) { 378 throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); 379 } 380 if (!$this->verifyJWTsignature($id_token)) { 381 throw new OpenIDConnectClientException ('Unable to verify signature'); 382 } 383 } else { 384 user_error('Warning: JWT signature verification unavailable.'); 385 } 386 387 // Save the id token 388 $this->idToken = $id_token; 389 390 // If this is a valid claim 391 if ($this->verifyJWTclaims($claims, $accessToken)) { 392 393 // Clean up the session a little 394 $this->unsetNonce(); 395 396 // Save the verified claims 397 $this->verifiedClaims = $claims; 398 399 // Save the access token 400 if ($accessToken) { 401 $this->accessToken = $accessToken; 402 } 403 404 // Success! 405 return true; 406 407 } 408 409 throw new OpenIDConnectClientException ('Unable to verify JWT claims'); 410 } 411 412 $this->requestAuthorization(); 413 return false; 414 415 } 416 417 /** 418 * It calls the end-session endpoint of the OpenID Connect provider to notify the OpenID 419 * Connect provider that the end-user has logged out of the relying party site 420 * (the client application). 421 * 422 * @param string $accessToken ID token (obtained at login) 423 * @param string|null $redirect URL to which the RP is requesting that the End-User's User Agent 424 * be redirected after a logout has been performed. The value MUST have been previously 425 * registered with the OP. Value can be null. 426 * 427 * @throws OpenIDConnectClientException 428 */ 429 public function signOut($accessToken, $redirect) { 430 $signout_endpoint = $this->getProviderConfigValue('end_session_endpoint'); 431 432 $signout_params = null; 433 if($redirect === null){ 434 $signout_params = array('id_token_hint' => $accessToken); 435 } 436 else { 437 $signout_params = array( 438 'id_token_hint' => $accessToken, 439 'post_logout_redirect_uri' => $redirect); 440 } 441 442 $signout_endpoint .= (strpos($signout_endpoint, '?') === false ? '?' : '&') . http_build_query( $signout_params, null, '&', $this->enc_type); 443 $this->redirect($signout_endpoint); 444 } 445 446 /** 447 * @param array $scope - example: openid, given_name, etc... 448 */ 449 public function addScope($scope) { 450 $this->scopes = array_merge($this->scopes, (array)$scope); 451 } 452 453 /** 454 * @param array $param - example: prompt=login 455 */ 456 public function addAuthParam($param) { 457 $this->authParams = array_merge($this->authParams, (array)$param); 458 } 459 460 /** 461 * @param array $param - example: post_logout_redirect_uris=[http://example.com/successful-logout] 462 */ 463 public function addRegistrationParam($param) { 464 $this->registrationParams = array_merge($this->registrationParams, (array)$param); 465 } 466 467 /** 468 * @param $jwk object - example: (object) array('kid' => ..., 'nbf' => ..., 'use' => 'sig', 'kty' => "RSA", 'e' => "", 'n' => "") 469 */ 470 protected function addAdditionalJwk($jwk) { 471 $this->additionalJwks[] = $jwk; 472 } 473 474 /** 475 * Get's anything that we need configuration wise including endpoints, and other values 476 * 477 * @param string $param 478 * @param string $default optional 479 * @throws OpenIDConnectClientException 480 * @return string 481 * 482 */ 483 protected function getProviderConfigValue($param, $default = null) { 484 485 // If the configuration value is not available, attempt to fetch it from a well known config endpoint 486 // This is also known as auto "discovery" 487 if (!isset($this->providerConfig[$param])) { 488 $this->providerConfig[$param] = $this->getWellKnownConfigValue($param, $default); 489 } 490 491 return $this->providerConfig[$param]; 492 } 493 494 /** 495 * Get's anything that we need configuration wise including endpoints, and other values 496 * 497 * @param string $param 498 * @param string $default optional 499 * @throws OpenIDConnectClientException 500 * @return string 501 * 502 */ 503 private function getWellKnownConfigValue($param, $default = null) { 504 505 // If the configuration value is not available, attempt to fetch it from a well known config endpoint 506 // This is also known as auto "discovery" 507 if(!$this->wellKnown) { 508 $well_known_config_url = rtrim($this->getProviderURL(), '/') . '/.well-known/openid-configuration'; 509 if (count($this->wellKnownConfigParameters) > 0){ 510 $well_known_config_url .= '?' . http_build_query($this->wellKnownConfigParameters) ; 511 } 512 $this->wellKnown = json_decode($this->fetchURL($well_known_config_url)); 513 } 514 515 $value = false; 516 if(isset($this->wellKnown->{$param})){ 517 $value = $this->wellKnown->{$param}; 518 } 519 520 if ($value) { 521 return $value; 522 } 523 524 if (isset($default)) { 525 // Uses default value if provided 526 return $default; 527 } 528 529 throw new OpenIDConnectClientException("The provider {$param} could not be fetched. Make sure your provider has a well known configuration available."); 530 } 531 532 /** 533 * Set optionnal parameters for .well-known/openid-configuration 534 * 535 * @param string $param 536 * 537 */ 538 public function setWellKnownConfigParameters(array $params = []){ 539 $this->wellKnownConfigParameters=$params; 540 } 541 542 543 /** 544 * @param string $url Sets redirect URL for auth flow 545 */ 546 public function setRedirectURL ($url) { 547 if (parse_url($url,PHP_URL_HOST) !== false) { 548 $this->redirectURL = $url; 549 } 550 } 551 552 /** 553 * Gets the URL of the current page we are on, encodes, and returns it 554 * 555 * @return string 556 */ 557 public function getRedirectURL() { 558 559 // If the redirect URL has been set then return it. 560 if (property_exists($this, 'redirectURL') && $this->redirectURL) { 561 return $this->redirectURL; 562 } 563 564 // Other-wise return the URL of the current page 565 566 /** 567 * Thank you 568 * http://stackoverflow.com/questions/189113/how-do-i-get-current-page-full-url-in-php-on-a-windows-iis-server 569 */ 570 571 /* 572 * Compatibility with multiple host headers. 573 * The problem with SSL over port 80 is resolved and non-SSL over port 443. 574 * Support of 'ProxyReverse' configurations. 575 */ 576 577 if (isset($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS']) && ($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS'] === '1')) { 578 $protocol = 'https'; 579 } else { 580 $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO'] 581 ?: @$_SERVER['REQUEST_SCHEME'] 582 ?: ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http'); 583 } 584 585 $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT']) 586 ?: @intval($_SERVER['SERVER_PORT']) 587 ?: (($protocol === 'https') ? 443 : 80); 588 589 $host = @explode(':', $_SERVER['HTTP_HOST'])[0] 590 ?: @$_SERVER['SERVER_NAME'] 591 ?: @$_SERVER['SERVER_ADDR']; 592 593 $port = (443 === $port) || (80 === $port) ? '' : ':' . $port; 594 595 return sprintf('%s://%s%s/%s', $protocol, $host, $port, @trim(reset(explode('?', $_SERVER['REQUEST_URI'])), '/')); 596 } 597 598 /** 599 * Used for arbitrary value generation for nonces and state 600 * 601 * @return string 602 */ 603 protected function generateRandString() { 604 return md5(uniqid(rand(), TRUE)); 605 } 606 607 /** 608 * Start Here 609 * @return void 610 * @throws OpenIDConnectClientException 611 */ 612 private function requestAuthorization() { 613 614 $auth_endpoint = $this->getProviderConfigValue('authorization_endpoint'); 615 $response_type = 'code'; 616 617 // Generate and store a nonce in the session 618 // The nonce is an arbitrary value 619 $nonce = $this->setNonce($this->generateRandString()); 620 621 // State essentially acts as a session key for OIDC 622 $state = $this->setState($this->generateRandString()); 623 624 $auth_params = array_merge($this->authParams, array( 625 'response_type' => $response_type, 626 'redirect_uri' => $this->getRedirectURL(), 627 'client_id' => $this->clientID, 628 'nonce' => $nonce, 629 'state' => $state, 630 'scope' => 'openid' 631 )); 632 633 // If the client has been registered with additional scopes 634 if (count($this->scopes) > 0) { 635 $auth_params = array_merge($auth_params, array('scope' => implode(' ', array_merge($this->scopes, array('openid'))))); 636 } 637 638 // If the client has been registered with additional response types 639 if (count($this->responseTypes) > 0) { 640 $auth_params = array_merge($auth_params, array('response_type' => implode(' ', $this->responseTypes))); 641 } 642 643 $auth_endpoint .= (strpos($auth_endpoint, '?') === false ? '?' : '&') . http_build_query($auth_params, null, '&', $this->enc_type); 644 645 $this->commitSession(); 646 $this->redirect($auth_endpoint); 647 } 648 649 /** 650 * Requests a client credentials token 651 * 652 * @throws OpenIDConnectClientException 653 */ 654 public function requestClientCredentialsToken() { 655 $token_endpoint = $this->getProviderConfigValue('token_endpoint'); 656 657 $headers = []; 658 659 $grant_type = 'client_credentials'; 660 661 $post_data = array( 662 'grant_type' => $grant_type, 663 'client_id' => $this->clientID, 664 'client_secret' => $this->clientSecret, 665 'scope' => implode(' ', $this->scopes) 666 ); 667 668 // Convert token params to string format 669 $post_params = http_build_query($post_data, null, '&', $this->enc_type); 670 671 return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); 672 } 673 674 675 /** 676 * Requests a resource owner token 677 * (Defined in https://tools.ietf.org/html/rfc6749#section-4.3) 678 * 679 * @param boolean $bClientAuth Indicates that the Client ID and Secret be used for client authentication 680 * @return mixed 681 * @throws OpenIDConnectClientException 682 */ 683 public function requestResourceOwnerToken($bClientAuth = FALSE) { 684 $token_endpoint = $this->getProviderConfigValue('token_endpoint'); 685 686 $headers = []; 687 688 $grant_type = 'password'; 689 690 $post_data = array( 691 'grant_type' => $grant_type, 692 'username' => $this->authParams['username'], 693 'password' => $this->authParams['password'], 694 'scope' => implode(' ', $this->scopes) 695 ); 696 697 //For client authentication include the client values 698 if($bClientAuth) { 699 $post_data['client_id'] = $this->clientID; 700 $post_data['client_secret'] = $this->clientSecret; 701 } 702 703 // Convert token params to string format 704 $post_params = http_build_query($post_data, null, '&', $this->enc_type); 705 706 return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); 707 } 708 709 710 /** 711 * Requests ID and Access tokens 712 * 713 * @param string $code 714 * @return mixed 715 * @throws OpenIDConnectClientException 716 */ 717 protected function requestTokens($code) { 718 $token_endpoint = $this->getProviderConfigValue('token_endpoint'); 719 $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); 720 721 $headers = []; 722 723 $grant_type = 'authorization_code'; 724 725 $token_params = array( 726 'grant_type' => $grant_type, 727 'code' => $code, 728 'redirect_uri' => $this->getRedirectURL(), 729 'client_id' => $this->clientID, 730 'client_secret' => $this->clientSecret 731 ); 732 733 # Consider Basic authentication if provider config is set this way 734 if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { 735 $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; 736 unset($token_params['client_secret']); 737 unset($token_params['client_id']); 738 } 739 740 // Convert token params to string format 741 $token_params = http_build_query($token_params, null, '&', $this->enc_type); 742 743 $this->tokenResponse = json_decode($this->fetchURL($token_endpoint, $token_params, $headers)); 744 745 return $this->tokenResponse; 746 } 747 748 /** 749 * Requests Access token with refresh token 750 * 751 * @param string $refresh_token 752 * @return mixed 753 * @throws OpenIDConnectClientException 754 */ 755 public function refreshToken($refresh_token) { 756 $token_endpoint = $this->getProviderConfigValue('token_endpoint'); 757 758 $grant_type = 'refresh_token'; 759 760 $token_params = array( 761 'grant_type' => $grant_type, 762 'refresh_token' => $refresh_token, 763 'client_id' => $this->clientID, 764 'client_secret' => $this->clientSecret, 765 ); 766 767 // Convert token params to string format 768 $token_params = http_build_query($token_params, null, '&', $this->enc_type); 769 770 $json = json_decode($this->fetchURL($token_endpoint, $token_params)); 771 772 if (isset($json->access_token)) { 773 $this->accessToken = $json->access_token; 774 } 775 776 if (isset($json->refresh_token)) { 777 $this->refreshToken = $json->refresh_token; 778 } 779 780 return $json; 781 } 782 783 /** 784 * @param array $keys 785 * @param array $header 786 * @throws OpenIDConnectClientException 787 * @return object 788 */ 789 private function get_key_for_header($keys, $header) { 790 foreach ($keys as $key) { 791 if ($key->kty === 'RSA') { 792 if (!isset($header->kid) || $key->kid === $header->kid) { 793 return $key; 794 } 795 } else { 796 if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { 797 return $key; 798 } 799 } 800 } 801 if ($this->additionalJwks) { 802 foreach ($this->additionalJwks as $key) { 803 if ($key->kty === 'RSA') { 804 if (!isset($header->kid) || $key->kid === $header->kid) { 805 return $key; 806 } 807 } else { 808 if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { 809 return $key; 810 } 811 } 812 } 813 } 814 if (isset($header->kid)) { 815 throw new OpenIDConnectClientException('Unable to find a key for (algorithm, kid):' . $header->alg . ', ' . $header->kid . ')'); 816 } 817 818 throw new OpenIDConnectClientException('Unable to find a key for RSA'); 819 } 820 821 822 /** 823 * @param string $hashtype 824 * @param object $key 825 * @param $payload 826 * @param $signature 827 * @param $signatureType 828 * @return bool 829 * @throws OpenIDConnectClientException 830 */ 831 private function verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $signatureType) { 832 if (!class_exists('\phpseclib\Crypt\RSA') && !class_exists('Crypt_RSA')) { 833 throw new OpenIDConnectClientException('Crypt_RSA support unavailable.'); 834 } 835 if (!(property_exists($key, 'n') && property_exists($key, 'e'))) { 836 throw new OpenIDConnectClientException('Malformed key object'); 837 } 838 839 /* We already have base64url-encoded data, so re-encode it as 840 regular base64 and use the XML key format for simplicity. 841 */ 842 $public_key_xml = "<RSAKeyValue>\r\n". 843 ' <Modulus>' . b64url2b64($key->n) . "</Modulus>\r\n" . 844 ' <Exponent>' . b64url2b64($key->e) . "</Exponent>\r\n" . 845 '</RSAKeyValue>'; 846 if(class_exists('Crypt_RSA', false)) { 847 $rsa = new Crypt_RSA(); 848 $rsa->setHash($hashtype); 849 if ($signatureType === 'PSS') { 850 $rsa->setMGFHash($hashtype); 851 } 852 $rsa->loadKey($public_key_xml, Crypt_RSA::PUBLIC_FORMAT_XML); 853 $rsa->signatureMode = $signatureType === 'PSS' ? Crypt_RSA::SIGNATURE_PSS : Crypt_RSA::SIGNATURE_PKCS1; 854 } else { 855 $rsa = new \phpseclib\Crypt\RSA(); 856 $rsa->setHash($hashtype); 857 if ($signatureType === 'PSS') { 858 $rsa->setMGFHash($hashtype); 859 } 860 $rsa->loadKey($public_key_xml, \phpseclib\Crypt\RSA::PUBLIC_FORMAT_XML); 861 $rsa->signatureMode = $signatureType === 'PSS' ? \phpseclib\Crypt\RSA::SIGNATURE_PSS : \phpseclib\Crypt\RSA::SIGNATURE_PKCS1; 862 } 863 return $rsa->verify($payload, $signature); 864 } 865 866 /** 867 * @param string $hashtype 868 * @param object $key 869 * @param $payload 870 * @param $signature 871 * @return bool 872 * @throws OpenIDConnectClientException 873 */ 874 private function verifyHMACJWTsignature($hashtype, $key, $payload, $signature) 875 { 876 if (!function_exists('hash_hmac')) { 877 throw new OpenIDConnectClientException('hash_hmac support unavailable.'); 878 } 879 880 $expected=hash_hmac($hashtype, $payload, $key, true); 881 882 if (function_exists('hash_equals')) { 883 return hash_equals($signature, $expected); 884 } 885 886 return self::hashEquals($signature, $expected); 887 } 888 889 /** 890 * @param string $jwt encoded JWT 891 * @throws OpenIDConnectClientException 892 * @return bool 893 */ 894 public function verifyJWTsignature($jwt) { 895 if (!\is_string($jwt)) { 896 throw new OpenIDConnectClientException('Error token is not a string'); 897 } 898 $parts = explode('.', $jwt); 899 if (!isset($parts[0])) { 900 throw new OpenIDConnectClientException('Error missing part 0 in token'); 901 } 902 $signature = base64url_decode(array_pop($parts)); 903 if (false === $signature || '' === $signature) { 904 throw new OpenIDConnectClientException('Error decoding signature from token'); 905 } 906 $header = json_decode(base64url_decode($parts[0])); 907 if (null === $header || !\is_object($header)) { 908 throw new OpenIDConnectClientException('Error decoding JSON from token header'); 909 } 910 $payload = implode('.', $parts); 911 $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri'))); 912 if ($jwks === NULL) { 913 throw new OpenIDConnectClientException('Error decoding JSON from jwks_uri'); 914 } 915 if (!isset($header->alg)) { 916 throw new OpenIDConnectClientException('Error missing signature type in token header'); 917 } 918 switch ($header->alg) { 919 case 'RS256': 920 case 'PS256': 921 case 'RS384': 922 case 'RS512': 923 $hashtype = 'sha' . substr($header->alg, 2); 924 $signatureType = $header->alg === 'PS256' ? 'PSS' : ''; 925 926 $verified = $this->verifyRSAJWTsignature($hashtype, 927 $this->get_key_for_header($jwks->keys, $header), 928 $payload, $signature, $signatureType); 929 break; 930 case 'HS256': 931 case 'HS512': 932 case 'HS384': 933 $hashtype = 'SHA' . substr($header->alg, 2); 934 $verified = $this->verifyHMACJWTsignature($hashtype, $this->getClientSecret(), $payload, $signature); 935 break; 936 default: 937 throw new OpenIDConnectClientException('No support for signature type: ' . $header->alg); 938 } 939 return $verified; 940 } 941 942 /** 943 * @param object $claims 944 * @param string|null $accessToken 945 * @return bool 946 */ 947 protected function verifyJWTclaims($claims, $accessToken = null) { 948 if(isset($claims->at_hash) && isset($accessToken)){ 949 if(isset($this->getIdTokenHeader()->alg) && $this->getIdTokenHeader()->alg !== 'none'){ 950 $bit = substr($this->getIdTokenHeader()->alg, 2, 3); 951 }else{ 952 // TODO: Error case. throw exception??? 953 $bit = '256'; 954 } 955 $len = ((int)$bit)/16; 956 $expected_at_hash = $this->urlEncode(substr(hash('sha'.$bit, $accessToken, true), 0, $len)); 957 } 958 return (($this->issuerValidator->__invoke($claims->iss)) 959 && (($claims->aud === $this->clientID) || in_array($this->clientID, $claims->aud, true)) 960 && ($claims->nonce === $this->getNonce()) 961 && ( !isset($claims->exp) || ((gettype($claims->exp) === 'integer') && ($claims->exp >= time() - $this->leeway))) 962 && ( !isset($claims->nbf) || ((gettype($claims->nbf) === 'integer') && ($claims->nbf <= time() + $this->leeway))) 963 && ( !isset($claims->at_hash) || $claims->at_hash === $expected_at_hash ) 964 ); 965 } 966 967 /** 968 * @param string $str 969 * @return string 970 */ 971 protected function urlEncode($str) { 972 $enc = base64_encode($str); 973 $enc = rtrim($enc, '='); 974 $enc = strtr($enc, '+/', '-_'); 975 return $enc; 976 } 977 978 /** 979 * @param string $jwt encoded JWT 980 * @param int $section the section we would like to decode 981 * @return object 982 */ 983 protected function decodeJWT($jwt, $section = 0) { 984 985 $parts = explode('.', $jwt); 986 return json_decode(base64url_decode($parts[$section])); 987 } 988 989 /** 990 * 991 * @param string|null $attribute optional 992 * 993 * Attribute Type Description 994 * user_id string REQUIRED Identifier for the End-User at the Issuer. 995 * name string End-User's full name in displayable form including all name parts, ordered according to End-User's locale and preferences. 996 * given_name string Given name or first name of the End-User. 997 * family_name string Surname or last name of the End-User. 998 * middle_name string Middle name of the End-User. 999 * nickname string Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. 1000 * profile string URL of End-User's profile page. 1001 * picture string URL of the End-User's profile picture. 1002 * website string URL of End-User's web page or blog. 1003 * email string The End-User's preferred e-mail address. 1004 * verified boolean True if the End-User's e-mail address has been verified; otherwise false. 1005 * gender string The End-User's gender: Values defined by this specification are female and male. Other values MAY be used when neither of the defined values are applicable. 1006 * birthday string The End-User's birthday, represented as a date string in MM/DD/YYYY format. The year MAY be 0000, indicating that it is omitted. 1007 * zoneinfo string String from zoneinfo [zoneinfo] time zone database. For example, Europe/Paris or America/Los_Angeles. 1008 * locale string The End-User's locale, represented as a BCP47 [RFC5646] language tag. This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. For example, en-US or fr-CA. As a compatibility note, some implementations have used an underscore as the separator rather than a dash, for example, en_US; Implementations MAY choose to accept this locale syntax as well. 1009 * phone_number string The End-User's preferred telephone number. E.164 [E.164] is RECOMMENDED as the format of this Claim. For example, +1 (425) 555-1212 or +56 (2) 687 2400. 1010 * address JSON object The End-User's preferred address. The value of the address member is a JSON [RFC4627] structure containing some or all of the members defined in Section 2.4.2.1. 1011 * updated_time string Time the End-User's information was last updated, represented as a RFC 3339 [RFC3339] datetime. For example, 2011-01-03T23:58:42+0000. 1012 * 1013 * @return mixed 1014 * 1015 * @throws OpenIDConnectClientException 1016 */ 1017 public function requestUserInfo($attribute = null) { 1018 1019 $user_info_endpoint = $this->getProviderConfigValue('userinfo_endpoint'); 1020 $schema = 'openid'; 1021 1022 $user_info_endpoint .= '?schema=' . $schema; 1023 1024 //The accessToken has to be sent in the Authorization header. 1025 // Accept json to indicate response type 1026 $headers = ["Authorization: Bearer {$this->accessToken}", 1027 'Accept: application/json']; 1028 1029 $user_json = json_decode($this->fetchURL($user_info_endpoint,null,$headers)); 1030 if ($this->getResponseCode() <> 200) { 1031 throw new OpenIDConnectClientException('The communication to retrieve user data has failed with status code '.$this->getResponseCode()); 1032 } 1033 $this->userInfo = $user_json; 1034 1035 if($attribute === null) { 1036 return $this->userInfo; 1037 } 1038 1039 if (property_exists($this->userInfo, $attribute)) { 1040 return $this->userInfo->$attribute; 1041 } 1042 1043 return null; 1044 } 1045 1046 /** 1047 * 1048 * @param string|null $attribute optional 1049 * 1050 * Attribute Type Description 1051 * exp int Expires at 1052 * nbf int Not before 1053 * ver string Version 1054 * iss string Issuer 1055 * sub string Subject 1056 * aud string Audience 1057 * nonce string nonce 1058 * iat int Issued At 1059 * auth_time int Authenatication time 1060 * oid string Object id 1061 * 1062 * @return mixed 1063 * 1064 */ 1065 public function getVerifiedClaims($attribute = null) { 1066 1067 if($attribute === null) { 1068 return $this->verifiedClaims; 1069 } 1070 1071 if (property_exists($this->verifiedClaims, $attribute)) { 1072 return $this->verifiedClaims->$attribute; 1073 } 1074 1075 return null; 1076 } 1077 1078 /** 1079 * @param string $url 1080 * @param string | null $post_body string If this is set the post type will be POST 1081 * @param array $headers Extra headers to be send with the request. Format as 'NameHeader: ValueHeader' 1082 * @throws OpenIDConnectClientException 1083 * @return mixed 1084 */ 1085 protected function fetchURL($url, $post_body = null, $headers = array()) { 1086 1087 1088 // OK cool - then let's create a new cURL resource handle 1089 $ch = curl_init(); 1090 1091 // Determine whether this is a GET or POST 1092 if ($post_body !== null) { 1093 // curl_setopt($ch, CURLOPT_POST, 1); 1094 // Alows to keep the POST method even after redirect 1095 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); 1096 curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body); 1097 1098 // Default content type is form encoded 1099 $content_type = 'application/x-www-form-urlencoded'; 1100 1101 // Determine if this is a JSON payload and add the appropriate content type 1102 if (is_object(json_decode($post_body))) { 1103 $content_type = 'application/json'; 1104 } 1105 1106 // Add POST-specific headers 1107 $headers[] = "Content-Type: {$content_type}"; 1108 1109 } 1110 1111 // If we set some headers include them 1112 if(count($headers) > 0) { 1113 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 1114 } 1115 1116 // Set URL to download 1117 curl_setopt($ch, CURLOPT_URL, $url); 1118 1119 if (isset($this->httpProxy)) { 1120 curl_setopt($ch, CURLOPT_PROXY, $this->httpProxy); 1121 } 1122 1123 // Include header in result? (0 = yes, 1 = no) 1124 curl_setopt($ch, CURLOPT_HEADER, 0); 1125 1126 // Allows to follow redirect 1127 // FIXME not possible in openbasedir-restrictions 1128 //curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 1129 1130 /** 1131 * Set cert 1132 * Otherwise ignore SSL peer verification 1133 */ 1134 if (isset($this->certPath)) { 1135 curl_setopt($ch, CURLOPT_CAINFO, $this->certPath); 1136 } 1137 1138 if($this->verifyHost) { 1139 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); 1140 } else { 1141 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); 1142 } 1143 1144 if($this->verifyPeer) { 1145 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); 1146 } else { 1147 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 1148 } 1149 1150 // Should cURL return or print out the data? (true = return, false = print) 1151 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 1152 1153 // Timeout in seconds 1154 curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeOut); 1155 1156 // Download the given URL, and return output 1157 $output = curl_exec($ch); 1158 1159 // HTTP Response code from server may be required from subclass 1160 $info = curl_getinfo($ch); 1161 $this->responseCode = $info['http_code']; 1162 1163 if ($output === false) { 1164 throw new OpenIDConnectClientException('Curl error: (' . curl_errno($ch) . ') ' . curl_error($ch)); 1165 } 1166 1167 // Close the cURL resource, and free system resources 1168 curl_close($ch); 1169 1170 return $output; 1171 } 1172 1173 /** 1174 * @param bool $appendSlash 1175 * @return string 1176 * @throws OpenIDConnectClientException 1177 */ 1178 public function getWellKnownIssuer($appendSlash = false) { 1179 1180 return $this->getWellKnownConfigValue('issuer') . ($appendSlash ? '/' : ''); 1181 } 1182 1183 /** 1184 * @return string 1185 * @throws OpenIDConnectClientException 1186 */ 1187 public function getIssuer() { 1188 1189 if (!isset($this->providerConfig['issuer'])) { 1190 throw new OpenIDConnectClientException('The issuer has not been set'); 1191 } 1192 1193 return $this->providerConfig['issuer']; 1194 } 1195 1196 /** 1197 * @return mixed 1198 * @throws OpenIDConnectClientException 1199 */ 1200 public function getProviderURL() { 1201 if (!isset($this->providerConfig['providerUrl'])) { 1202 throw new OpenIDConnectClientException('The provider URL has not been set'); 1203 } 1204 1205 return $this->providerConfig['providerUrl']; 1206 } 1207 1208 /** 1209 * @param string $url 1210 */ 1211 public function redirect($url) { 1212 header('Location: ' . $url); 1213 exit; 1214 } 1215 1216 /** 1217 * @param string $httpProxy 1218 */ 1219 public function setHttpProxy($httpProxy) { 1220 $this->httpProxy = $httpProxy; 1221 } 1222 1223 /** 1224 * @param string $certPath 1225 */ 1226 public function setCertPath($certPath) { 1227 $this->certPath = $certPath; 1228 } 1229 1230 /** 1231 * @return string|null 1232 */ 1233 public function getCertPath() 1234 { 1235 return $this->certPath; 1236 } 1237 1238 /** 1239 * @param bool $verifyPeer 1240 */ 1241 public function setVerifyPeer($verifyPeer) { 1242 $this->verifyPeer = $verifyPeer; 1243 } 1244 1245 /** 1246 * @param bool $verifyHost 1247 */ 1248 public function setVerifyHost($verifyHost) { 1249 $this->verifyHost = $verifyHost; 1250 } 1251 1252 /** 1253 * @return bool 1254 */ 1255 public function getVerifyHost() 1256 { 1257 return $this->verifyHost; 1258 } 1259 1260 /** 1261 * @return bool 1262 */ 1263 public function getVerifyPeer() 1264 { 1265 return $this->verifyPeer; 1266 } 1267 1268 /** 1269 * Use this for custom issuer validation 1270 * The given function should accept the issuer string from the JWT claim as the only argument 1271 * and return true if the issuer is valid, otherwise return false 1272 * 1273 * @param callable $issuerValidator 1274 */ 1275 public function setIssuerValidator($issuerValidator){ 1276 $this->issuerValidator = $issuerValidator; 1277 } 1278 1279 /** 1280 * @param bool $allowImplicitFlow 1281 */ 1282 public function setAllowImplicitFlow($allowImplicitFlow) { 1283 $this->allowImplicitFlow = $allowImplicitFlow; 1284 } 1285 1286 /** 1287 * @return bool 1288 */ 1289 public function getAllowImplicitFlow() 1290 { 1291 return $this->allowImplicitFlow; 1292 } 1293 1294 /** 1295 * 1296 * Use this to alter a provider's endpoints and other attributes 1297 * 1298 * @param array $array 1299 * simple key => value 1300 */ 1301 public function providerConfigParam($array) { 1302 $this->providerConfig = array_merge($this->providerConfig, $array); 1303 } 1304 1305 /** 1306 * @param string $clientSecret 1307 */ 1308 public function setClientSecret($clientSecret) { 1309 $this->clientSecret = $clientSecret; 1310 } 1311 1312 /** 1313 * @param string $clientID 1314 */ 1315 public function setClientID($clientID) { 1316 $this->clientID = $clientID; 1317 } 1318 1319 1320 /** 1321 * Dynamic registration 1322 * 1323 * @throws OpenIDConnectClientException 1324 */ 1325 public function register() { 1326 1327 $registration_endpoint = $this->getProviderConfigValue('registration_endpoint'); 1328 1329 $send_object = (object ) array_merge($this->registrationParams, array( 1330 'redirect_uris' => array($this->getRedirectURL()), 1331 'client_name' => $this->getClientName() 1332 )); 1333 1334 $response = $this->fetchURL($registration_endpoint, json_encode($send_object)); 1335 1336 $json_response = json_decode($response); 1337 1338 // Throw some errors if we encounter them 1339 if ($json_response === false) { 1340 throw new OpenIDConnectClientException('Error registering: JSON response received from the server was invalid.'); 1341 } 1342 1343 if (isset($json_response->{'error_description'})) { 1344 throw new OpenIDConnectClientException($json_response->{'error_description'}); 1345 } 1346 1347 $this->setClientID($json_response->{'client_id'}); 1348 1349 // The OpenID Connect Dynamic registration protocol makes the client secret optional 1350 // and provides a registration access token and URI endpoint if it is not present 1351 if (isset($json_response->{'client_secret'})) { 1352 $this->setClientSecret($json_response->{'client_secret'}); 1353 } else { 1354 throw new OpenIDConnectClientException('Error registering: 1355 Please contact the OpenID Connect provider and obtain a Client ID and Secret directly from them'); 1356 } 1357 1358 } 1359 1360 /** 1361 * Introspect a given token - either access token or refresh token. 1362 * @see https://tools.ietf.org/html/rfc7662 1363 * 1364 * @param string $token 1365 * @param string $token_type_hint 1366 * @param string|null $clientId 1367 * @param string|null $clientSecret 1368 * @return mixed 1369 * @throws OpenIDConnectClientException 1370 */ 1371 public function introspectToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) { 1372 $introspection_endpoint = $this->getProviderConfigValue('introspection_endpoint'); 1373 1374 $post_data = array( 1375 'token' => $token, 1376 ); 1377 if ($token_type_hint) { 1378 $post_data['token_type_hint'] = $token_type_hint; 1379 } 1380 $clientId = $clientId !== null ? $clientId : $this->clientID; 1381 $clientSecret = $clientSecret !== null ? $clientSecret : $this->clientSecret; 1382 1383 // Convert token params to string format 1384 $post_params = http_build_query($post_data, null, '&'); 1385 $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), 1386 'Accept: application/json']; 1387 1388 return json_decode($this->fetchURL($introspection_endpoint, $post_params, $headers)); 1389 } 1390 1391 /** 1392 * Revoke a given token - either access token or refresh token. 1393 * @see https://tools.ietf.org/html/rfc7009 1394 * 1395 * @param string $token 1396 * @param string $token_type_hint 1397 * @param string|null $clientId 1398 * @param string|null $clientSecret 1399 * @return mixed 1400 * @throws OpenIDConnectClientException 1401 */ 1402 public function revokeToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) { 1403 $revocation_endpoint = $this->getProviderConfigValue('revocation_endpoint'); 1404 1405 $post_data = array( 1406 'token' => $token, 1407 ); 1408 if ($token_type_hint) { 1409 $post_data['token_type_hint'] = $token_type_hint; 1410 } 1411 $clientId = $clientId !== null ? $clientId : $this->clientID; 1412 $clientSecret = $clientSecret !== null ? $clientSecret : $this->clientSecret; 1413 1414 // Convert token params to string format 1415 $post_params = http_build_query($post_data, null, '&'); 1416 $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), 1417 'Accept: application/json']; 1418 1419 return json_decode($this->fetchURL($revocation_endpoint, $post_params, $headers)); 1420 } 1421 1422 /** 1423 * @return string 1424 */ 1425 public function getClientName() { 1426 return $this->clientName; 1427 } 1428 1429 /** 1430 * @param string $clientName 1431 */ 1432 public function setClientName($clientName) { 1433 $this->clientName = $clientName; 1434 } 1435 1436 /** 1437 * @return string 1438 */ 1439 public function getClientID() { 1440 return $this->clientID; 1441 } 1442 1443 /** 1444 * @return string 1445 */ 1446 public function getClientSecret() { 1447 return $this->clientSecret; 1448 } 1449 1450 /** 1451 * @return bool 1452 */ 1453 public function canVerifySignatures() { 1454 return class_exists('\phpseclib\Crypt\RSA') || class_exists('Crypt_RSA'); 1455 } 1456 1457 /** 1458 * Set the access token. 1459 * 1460 * May be required for subclasses of this Client. 1461 * 1462 * @param string $accessToken 1463 * @return void 1464 */ 1465 public function setAccessToken($accessToken) { 1466 $this->accessToken = $accessToken; 1467 } 1468 1469 /** 1470 * @return string 1471 */ 1472 public function getAccessToken() { 1473 return $this->accessToken; 1474 } 1475 1476 /** 1477 * @return string 1478 */ 1479 public function getRefreshToken() { 1480 return $this->refreshToken; 1481 } 1482 1483 /** 1484 * @return string 1485 */ 1486 public function getIdToken() { 1487 return $this->idToken; 1488 } 1489 1490 /** 1491 * @return object 1492 */ 1493 public function getAccessTokenHeader() { 1494 return $this->decodeJWT($this->accessToken); 1495 } 1496 1497 /** 1498 * @return object 1499 */ 1500 public function getAccessTokenPayload() { 1501 return $this->decodeJWT($this->accessToken, 1); 1502 } 1503 1504 /** 1505 * @return object 1506 */ 1507 public function getIdTokenHeader() { 1508 return $this->decodeJWT($this->idToken); 1509 } 1510 1511 /** 1512 * @return object 1513 */ 1514 public function getIdTokenPayload() { 1515 return $this->decodeJWT($this->idToken, 1); 1516 } 1517 1518 /** 1519 * @return string 1520 */ 1521 public function getTokenResponse() { 1522 return $this->tokenResponse; 1523 } 1524 1525 /** 1526 * Stores nonce 1527 * 1528 * @param string $nonce 1529 * @return string 1530 */ 1531 protected function setNonce($nonce) { 1532 $this->setSessionKey('openid_connect_nonce', $nonce); 1533 return $nonce; 1534 } 1535 1536 /** 1537 * Get stored nonce 1538 * 1539 * @return string 1540 */ 1541 protected function getNonce() { 1542 return $this->getSessionKey('openid_connect_nonce'); 1543 } 1544 1545 /** 1546 * Cleanup nonce 1547 * 1548 * @return void 1549 */ 1550 protected function unsetNonce() { 1551 $this->unsetSessionKey('openid_connect_nonce'); 1552 } 1553 1554 /** 1555 * Stores $state 1556 * 1557 * @param string $state 1558 * @return string 1559 */ 1560 protected function setState($state) { 1561 $this->setSessionKey('openid_connect_state', $state); 1562 return $state; 1563 } 1564 1565 /** 1566 * Get stored state 1567 * 1568 * @return string 1569 */ 1570 protected function getState() { 1571 return $this->getSessionKey('openid_connect_state'); 1572 } 1573 1574 /** 1575 * Cleanup state 1576 * 1577 * @return void 1578 */ 1579 protected function unsetState() { 1580 $this->unsetSessionKey('openid_connect_state'); 1581 } 1582 1583 /** 1584 * Get the response code from last action/curl request. 1585 * 1586 * @return int 1587 */ 1588 public function getResponseCode() 1589 { 1590 return $this->responseCode; 1591 } 1592 1593 /** 1594 * Set timeout (seconds) 1595 * 1596 * @param int $timeout 1597 */ 1598 public function setTimeout($timeout) 1599 { 1600 $this->timeOut = $timeout; 1601 } 1602 1603 /** 1604 * @return int 1605 */ 1606 public function getTimeout() 1607 { 1608 return $this->timeOut; 1609 } 1610 1611 /** 1612 * Safely calculate length of binary string 1613 * @param string $str 1614 * @return int 1615 */ 1616 private static function safeLength($str) 1617 { 1618 if (function_exists('mb_strlen')) { 1619 return mb_strlen($str, '8bit'); 1620 } 1621 return strlen($str); 1622 } 1623 1624 /** 1625 * Where has_equals is not available, this provides a timing-attack safe string comparison 1626 * @param string $str1 1627 * @param string $str2 1628 * @return bool 1629 */ 1630 private static function hashEquals($str1, $str2) 1631 { 1632 $len1=static::safeLength($str1); 1633 $len2=static::safeLength($str2); 1634 1635 //compare strings without any early abort... 1636 $len = min($len1, $len2); 1637 $status = 0; 1638 for ($i = 0; $i < $len; $i++) { 1639 $status |= (ord($str1[$i]) ^ ord($str2[$i])); 1640 } 1641 //if strings were different lengths, we fail 1642 $status |= ($len1 ^ $len2); 1643 return ($status === 0); 1644 } 1645 1646 /** 1647 * Use session to manage a nonce 1648 */ 1649 protected function startSession() { 1650 if (!isset($_SESSION)) { 1651 @session_start(); 1652 } 1653 } 1654 1655 protected function commitSession() { 1656 $this->startSession(); 1657 1658 session_write_close(); 1659 } 1660 1661 protected function getSessionKey($key) { 1662 $this->startSession(); 1663 1664 return $_SESSION[$key]; 1665 } 1666 1667 protected function setSessionKey($key, $value) { 1668 $this->startSession(); 1669 1670 $_SESSION[$key] = $value; 1671 } 1672 1673 protected function unsetSessionKey($key) { 1674 $this->startSession(); 1675 1676 unset($_SESSION[$key]); 1677 } 1678 1679 public function setUrlEncoding($curEncoding) 1680 { 1681 switch ($curEncoding) 1682 { 1683 case PHP_QUERY_RFC1738: 1684 $this->enc_type = PHP_QUERY_RFC1738; 1685 break; 1686 1687 case PHP_QUERY_RFC3986: 1688 $this->enc_type = PHP_QUERY_RFC3986; 1689 break; 1690 1691 default: 1692 break; 1693 } 1694 1695 } 1696 1697 /** 1698 * @return array 1699 */ 1700 public function getScopes() 1701 { 1702 return $this->scopes; 1703 } 1704 1705 /** 1706 * @return array 1707 */ 1708 public function getResponseTypes() 1709 { 1710 return $this->responseTypes; 1711 } 1712 1713 /** 1714 * @return array 1715 */ 1716 public function getAuthParams() 1717 { 1718 return $this->authParams; 1719 } 1720 1721 /** 1722 * @return callable 1723 */ 1724 public function getIssuerValidator() 1725 { 1726 return $this->issuerValidator; 1727 } 1728 1729 /** 1730 * @return int 1731 */ 1732 public function getLeeway() 1733 { 1734 return $this->leeway; 1735 } 1736 }
Download modules/openid_connect/OpenIDConnectClient.class.php
History Sat, 31 Oct 2020 03:48:03 +0100 Jan Dankert Some bad fixes for OIDC to work properly. Sat, 31 Oct 2020 00:55:00 +0100 Jan Dankert Fix: OpenId-Connect-Buttons must be clickable; Fixed OpenId-Connect configuration.