ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
OpenIDConnectClient.php
Go to the documentation of this file.
1 <?php
23 namespace Jumbojett;
24 
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 
44 function base64url_decode($base64url) {
45  return base64_decode(b64url2b64($base64url));
46 }
47 
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 
69 class OpenIDConnectClientException extends \Exception
70 {
71 
72 }
73 
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 
90 {
91 
95  private $clientID;
96 
100  private $clientName;
101 
105  private $clientSecret;
106 
110  private $providerConfig = array();
111 
115  private $httpProxy;
116 
120  private $certPath;
121 
125  private $verifyPeer = true;
126 
130  private $verifyHost = true;
131 
135  protected $accessToken;
136 
140  private $refreshToken;
141 
145  protected $idToken;
146 
150  private $tokenResponse;
151 
155  private $scopes = array();
156 
160  private $responseCode;
161 
165  private $responseTypes = array();
166 
170  private $userInfo = array();
171 
175  private $authParams = array();
176 
180  private $registrationParams = array();
181 
185  private $wellKnown = false;
186 
191  private $wellKnownConfigParameters = array();
192 
196  protected $timeOut = 60;
197 
201  private $leeway = 300;
202 
206  private $additionalJwks = array();
207 
211  protected $verifiedClaims = array();
212 
217 
221  private $allowImplicitFlow = false;
225  private $redirectURL;
226 
227  protected $enc_type = PHP_QUERY_RFC1738;
228 
233  private $codeChallengeMethod = false;
234 
238  private $pkceAlgs = array('S256' => 'sha256', 'plain' => false);
239 
247  public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null) {
248  $this->setProviderURL($provider_url);
249  if ($issuer === null) {
250  $this->setIssuer($provider_url);
251  } else {
252  $this->setIssuer($issuer);
253  }
254 
255  $this->clientID = $client_id;
256  $this->clientSecret = $client_secret;
257 
258  $this->issuerValidator = function($iss){
259  return ($iss === $this->getIssuer() || $iss === $this->getWellKnownIssuer() || $iss === $this->getWellKnownIssuer(true));
260  };
261  }
262 
266  public function setProviderURL($provider_url) {
267  $this->providerConfig['providerUrl'] = $provider_url;
268  }
269 
273  public function setIssuer($issuer) {
274  $this->providerConfig['issuer'] = $issuer;
275  }
276 
280  public function setResponseTypes($response_types) {
281  $this->responseTypes = array_merge($this->responseTypes, (array)$response_types);
282  }
283 
288  public function authenticate() {
289 
290  // Do a preemptive check to see if the provider has thrown an error from a previous redirect
291  if (isset($_REQUEST['error'])) {
292  $desc = isset($_REQUEST['error_description']) ? ' Description: ' . $_REQUEST['error_description'] : '';
293  throw new OpenIDConnectClientException('Error: ' . $_REQUEST['error'] .$desc);
294  }
295 
296  // If we have an authorization code then proceed to request a token
297  if (isset($_REQUEST['code'])) {
298 
299  $code = $_REQUEST['code'];
300  $token_json = $this->requestTokens($code);
301 
302  // Throw an error if the server returns one
303  if (isset($token_json->error)) {
304  if (isset($token_json->error_description)) {
305  throw new OpenIDConnectClientException($token_json->error_description);
306  }
307  throw new OpenIDConnectClientException('Got response: ' . $token_json->error);
308  }
309 
310  // Do an OpenID Connect session check
311  if ($_REQUEST['state'] !== $this->getState()) {
312  throw new OpenIDConnectClientException('Unable to determine state');
313  }
314 
315  // Cleanup state
316  $this->unsetState();
317 
318  if (!property_exists($token_json, 'id_token')) {
319  throw new OpenIDConnectClientException('User did not authorize openid scope.');
320  }
321 
322  $claims = $this->decodeJWT($token_json->id_token, 1);
323 
324  // Verify the signature
325  if ($this->canVerifySignatures()) {
326  if (!$this->getProviderConfigValue('jwks_uri')) {
327  throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined');
328  }
329  if (!$this->verifyJWTsignature($token_json->id_token)) {
330  throw new OpenIDConnectClientException ('Unable to verify signature');
331  }
332  } else {
333  user_error('Warning: JWT signature verification unavailable.');
334  }
335 
336  // Save the id token
337  $this->idToken = $token_json->id_token;
338 
339  // Save the access token
340  $this->accessToken = $token_json->access_token;
341 
342  // If this is a valid claim
343  if ($this->verifyJWTclaims($claims, $token_json->access_token)) {
344 
345  // Clean up the session a little
346  $this->unsetNonce();
347 
348  // Save the full response
349  $this->tokenResponse = $token_json;
350 
351  // Save the verified claims
352  $this->verifiedClaims = $claims;
353 
354  // Save the refresh token, if we got one
355  if (isset($token_json->refresh_token)) {
356  $this->refreshToken = $token_json->refresh_token;
357  }
358 
359  // Success!
360  return true;
361 
362  }
363 
364  throw new OpenIDConnectClientException ('Unable to verify JWT claims');
365  }
366 
367  if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) {
368  // if we have no code but an id_token use that
369  $id_token = $_REQUEST['id_token'];
370 
371  $accessToken = null;
372  if (isset($_REQUEST['access_token'])) {
373  $accessToken = $_REQUEST['access_token'];
374  }
375 
376  // Do an OpenID Connect session check
377  if ($_REQUEST['state'] !== $this->getState()) {
378  throw new OpenIDConnectClientException('Unable to determine state');
379  }
380 
381  // Cleanup state
382  $this->unsetState();
383 
384  $claims = $this->decodeJWT($id_token, 1);
385 
386  // Verify the signature
387  if ($this->canVerifySignatures()) {
388  if (!$this->getProviderConfigValue('jwks_uri')) {
389  throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined');
390  }
391  if (!$this->verifyJWTsignature($id_token)) {
392  throw new OpenIDConnectClientException ('Unable to verify signature');
393  }
394  } else {
395  user_error('Warning: JWT signature verification unavailable.');
396  }
397 
398  // Save the id token
399  $this->idToken = $id_token;
400 
401  // If this is a valid claim
402  if ($this->verifyJWTclaims($claims, $accessToken)) {
403 
404  // Clean up the session a little
405  $this->unsetNonce();
406 
407  // Save the verified claims
408  $this->verifiedClaims = $claims;
409 
410  // Save the access token
411  if ($accessToken) {
412  $this->accessToken = $accessToken;
413  }
414 
415  // Success!
416  return true;
417 
418  }
419 
420  throw new OpenIDConnectClientException ('Unable to verify JWT claims');
421  }
422 
423  $this->requestAuthorization();
424  return false;
425 
426  }
427 
440  public function signOut($accessToken, $redirect) {
441  $signout_endpoint = $this->getProviderConfigValue('end_session_endpoint');
442 
443  $signout_params = null;
444  if($redirect === null){
445  $signout_params = array('id_token_hint' => $accessToken);
446  }
447  else {
448  $signout_params = array(
449  'id_token_hint' => $accessToken,
450  'post_logout_redirect_uri' => $redirect);
451  }
452 
453  $signout_endpoint .= (strpos($signout_endpoint, '?') === false ? '?' : '&') . http_build_query( $signout_params, null, '&', $this->enc_type);
454  $this->redirect($signout_endpoint);
455  }
456 
460  public function addScope($scope) {
461  $this->scopes = array_merge($this->scopes, (array)$scope);
462  }
463 
467  public function addAuthParam($param) {
468  $this->authParams = array_merge($this->authParams, (array)$param);
469  }
470 
474  public function addRegistrationParam($param) {
475  $this->registrationParams = array_merge($this->registrationParams, (array)$param);
476  }
477 
481  protected function addAdditionalJwk($jwk) {
482  $this->additionalJwks[] = $jwk;
483  }
484 
494  protected function getProviderConfigValue($param, $default = null) {
495 
496  // If the configuration value is not available, attempt to fetch it from a well known config endpoint
497  // This is also known as auto "discovery"
498  if (!isset($this->providerConfig[$param])) {
499  $this->providerConfig[$param] = $this->getWellKnownConfigValue($param, $default);
500  }
501 
502  return $this->providerConfig[$param];
503  }
504 
514  private function getWellKnownConfigValue($param, $default = null) {
515 
516  // If the configuration value is not available, attempt to fetch it from a well known config endpoint
517  // This is also known as auto "discovery"
518  if(!$this->wellKnown) {
519  $well_known_config_url = rtrim($this->getProviderURL(), '/') . '/.well-known/openid-configuration';
520  if (count($this->wellKnownConfigParameters) > 0){
521  $well_known_config_url .= '?' . http_build_query($this->wellKnownConfigParameters) ;
522  }
523  $this->wellKnown = json_decode($this->fetchURL($well_known_config_url));
524  }
525 
526  $value = false;
527  if(isset($this->wellKnown->{$param})){
528  $value = $this->wellKnown->{$param};
529  }
530 
531  if ($value) {
532  return $value;
533  }
534 
535  if (isset($default)) {
536  // Uses default value if provided
537  return $default;
538  }
539 
540  throw new OpenIDConnectClientException("The provider {$param} could not be fetched. Make sure your provider has a well known configuration available.");
541  }
542 
549  public function setWellKnownConfigParameters(array $params = []){
550  $this->wellKnownConfigParameters=$params;
551  }
552 
553 
557  public function setRedirectURL ($url) {
558  if (parse_url($url,PHP_URL_HOST) !== false) {
559  $this->redirectURL = $url;
560  }
561  }
562 
568  public function getRedirectURL() {
569 
570  // If the redirect URL has been set then return it.
571  if (property_exists($this, 'redirectURL') && $this->redirectURL) {
572  return $this->redirectURL;
573  }
574 
575  // Other-wise return the URL of the current page
576 
582  /*
583  * Compatibility with multiple host headers.
584  * The problem with SSL over port 80 is resolved and non-SSL over port 443.
585  * Support of 'ProxyReverse' configurations.
586  */
587 
588  if (isset($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS']) && ($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS'] === '1')) {
589  $protocol = 'https';
590  } else {
591  $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO']
592  ?: @$_SERVER['REQUEST_SCHEME']
593  ?: ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http');
594  }
595 
596  $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT'])
597  ?: @intval($_SERVER['SERVER_PORT'])
598  ?: (($protocol === 'https') ? 443 : 80);
599 
600  $host = @explode(':', $_SERVER['HTTP_HOST'])[0]
601  ?: @$_SERVER['SERVER_NAME']
602  ?: @$_SERVER['SERVER_ADDR'];
603 
604  $port = (443 === $port) || (80 === $port) ? '' : ':' . $port;
605 
606  return sprintf('%s://%s%s/%s', $protocol, $host, $port, @trim(reset(explode('?', $_SERVER['REQUEST_URI'])), '/'));
607  }
608 
615  protected function generateRandString() {
616  // Error and Exception need to be catched in this order, see https://github.com/paragonie/random_compat/blob/master/README.md
617  // random_compat polyfill library should be removed if support for PHP versions < 7 is dropped
618  try {
619  return \bin2hex(\random_bytes(16));
620  } catch (Error $e) {
621  throw new OpenIDConnectClientException('Random token generation failed.');
622  } catch (Exception $e) {
623  throw new OpenIDConnectClientException('Random token generation failed.');
624  };
625  }
626 
632  private function requestAuthorization() {
633 
634  $auth_endpoint = $this->getProviderConfigValue('authorization_endpoint');
635  $response_type = 'code';
636 
637  // Generate and store a nonce in the session
638  // The nonce is an arbitrary value
639  $nonce = $this->setNonce($this->generateRandString());
640 
641  // State essentially acts as a session key for OIDC
642  $state = $this->setState($this->generateRandString());
643 
644  $auth_params = array_merge($this->authParams, array(
645  'response_type' => $response_type,
646  'redirect_uri' => $this->getRedirectURL(),
647  'client_id' => $this->clientID,
648  'nonce' => $nonce,
649  'state' => $state,
650  'scope' => 'openid'
651  ));
652 
653  // If the client has been registered with additional scopes
654  if (count($this->scopes) > 0) {
655  $auth_params = array_merge($auth_params, array('scope' => implode(' ', array_merge($this->scopes, array('openid')))));
656  }
657 
658  // If the client has been registered with additional response types
659  if (count($this->responseTypes) > 0) {
660  $auth_params = array_merge($auth_params, array('response_type' => implode(' ', $this->responseTypes)));
661  }
662 
663  // If the client supports Proof Key for Code Exchange (PKCE)
664  if (!empty($this->getCodeChallengeMethod()) && in_array($this->getCodeChallengeMethod(), $this->getProviderConfigValue('code_challenge_methods_supported'))) {
665  $codeVerifier = bin2hex(random_bytes(64));
666  $this->setCodeVerifier($codeVerifier);
667  if (!empty($this->pkceAlgs[$this->getCodeChallengeMethod()])) {
668  $codeChallenge = rtrim(strtr(base64_encode(hash($this->pkceAlgs[$this->getCodeChallengeMethod()], $codeVerifier, true)), '+/', '-_'), '=');
669  } else {
670  $codeChallenge = $codeVerifier;
671  }
672  $auth_params = array_merge($auth_params, array(
673  'code_challenge' => $codeChallenge,
674  'code_challenge_method' => $this->getCodeChallengeMethod()
675  ));
676  }
677 
678  $auth_endpoint .= (strpos($auth_endpoint, '?') === false ? '?' : '&') . http_build_query($auth_params, null, '&', $this->enc_type);
679 
680  $this->commitSession();
681  $this->redirect($auth_endpoint);
682  }
683 
689  public function requestClientCredentialsToken() {
690  $token_endpoint = $this->getProviderConfigValue('token_endpoint');
691 
692  $headers = [];
693 
694  $grant_type = 'client_credentials';
695 
696  $post_data = array(
697  'grant_type' => $grant_type,
698  'client_id' => $this->clientID,
699  'client_secret' => $this->clientSecret,
700  'scope' => implode(' ', $this->scopes)
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 
718  public function requestResourceOwnerToken($bClientAuth = FALSE) {
719  $token_endpoint = $this->getProviderConfigValue('token_endpoint');
720 
721  $headers = [];
722 
723  $grant_type = 'password';
724 
725  $post_data = array(
726  'grant_type' => $grant_type,
727  'username' => $this->authParams['username'],
728  'password' => $this->authParams['password'],
729  'scope' => implode(' ', $this->scopes)
730  );
731 
732  //For client authentication include the client values
733  if($bClientAuth) {
734  $post_data['client_id'] = $this->clientID;
735  $post_data['client_secret'] = $this->clientSecret;
736  }
737 
738  // Convert token params to string format
739  $post_params = http_build_query($post_data, null, '&', $this->enc_type);
740 
741  return json_decode($this->fetchURL($token_endpoint, $post_params, $headers));
742  }
743 
744 
752  protected function requestTokens($code) {
753  $token_endpoint = $this->getProviderConfigValue('token_endpoint');
754  $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']);
755 
756  $headers = [];
757 
758  $grant_type = 'authorization_code';
759 
760  $token_params = array(
761  'grant_type' => $grant_type,
762  'code' => $code,
763  'redirect_uri' => $this->getRedirectURL(),
764  'client_id' => $this->clientID,
765  'client_secret' => $this->clientSecret
766  );
767 
768  # Consider Basic authentication if provider config is set this way
769  if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) {
770  $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))];
771  unset($token_params['client_secret']);
772  unset($token_params['client_id']);
773  }
774 
775  if (!empty($this->getCodeChallengeMethod()) && !empty($this->getCodeVerifier())) {
776  $headers = [];
777  unset($token_params['client_secret']);
778  $token_params = array_merge($token_params, array(
779  'client_id' => $this->clientID,
780  'code_verifier' => $this->getCodeVerifier()
781  ));
782  }
783 
784  // Convert token params to string format
785  $token_params = http_build_query($token_params, null, '&', $this->enc_type);
786 
787  $this->tokenResponse = json_decode($this->fetchURL($token_endpoint, $token_params, $headers));
788 
789  return $this->tokenResponse;
790  }
791 
799  public function refreshToken($refresh_token) {
800  $token_endpoint = $this->getProviderConfigValue('token_endpoint');
801 
802  $grant_type = 'refresh_token';
803 
804  $token_params = array(
805  'grant_type' => $grant_type,
806  'refresh_token' => $refresh_token,
807  'client_id' => $this->clientID,
808  'client_secret' => $this->clientSecret,
809  );
810 
811  // Convert token params to string format
812  $token_params = http_build_query($token_params, null, '&', $this->enc_type);
813 
814  $json = json_decode($this->fetchURL($token_endpoint, $token_params));
815 
816  if (isset($json->access_token)) {
817  $this->accessToken = $json->access_token;
818  }
819 
820  if (isset($json->refresh_token)) {
821  $this->refreshToken = $json->refresh_token;
822  }
823 
824  return $json;
825  }
826 
833  private function get_key_for_header($keys, $header) {
834  foreach ($keys as $key) {
835  if ($key->kty === 'RSA') {
836  if (!isset($header->kid) || $key->kid === $header->kid) {
837  return $key;
838  }
839  } else {
840  if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) {
841  return $key;
842  }
843  }
844  }
845  if ($this->additionalJwks) {
846  foreach ($this->additionalJwks as $key) {
847  if ($key->kty === 'RSA') {
848  if (!isset($header->kid) || $key->kid === $header->kid) {
849  return $key;
850  }
851  } else {
852  if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) {
853  return $key;
854  }
855  }
856  }
857  }
858  if (isset($header->kid)) {
859  throw new OpenIDConnectClientException('Unable to find a key for (algorithm, kid):' . $header->alg . ', ' . $header->kid . ')');
860  }
861 
862  throw new OpenIDConnectClientException('Unable to find a key for RSA');
863  }
864 
865 
875  private function verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $signatureType) {
876  if (!class_exists('\phpseclib\Crypt\RSA') && !class_exists('Crypt_RSA')) {
877  throw new OpenIDConnectClientException('Crypt_RSA support unavailable.');
878  }
879  if (!(property_exists($key, 'n') && property_exists($key, 'e'))) {
880  throw new OpenIDConnectClientException('Malformed key object');
881  }
882 
883  /* We already have base64url-encoded data, so re-encode it as
884  regular base64 and use the XML key format for simplicity.
885  */
886  $public_key_xml = "<RSAKeyValue>\r\n".
887  ' <Modulus>' . b64url2b64($key->n) . "</Modulus>\r\n" .
888  ' <Exponent>' . b64url2b64($key->e) . "</Exponent>\r\n" .
889  '</RSAKeyValue>';
890  if(class_exists('Crypt_RSA', false)) {
891  $rsa = new Crypt_RSA();
892  $rsa->setHash($hashtype);
893  if ($signatureType === 'PSS') {
894  $rsa->setMGFHash($hashtype);
895  }
896  $rsa->loadKey($public_key_xml, Crypt_RSA::PUBLIC_FORMAT_XML);
897  $rsa->signatureMode = $signatureType === 'PSS' ? Crypt_RSA::SIGNATURE_PSS : Crypt_RSA::SIGNATURE_PKCS1;
898  } else {
899  $rsa = new \phpseclib\Crypt\RSA();
900  $rsa->setHash($hashtype);
901  if ($signatureType === 'PSS') {
902  $rsa->setMGFHash($hashtype);
903  }
904  $rsa->loadKey($public_key_xml, \phpseclib\Crypt\RSA::PUBLIC_FORMAT_XML);
905  $rsa->signatureMode = $signatureType === 'PSS' ? \phpseclib\Crypt\RSA::SIGNATURE_PSS : \phpseclib\Crypt\RSA::SIGNATURE_PKCS1;
906  }
907  return $rsa->verify($payload, $signature);
908  }
909 
918  private function verifyHMACJWTsignature($hashtype, $key, $payload, $signature)
919  {
920  if (!function_exists('hash_hmac')) {
921  throw new OpenIDConnectClientException('hash_hmac support unavailable.');
922  }
923 
924  $expected=hash_hmac($hashtype, $payload, $key, true);
925 
926  if (function_exists('hash_equals')) {
927  return hash_equals($signature, $expected);
928  }
929 
930  return self::hashEquals($signature, $expected);
931  }
932 
938  public function verifyJWTsignature($jwt) {
939  if (!\is_string($jwt)) {
940  throw new OpenIDConnectClientException('Error token is not a string');
941  }
942  $parts = explode('.', $jwt);
943  if (!isset($parts[0])) {
944  throw new OpenIDConnectClientException('Error missing part 0 in token');
945  }
946  $signature = base64url_decode(array_pop($parts));
947  if (false === $signature || '' === $signature) {
948  throw new OpenIDConnectClientException('Error decoding signature from token');
949  }
950  $header = json_decode(base64url_decode($parts[0]));
951  if (null === $header || !\is_object($header)) {
952  throw new OpenIDConnectClientException('Error decoding JSON from token header');
953  }
954  $payload = implode('.', $parts);
955  $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri')));
956  if ($jwks === NULL) {
957  throw new OpenIDConnectClientException('Error decoding JSON from jwks_uri');
958  }
959  if (!isset($header->alg)) {
960  throw new OpenIDConnectClientException('Error missing signature type in token header');
961  }
962  switch ($header->alg) {
963  case 'RS256':
964  case 'PS256':
965  case 'RS384':
966  case 'RS512':
967  $hashtype = 'sha' . substr($header->alg, 2);
968  $signatureType = $header->alg === 'PS256' ? 'PSS' : '';
969 
970  $verified = $this->verifyRSAJWTsignature($hashtype,
971  $this->get_key_for_header($jwks->keys, $header),
972  $payload, $signature, $signatureType);
973  break;
974  case 'HS256':
975  case 'HS512':
976  case 'HS384':
977  $hashtype = 'SHA' . substr($header->alg, 2);
978  $verified = $this->verifyHMACJWTsignature($hashtype, $this->getClientSecret(), $payload, $signature);
979  break;
980  default:
981  throw new OpenIDConnectClientException('No support for signature type: ' . $header->alg);
982  }
983  return $verified;
984  }
985 
991  protected function verifyJWTclaims($claims, $accessToken = null) {
992  if(isset($claims->at_hash) && isset($accessToken)){
993  if(isset($this->getIdTokenHeader()->alg) && $this->getIdTokenHeader()->alg !== 'none'){
994  $bit = substr($this->getIdTokenHeader()->alg, 2, 3);
995  }else{
996  // TODO: Error case. throw exception???
997  $bit = '256';
998  }
999  $len = ((int)$bit)/16;
1000  $expected_at_hash = $this->urlEncode(substr(hash('sha'.$bit, $accessToken, true), 0, $len));
1001  }
1002  return (($this->issuerValidator->__invoke($claims->iss))
1003  && (($claims->aud === $this->clientID) || in_array($this->clientID, $claims->aud, true))
1004  && ($claims->nonce === $this->getNonce())
1005  && ( !isset($claims->exp) || ((gettype($claims->exp) === 'integer') && ($claims->exp >= time() - $this->leeway)))
1006  && ( !isset($claims->nbf) || ((gettype($claims->nbf) === 'integer') && ($claims->nbf <= time() + $this->leeway)))
1007  && ( !isset($claims->at_hash) || $claims->at_hash === $expected_at_hash )
1008  );
1009  }
1010 
1015  protected function urlEncode($str) {
1016  $enc = base64_encode($str);
1017  $enc = rtrim($enc, '=');
1018  $enc = strtr($enc, '+/', '-_');
1019  return $enc;
1020  }
1021 
1027  protected function decodeJWT($jwt, $section = 0) {
1028 
1029  $parts = explode('.', $jwt);
1030  return json_decode(base64url_decode($parts[$section]));
1031  }
1032 
1061  public function requestUserInfo($attribute = null) {
1062 
1063  $user_info_endpoint = $this->getProviderConfigValue('userinfo_endpoint');
1064  $schema = 'openid';
1065 
1066  $user_info_endpoint .= '?schema=' . $schema;
1067 
1068  //The accessToken has to be sent in the Authorization header.
1069  // Accept json to indicate response type
1070  $headers = ["Authorization: Bearer {$this->accessToken}",
1071  'Accept: application/json'];
1072 
1073  $user_json = json_decode($this->fetchURL($user_info_endpoint,null,$headers));
1074  if ($this->getResponseCode() <> 200) {
1075  throw new OpenIDConnectClientException('The communication to retrieve user data has failed with status code '.$this->getResponseCode());
1076  }
1077  $this->userInfo = $user_json;
1078 
1079  if($attribute === null) {
1080  return $this->userInfo;
1081  }
1082 
1083  if (property_exists($this->userInfo, $attribute)) {
1084  return $this->userInfo->$attribute;
1085  }
1086 
1087  return null;
1088  }
1089 
1109  public function getVerifiedClaims($attribute = null) {
1110 
1111  if($attribute === null) {
1112  return $this->verifiedClaims;
1113  }
1114 
1115  if (property_exists($this->verifiedClaims, $attribute)) {
1116  return $this->verifiedClaims->$attribute;
1117  }
1118 
1119  return null;
1120  }
1121 
1129  protected function fetchURL($url, $post_body = null, $headers = array()) {
1130 
1131 
1132  // OK cool - then let's create a new cURL resource handle
1133  $ch = curl_init();
1134 
1135  // Determine whether this is a GET or POST
1136  if ($post_body !== null) {
1137  // curl_setopt($ch, CURLOPT_POST, 1);
1138  // Alows to keep the POST method even after redirect
1139  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
1140  curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body);
1141 
1142  // Default content type is form encoded
1143  $content_type = 'application/x-www-form-urlencoded';
1144 
1145  // Determine if this is a JSON payload and add the appropriate content type
1146  if (is_object(json_decode($post_body))) {
1147  $content_type = 'application/json';
1148  }
1149 
1150  // Add POST-specific headers
1151  $headers[] = "Content-Type: {$content_type}";
1152 
1153  }
1154 
1155  // If we set some headers include them
1156  if(count($headers) > 0) {
1157  curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
1158  }
1159 
1160  // Set URL to download
1161  curl_setopt($ch, CURLOPT_URL, $url);
1162 
1163  if (isset($this->httpProxy)) {
1164  curl_setopt($ch, CURLOPT_PROXY, $this->httpProxy);
1165  }
1166 
1167  // Include header in result? (0 = yes, 1 = no)
1168  curl_setopt($ch, CURLOPT_HEADER, 0);
1169 
1170  // Allows to follow redirect
1171  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1172 
1177  if (isset($this->certPath)) {
1178  curl_setopt($ch, CURLOPT_CAINFO, $this->certPath);
1179  }
1180 
1181  if($this->verifyHost) {
1182  curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
1183  } else {
1184  curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
1185  }
1186 
1187  if($this->verifyPeer) {
1188  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
1189  } else {
1190  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
1191  }
1192 
1193  // Should cURL return or print out the data? (true = return, false = print)
1194  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1195 
1196  // Timeout in seconds
1197  curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeOut);
1198 
1199  // Download the given URL, and return output
1200  $output = curl_exec($ch);
1201 
1202  // HTTP Response code from server may be required from subclass
1203  $info = curl_getinfo($ch);
1204  $this->responseCode = $info['http_code'];
1205 
1206  if ($output === false) {
1207  throw new OpenIDConnectClientException('Curl error: (' . curl_errno($ch) . ') ' . curl_error($ch));
1208  }
1209 
1210  // Close the cURL resource, and free system resources
1211  curl_close($ch);
1212 
1213  return $output;
1214  }
1215 
1221  public function getWellKnownIssuer($appendSlash = false) {
1222 
1223  return $this->getWellKnownConfigValue('issuer') . ($appendSlash ? '/' : '');
1224  }
1225 
1230  public function getIssuer() {
1231 
1232  if (!isset($this->providerConfig['issuer'])) {
1233  throw new OpenIDConnectClientException('The issuer has not been set');
1234  }
1235 
1236  return $this->providerConfig['issuer'];
1237  }
1238 
1243  public function getProviderURL() {
1244  if (!isset($this->providerConfig['providerUrl'])) {
1245  throw new OpenIDConnectClientException('The provider URL has not been set');
1246  }
1247 
1248  return $this->providerConfig['providerUrl'];
1249  }
1250 
1254  public function redirect($url) {
1255  header('Location: ' . $url);
1256  exit;
1257  }
1258 
1262  public function setHttpProxy($httpProxy) {
1263  $this->httpProxy = $httpProxy;
1264  }
1265 
1269  public function setCertPath($certPath) {
1270  $this->certPath = $certPath;
1271  }
1272 
1276  public function getCertPath()
1277  {
1278  return $this->certPath;
1279  }
1280 
1284  public function setVerifyPeer($verifyPeer) {
1285  $this->verifyPeer = $verifyPeer;
1286  }
1287 
1291  public function setVerifyHost($verifyHost) {
1292  $this->verifyHost = $verifyHost;
1293  }
1294 
1298  public function getVerifyHost()
1299  {
1300  return $this->verifyHost;
1301  }
1302 
1306  public function getVerifyPeer()
1307  {
1308  return $this->verifyPeer;
1309  }
1310 
1318  public function setIssuerValidator($issuerValidator){
1319  $this->issuerValidator = $issuerValidator;
1320  }
1321 
1325  public function setAllowImplicitFlow($allowImplicitFlow) {
1326  $this->allowImplicitFlow = $allowImplicitFlow;
1327  }
1328 
1332  public function getAllowImplicitFlow()
1333  {
1334  return $this->allowImplicitFlow;
1335  }
1336 
1344  public function providerConfigParam($array) {
1345  $this->providerConfig = array_merge($this->providerConfig, $array);
1346  }
1347 
1351  public function setClientSecret($clientSecret) {
1352  $this->clientSecret = $clientSecret;
1353  }
1354 
1358  public function setClientID($clientID) {
1359  $this->clientID = $clientID;
1360  }
1361 
1362 
1368  public function register() {
1369 
1370  $registration_endpoint = $this->getProviderConfigValue('registration_endpoint');
1371 
1372  $send_object = (object ) array_merge($this->registrationParams, array(
1373  'redirect_uris' => array($this->getRedirectURL()),
1374  'client_name' => $this->getClientName()
1375  ));
1376 
1377  $response = $this->fetchURL($registration_endpoint, json_encode($send_object));
1378 
1379  $json_response = json_decode($response);
1380 
1381  // Throw some errors if we encounter them
1382  if ($json_response === false) {
1383  throw new OpenIDConnectClientException('Error registering: JSON response received from the server was invalid.');
1384  }
1385 
1386  if (isset($json_response->{'error_description'})) {
1387  throw new OpenIDConnectClientException($json_response->{'error_description'});
1388  }
1389 
1390  $this->setClientID($json_response->{'client_id'});
1391 
1392  // The OpenID Connect Dynamic registration protocol makes the client secret optional
1393  // and provides a registration access token and URI endpoint if it is not present
1394  if (isset($json_response->{'client_secret'})) {
1395  $this->setClientSecret($json_response->{'client_secret'});
1396  } else {
1397  throw new OpenIDConnectClientException('Error registering:
1398  Please contact the OpenID Connect provider and obtain a Client ID and Secret directly from them');
1399  }
1400 
1401  }
1402 
1414  public function introspectToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) {
1415  $introspection_endpoint = $this->getProviderConfigValue('introspection_endpoint');
1416 
1417  $post_data = array(
1418  'token' => $token,
1419  );
1420  if ($token_type_hint) {
1421  $post_data['token_type_hint'] = $token_type_hint;
1422  }
1423  $clientId = $clientId !== null ? $clientId : $this->clientID;
1425 
1426  // Convert token params to string format
1427  $post_params = http_build_query($post_data, null, '&');
1428  $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)),
1429  'Accept: application/json'];
1430 
1431  return json_decode($this->fetchURL($introspection_endpoint, $post_params, $headers));
1432  }
1433 
1445  public function revokeToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) {
1446  $revocation_endpoint = $this->getProviderConfigValue('revocation_endpoint');
1447 
1448  $post_data = array(
1449  'token' => $token,
1450  );
1451  if ($token_type_hint) {
1452  $post_data['token_type_hint'] = $token_type_hint;
1453  }
1454  $clientId = $clientId !== null ? $clientId : $this->clientID;
1456 
1457  // Convert token params to string format
1458  $post_params = http_build_query($post_data, null, '&');
1459  $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)),
1460  'Accept: application/json'];
1461 
1462  return json_decode($this->fetchURL($revocation_endpoint, $post_params, $headers));
1463  }
1464 
1468  public function getClientName() {
1469  return $this->clientName;
1470  }
1471 
1475  public function setClientName($clientName) {
1476  $this->clientName = $clientName;
1477  }
1478 
1482  public function getClientID() {
1483  return $this->clientID;
1484  }
1485 
1489  public function getClientSecret() {
1490  return $this->clientSecret;
1491  }
1492 
1496  public function canVerifySignatures() {
1497  return class_exists('\phpseclib\Crypt\RSA') || class_exists('Crypt_RSA');
1498  }
1499 
1508  public function setAccessToken($accessToken) {
1509  $this->accessToken = $accessToken;
1510  }
1511 
1515  public function getAccessToken() {
1516  return $this->accessToken;
1517  }
1518 
1522  public function getRefreshToken() {
1523  return $this->refreshToken;
1524  }
1525 
1529  public function getIdToken() {
1530  return $this->idToken;
1531  }
1532 
1536  public function getAccessTokenHeader() {
1537  return $this->decodeJWT($this->accessToken);
1538  }
1539 
1543  public function getAccessTokenPayload() {
1544  return $this->decodeJWT($this->accessToken, 1);
1545  }
1546 
1550  public function getIdTokenHeader() {
1551  return $this->decodeJWT($this->idToken);
1552  }
1553 
1557  public function getIdTokenPayload() {
1558  return $this->decodeJWT($this->idToken, 1);
1559  }
1560 
1564  public function getTokenResponse() {
1565  return $this->tokenResponse;
1566  }
1567 
1574  protected function setNonce($nonce) {
1575  $this->setSessionKey('openid_connect_nonce', $nonce);
1576  return $nonce;
1577  }
1578 
1584  protected function getNonce() {
1585  return $this->getSessionKey('openid_connect_nonce');
1586  }
1587 
1593  protected function unsetNonce() {
1594  $this->unsetSessionKey('openid_connect_nonce');
1595  }
1596 
1603  protected function setState($state) {
1604  $this->setSessionKey('openid_connect_state', $state);
1605  return $state;
1606  }
1607 
1613  protected function getState() {
1614  return $this->getSessionKey('openid_connect_state');
1615  }
1616 
1622  protected function unsetState() {
1623  $this->unsetSessionKey('openid_connect_state');
1624  }
1625 
1632  protected function setCodeVerifier($codeVerifier) {
1633  $this->setSessionKey('openid_connect_code_verifier', $codeVerifier);
1634  return $codeVerifier;
1635  }
1636 
1642  protected function getCodeVerifier() {
1643  return $this->getSessionKey('openid_connect_code_verifier');
1644  }
1645 
1651  protected function unsetCodeVerifier() {
1652  $this->unsetSessionKey('openid_connect_code_verifier');
1653  }
1654 
1660  public function getResponseCode()
1661  {
1662  return $this->responseCode;
1663  }
1664 
1670  public function setTimeout($timeout)
1671  {
1672  $this->timeOut = $timeout;
1673  }
1674 
1678  public function getTimeout()
1679  {
1680  return $this->timeOut;
1681  }
1682 
1688  private static function safeLength($str)
1689  {
1690  if (function_exists('mb_strlen')) {
1691  return mb_strlen($str, '8bit');
1692  }
1693  return strlen($str);
1694  }
1695 
1702  private static function hashEquals($str1, $str2)
1703  {
1704  $len1=static::safeLength($str1);
1705  $len2=static::safeLength($str2);
1706 
1707  //compare strings without any early abort...
1708  $len = min($len1, $len2);
1709  $status = 0;
1710  for ($i = 0; $i < $len; $i++) {
1711  $status |= (ord($str1[$i]) ^ ord($str2[$i]));
1712  }
1713  //if strings were different lengths, we fail
1714  $status |= ($len1 ^ $len2);
1715  return ($status === 0);
1716  }
1717 
1721  protected function startSession() {
1722  if (!isset($_SESSION)) {
1723  @session_start();
1724  }
1725  }
1726 
1727  protected function commitSession() {
1728  $this->startSession();
1729 
1730  session_write_close();
1731  }
1732 
1733  protected function getSessionKey($key) {
1734  $this->startSession();
1735 
1736  return $_SESSION[$key];
1737  }
1738 
1739  protected function setSessionKey($key, $value) {
1740  $this->startSession();
1741 
1742  $_SESSION[$key] = $value;
1743  }
1744 
1745  protected function unsetSessionKey($key) {
1746  $this->startSession();
1747 
1748  unset($_SESSION[$key]);
1749  }
1750 
1751  public function setUrlEncoding($curEncoding)
1752  {
1753  switch ($curEncoding)
1754  {
1755  case PHP_QUERY_RFC1738:
1756  $this->enc_type = PHP_QUERY_RFC1738;
1757  break;
1758 
1759  case PHP_QUERY_RFC3986:
1760  $this->enc_type = PHP_QUERY_RFC3986;
1761  break;
1762 
1763  default:
1764  break;
1765  }
1766 
1767  }
1768 
1772  public function getScopes()
1773  {
1774  return $this->scopes;
1775  }
1776 
1780  public function getResponseTypes()
1781  {
1782  return $this->responseTypes;
1783  }
1784 
1788  public function getAuthParams()
1789  {
1790  return $this->authParams;
1791  }
1792 
1796  public function getIssuerValidator()
1797  {
1798  return $this->issuerValidator;
1799  }
1800 
1804  public function getLeeway()
1805  {
1806  return $this->leeway;
1807  }
1808 
1812  public function getCodeChallengeMethod() {
1813  return $this->codeChallengeMethod;
1814  }
1815 
1819  public function setCodeChallengeMethod($codeChallengeMethod) {
1820  $this->codeChallengeMethod = $codeChallengeMethod;
1821  }
1822 }
verifyHMACJWTsignature($hashtype, $key, $payload, $signature)
getResponseCode()
Get the response code from last action/curl request.
getCodeVerifier()
Get stored codeVerifier.
if((!isset($_SERVER['DOCUMENT_ROOT'])) OR(empty($_SERVER['DOCUMENT_ROOT']))) $_SERVER['DOCUMENT_ROOT']
setIssuerValidator($issuerValidator)
Use this for custom issuer validation The given function should accept the issuer string from the JWT...
if(!class_exists('\phpseclib\Crypt\RSA') &&!class_exists('Crypt_RSA')) base64url_decode($base64url)
JWT signature verification support by Jonathan Reed jdreed@mit.edu Licensed under the same license as...
introspectToken($token, $token_type_hint='', $clientId=null, $clientSecret=null)
Introspect a given token - either access token or refresh token.
$_SESSION["AccountId"]
refreshToken($refresh_token)
Requests Access token with refresh token.
providerConfigParam($array)
Use this to alter a provider&#39;s endpoints and other attributes.
requestTokens($code)
Requests ID and Access tokens.
$code
Definition: example_050.php:99
OpenIDConnect Exception Class.
signOut($accessToken, $redirect)
It calls the end-session endpoint of the OpenID Connect provider to notify the OpenID Connect provide...
setAccessToken($accessToken)
Set the access token.
requestClientCredentialsToken()
Requests a client credentials token.
getProviderConfigValue($param, $default=null)
Get&#39;s anything that we need configuration wise including endpoints, and other values.
setTimeout($timeout)
Set timeout (seconds)
static safeLength($str)
Safely calculate length of binary string.
$keys
requestResourceOwnerToken($bClientAuth=FALSE)
Requests a resource owner token (Defined in https://tools.ietf.org/html/rfc6749#section-4.3)
$section
Definition: Utf8Test.php:83
Copyright MITRE 2020.
fetchURL($url, $post_body=null, $headers=array())
if(!array_key_exists('stateid', $_REQUEST)) $state
Handle linkback() response from LinkedIn.
Definition: linkback.php:10
revokeToken($token, $token_type_hint='', $clientId=null, $clientSecret=null)
Revoke a given token - either access token or refresh token.
setWellKnownConfigParameters(array $params=[])
Set optionnal parameters for .well-known/openid-configuration.
getWellKnownConfigValue($param, $default=null)
Get&#39;s anything that we need configuration wise including endpoints, and other values.
static hashEquals($str1, $str2)
Where has_equals is not available, this provides a timing-attack safe string comparison.
startSession()
Use session to manage a nonce.
catch(Exception $e) if(!($request instanceof \SAML2\ArtifactResolve)) $issuer
setCodeVerifier($codeVerifier)
Stores $codeVerifier.
$default
Definition: build.php:20
__construct($provider_url=null, $client_id=null, $client_secret=null, $issuer=null)
const SIGNATURE_PKCS1
Use the PKCS#1 scheme by default.
Definition: RSA.php:124
b64url2b64($base64url)
Per RFC4648, "base64 encoding with URL-safe and filename-safe alphabet".
exit
Definition: backend.php:16
setAllowImplicitFlow($allowImplicitFlow)
$i
Definition: disco.tpl.php:19
verifyJWTclaims($claims, $accessToken=null)
$url
setCodeChallengeMethod($codeChallengeMethod)
$client_id
$info
Definition: index.php:5
$response
hash(StreamInterface $stream, $algo, $rawOutput=false)
Calculate a hash of a Stream.
Definition: functions.php:406
$key
Definition: croninfo.php:18
generateRandString()
Used for arbitrary value generation for nonces and state.
Require the CURL and JSON PHP extensions to be installed.
verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $signatureType)
if(array_key_exists('provider', $_GET)) elseif(array_key_exists('provider', $_SESSION)) if(!in_array($providerName, ['Google', 'Microsoft', 'Yahoo'])) $clientId
getRedirectURL()
Gets the URL of the current page we are on, encodes, and returns it.
const SIGNATURE_PSS
#-
Definition: RSA.php:117