ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
OpenIDConnectClient.php
Go to the documentation of this file.
1<?php
23namespace Jumbojett;
24
34if (!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
44function base64url_decode($base64url) {
45 return base64_decode(b64url2b64($base64url));
46}
47
56function 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
69class OpenIDConnectClientException extends \Exception
70{
71
72}
73
77if (!function_exists('curl_init')) {
78 throw new OpenIDConnectClientException('OpenIDConnect needs the CURL PHP extension.');
79}
80if (!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
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
141
145 protected $idToken;
146
151
155 private $scopes = array();
156
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;
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
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
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}
catch(Exception $e) if(!($request instanceof \SAML2\ArtifactResolve)) $issuer
$section
Definition: Utf8Test.php:83
if(!array_key_exists('stateid', $_REQUEST)) $state
Handle linkback() response from LinkedIn.
Definition: linkback.php:10
exit
Definition: backend.php:16
$default
Definition: build.php:20
$_SESSION["AccountId"]
An exception for terminatinating execution or to throw for unit testing.
Require the CURL and JSON PHP extensions to be installed.
getRedirectURL()
Gets the URL of the current page we are on, encodes, and returns it.
providerConfigParam($array)
Use this to alter a provider's endpoints and other attributes.
requestClientCredentialsToken()
Requests a client credentials token.
fetchURL($url, $post_body=null, $headers=array())
introspectToken($token, $token_type_hint='', $clientId=null, $clientSecret=null)
Introspect a given token - either access token or refresh token.
requestTokens($code)
Requests ID and Access tokens.
setAccessToken($accessToken)
Set the access token.
setCodeChallengeMethod($codeChallengeMethod)
getResponseCode()
Get the response code from last action/curl request.
static safeLength($str)
Safely calculate length of binary string.
revokeToken($token, $token_type_hint='', $clientId=null, $clientSecret=null)
Revoke a given token - either access token or refresh token.
startSession()
Use session to manage a nonce.
__construct($provider_url=null, $client_id=null, $client_secret=null, $issuer=null)
getProviderConfigValue($param, $default=null)
Get's anything that we need configuration wise including endpoints, and other values.
generateRandString()
Used for arbitrary value generation for nonces and state.
verifyHMACJWTsignature($hashtype, $key, $payload, $signature)
refreshToken($refresh_token)
Requests Access token with refresh token.
getWellKnownConfigValue($param, $default=null)
Get's anything that we need configuration wise including endpoints, and other values.
setTimeout($timeout)
Set timeout (seconds)
getCodeVerifier()
Get stored codeVerifier.
requestResourceOwnerToken($bClientAuth=FALSE)
Requests a resource owner token (Defined in https://tools.ietf.org/html/rfc6749#section-4....
signOut($accessToken, $redirect)
It calls the end-session endpoint of the OpenID Connect provider to notify the OpenID Connect provide...
setWellKnownConfigParameters(array $params=[])
Set optionnal parameters for .well-known/openid-configuration.
setIssuerValidator($issuerValidator)
Use this for custom issuer validation The given function should accept the issuer string from the JWT...
static hashEquals($str1, $str2)
Where has_equals is not available, this provides a timing-attack safe string comparison.
verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $signatureType)
verifyJWTclaims($claims, $accessToken=null)
setCodeVerifier($codeVerifier)
Stores $codeVerifier.
setAllowImplicitFlow($allowImplicitFlow)
const SIGNATURE_PSS
#-
Definition: RSA.php:117
const SIGNATURE_PKCS1
Use the PKCS#1 scheme by default.
Definition: RSA.php:124
$key
Definition: croninfo.php:18
$i
Definition: disco.tpl.php:19
$code
Definition: example_050.php:99
$client_id
$info
Definition: index.php:5
$keys
hash(StreamInterface $stream, $algo, $rawOutput=false)
Calculate a hash of a Stream.
Definition: functions.php:406
Copyright MITRE 2020.
b64url2b64($base64url)
Per RFC4648, "base64 encoding with URL-safe and filename-safe alphabet".
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...
if(array_key_exists('provider', $_GET)) elseif(array_key_exists( 'provider', $_SESSION)) if(!in_array($providerName, ['Google', 'Microsoft', 'Yahoo'])) $clientId
$url
$response
if((!isset($_SERVER['DOCUMENT_ROOT'])) OR(empty($_SERVER['DOCUMENT_ROOT']))) $_SERVER['DOCUMENT_ROOT']