openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

OpenIDConnectClient.class.php (46660B)


      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 }