From a5510d2801d51b62221071751403ad9091d9374c Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Mon, 13 Jun 2022 13:56:52 +0200 Subject: [PATCH 01/62] Added userInfo response type check to handle signed and encrypted responses --- src/OpenIDConnectClient.php | 106 ++++++++++++++---- src/interfaces/HandleJweResponseInterface.php | 13 +++ 2 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 src/interfaces/HandleJweResponseInterface.php diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 0b8890bb..51ab3bc6 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -22,6 +22,8 @@ namespace Jumbojett; +use Jumbojett\Interfaces\HandleJweResponseInterface; + /** * * JWT signature verification support by Jonathan Reed @@ -159,6 +161,11 @@ class OpenIDConnectClient */ private $responseCode; + /** + * @var string|null Content type from the server + */ + private $responseContentType; + /** * @var array holds response types */ @@ -242,6 +249,11 @@ class OpenIDConnectClient */ private $pkceAlgs = array('S256' => 'sha256', 'plain' => false); + /** + * @var HandleJweResponseInterface|null + */ + private $jweResponseHandler; + /** * @param $provider_url string optional * @@ -282,6 +294,13 @@ public function setResponseTypes($response_types) { $this->responseTypes = array_merge($this->responseTypes, (array)$response_types); } + /** + * @param HandleJweResponseInterface $jwe_response_handler + */ + public function setJweResponseHandler($jwe_response_handler) { + $this->jweResponseHandler = $jwe_response_handler; + } + /** * @return bool * @throws OpenIDConnectClientException @@ -323,16 +342,7 @@ public function authenticate() { $claims = $this->decodeJWT($token_json->id_token, 1); // Verify the signature - if ($this->canVerifySignatures()) { - if (!$this->getProviderConfigValue('jwks_uri')) { - throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); - } - if (!$this->verifyJWTsignature($token_json->id_token)) { - throw new OpenIDConnectClientException ('Unable to verify signature'); - } - } else { - user_error('Warning: JWT signature verification unavailable.'); - } + $this->verifySignatures($token_json->id_token); // Save the id token $this->idToken = $token_json->id_token; @@ -385,16 +395,7 @@ public function authenticate() { $claims = $this->decodeJWT($id_token, 1); // Verify the signature - if ($this->canVerifySignatures()) { - if (!$this->getProviderConfigValue('jwks_uri')) { - throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); - } - if (!$this->verifyJWTsignature($id_token)) { - throw new OpenIDConnectClientException ('Unable to verify signature'); - } - } else { - user_error('Warning: JWT signature verification unavailable.'); - } + $this->verifySignatures($id_token); // Save the id token $this->idToken = $id_token; @@ -793,7 +794,7 @@ protected function requestTokens($code, $headers = array()) { if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { $authorizationHeader = 'Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret)); unset($token_params['client_secret']); - unset($token_params['client_id']); + unset($token_params['client_id']); } $ccm = $this->getCodeChallengeMethod(); @@ -1037,6 +1038,25 @@ public function verifyJWTsignature($jwt) { return $verified; } + /** + * @param string $jwt encoded JWT + * @return void + * @throws OpenIDConnectClientException + */ + public function verifySignatures($jwt) + { + if ($this->canVerifySignatures()) { + if (!$this->getProviderConfigValue('jwks_uri')) { + throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); + } + if (!$this->verifyJWTsignature($jwt)) { + throw new OpenIDConnectClientException ('Unable to verify signature'); + } + } else { + user_error('Warning: JWT signature verification unavailable.'); + } + } + /** * @param string $iss * @return bool @@ -1137,10 +1157,41 @@ public function requestUserInfo($attribute = null) { $headers = ["Authorization: Bearer {$this->accessToken}", 'Accept: application/json']; - $user_json = json_decode($this->fetchURL($user_info_endpoint,null,$headers)); + $response = $this->fetchURL($user_info_endpoint,null,$headers); if ($this->getResponseCode() <> 200) { throw new OpenIDConnectClientException('The communication to retrieve user data has failed with status code '.$this->getResponseCode()); } + + // When we receive application/jwt, the UserInfo Response is signed and/or encrypted. + if ($this->getResponseContentType() === 'application/jwt' ) { + // Check if the response is encrypted + $jwtHeaders = $this->decodeJWT($response); + if (isset($jwtHeaders->enc)) { + // If we don't have a JWE handler then throw error + if ($this->jweResponseHandler === null) { + throw new OpenIDConnectClientException('JWE response handler not set'); + } + + // Handle JWE + $jwt = $this->jweResponseHandler->handleJweResponse($response); + } + + // Verify the signature + $this->verifySignatures($jwt); + + // Get claims from JWT + $claims = $this->decodeJWT($jwt, 1); + + // Verify the JWT claims + if (!$this->verifyJWTclaims($claims)) { + throw new OpenIDConnectClientException('Invalid JWT signature'); + } + + $user_json = $claims; + } else { + $user_json = json_decode($response); + } + $this->userInfo = $user_json; if($attribute === null) { @@ -1269,6 +1320,7 @@ protected function fetchURL($url, $post_body = null, $headers = array()) { // HTTP Response code from server may be required from subclass $info = curl_getinfo($ch); $this->responseCode = $info['http_code']; + $this->responseContentType = $info['content_type']; if ($output === false) { throw new OpenIDConnectClientException('Curl error: (' . curl_errno($ch) . ') ' . curl_error($ch)); @@ -1747,6 +1799,16 @@ public function getResponseCode() return $this->responseCode; } + /** + * Get the content type from last action/curl request. + * + * @return string|null + */ + public function getResponseContentType() + { + return $this->responseContentType; + } + /** * Set timeout (seconds) * diff --git a/src/interfaces/HandleJweResponseInterface.php b/src/interfaces/HandleJweResponseInterface.php new file mode 100644 index 00000000..3eef5b2c --- /dev/null +++ b/src/interfaces/HandleJweResponseInterface.php @@ -0,0 +1,13 @@ + Date: Thu, 21 Jul 2022 19:44:32 +0300 Subject: [PATCH 02/62] feat: add client_secret_jwt support --- src/OpenIDConnectClient.php | 84 +++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 6fd6d117..a84b8a09 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -798,6 +798,21 @@ protected function requestTokens($code, $headers = array()) { unset($token_params['client_id']); } + if (in_array('client_secret_jwt', $token_endpoint_auth_methods_supported, true)) { + $client_assertion_type = $this->getProviderConfigValue('client_assertion_type'); + + if(isset($this->providerConfig['client_assertion'])){ + $client_assertion = $this->getProviderConfigValue('client_assertion'); + } + else{ + $client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('token_endpoint')); + } + + $token_params['client_assertion_type'] = $client_assertion_type; + $token_params['client_assertion'] = $client_assertion; + unset($token_params['client_secret']); + } + $ccm = $this->getCodeChallengeMethod(); $cv = $this->getCodeVerifier(); if (!empty($ccm) && !empty($cv)) { @@ -897,6 +912,21 @@ public function refreshToken($refresh_token) { unset($token_params['client_id']); } + if (in_array('client_secret_jwt', $token_endpoint_auth_methods_supported, true)) { + $client_assertion_type = $this->getProviderConfigValue('client_assertion_type'); + $client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('token_endpoint')); + + $token_params["grant_type"] = "urn:ietf:params:oauth:grant-type:token-exchange"; + $token_params["subject_token"] = $refresh_token; + $token_params["audience"] = $this->clientID; + $token_params["subject_token_type"] = "urn:ietf:params:oauth:token-type:refresh_token"; + $token_params["requested_token_type"] = "urn:ietf:params:oauth:token-type:access_token"; + $token_params['client_assertion_type']=$client_assertion_type; + $token_params['client_assertion'] = $client_assertion; + + unset($token_params['client_secret']); + unset($token_params['client_id']); + } // Convert token params to string format $token_params = http_build_query($token_params, '', '&', $this->encType); @@ -1059,11 +1089,11 @@ public function verifyJWTsignature($jwt) { switch ($header->alg) { case 'RS256': case 'PS256': + case 'PS512': case 'RS384': case 'RS512': $hashtype = 'sha' . substr($header->alg, 2); - $signatureType = $header->alg === 'PS256' ? 'PSS' : ''; - + $signatureType = $header->alg === 'PS256' || $header->alg === 'PS512' ? 'PSS' : ''; if (isset($header->jwk)) { $jwk = $header->jwk; } else { @@ -1546,6 +1576,7 @@ public function register() { */ public function introspectToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) { $introspection_endpoint = $this->getProviderConfigValue('introspection_endpoint'); + $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); $post_data = ['token' => $token]; @@ -1556,10 +1587,20 @@ public function introspectToken($token, $token_type_hint = '', $clientId = null, $clientSecret = $clientSecret !== null ? $clientSecret : $this->clientSecret; // Convert token params to string format - $post_params = http_build_query($post_data, '', '&'); $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), 'Accept: application/json']; + if (in_array('client_secret_jwt', $token_endpoint_auth_methods_supported, true)) { + $client_assertion_type = $this->getProviderConfigValue('client_assertion_type'); + $client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('introspection_endpoint')); + + $post_data['client_assertion_type']=$client_assertion_type; + $post_data['client_assertion'] = $client_assertion; + $headers = ['Accept: application/json']; + } + + $post_params = http_build_query($post_data, '', '&'); + return json_decode($this->fetchURL($introspection_endpoint, $post_params, $headers)); } @@ -1877,6 +1918,43 @@ protected function unsetSessionKey($key) { unset($_SESSION[$key]); } + protected function getJWTClientAssertion($aud) { + $jti = hash('sha256',bin2hex(random_bytes(64))); + + $now = time(); + + $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']); + $payload = json_encode([ + 'sub' => $this->getClientID(), + 'iss' => $this->getClientID(), + 'aud' => $aud, + 'jti' => $jti, + 'exp' => $now + 3600, + 'iat' => $now, + ]); + // Encode Header to Base64Url String + $base64UrlHeader = $this->urlEncode($header); + + + // Encode Payload to Base64Url String + $base64UrlPayload = $this->urlEncode($payload); + + // Create Signature Hash + $signature = hash_hmac( + 'sha256', + $base64UrlHeader . "." . $base64UrlPayload, + $this->getClientSecret(), + true + ); + + // Encode Signature to Base64Url String + $base64UrlSignature = $this->urlEncode($signature); + + $jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; + + return $jwt; + } + public function setUrlEncoding($curEncoding) { switch ($curEncoding) { From 53057138f712f9b5aa31ee8e255bbf6cbf1afeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 5 Aug 2022 15:26:19 +0200 Subject: [PATCH 03/62] fix: use empty array as fallback if the IdP is not exposing/supporting code_challenge_methods_supported in well-known configuration --- src/OpenIDConnectClient.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 6fd6d117..2bad8b57 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -492,7 +492,7 @@ protected function addAdditionalJwk($jwk) { * @param string $param * @param string $default optional * @throws OpenIDConnectClientException - * @return string + * @return string|array * */ protected function getProviderConfigValue($param, $default = null) { @@ -682,18 +682,18 @@ private function requestAuthorization() { } // If the client supports Proof Key for Code Exchange (PKCE) - $ccm = $this->getCodeChallengeMethod(); - if (!empty($ccm) && in_array($this->getCodeChallengeMethod(), $this->getProviderConfigValue('code_challenge_methods_supported'))) { + $codeChallengeMethod = $this->getCodeChallengeMethod(); + if (!empty($codeChallengeMethod) && in_array($codeChallengeMethod, $this->getProviderConfigValue('code_challenge_methods_supported', []), true)) { $codeVerifier = bin2hex(random_bytes(64)); $this->setCodeVerifier($codeVerifier); - if (!empty($this->pkceAlgs[$this->getCodeChallengeMethod()])) { - $codeChallenge = rtrim(strtr(base64_encode(hash($this->pkceAlgs[$this->getCodeChallengeMethod()], $codeVerifier, true)), '+/', '-_'), '='); + if (!empty($this->pkceAlgs[$codeChallengeMethod])) { + $codeChallenge = rtrim(strtr(base64_encode(hash($this->pkceAlgs[$codeChallengeMethod], $codeVerifier, true)), '+/', '-_'), '='); } else { $codeChallenge = $codeVerifier; } $auth_params = array_merge($auth_params, [ 'code_challenge' => $codeChallenge, - 'code_challenge_method' => $this->getCodeChallengeMethod() + 'code_challenge_method' => $codeChallengeMethod ]); } From 1f800145f64e1151f9d623dedc71a66f4143341f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 5 Aug 2022 15:48:50 +0200 Subject: [PATCH 04/62] Release 0.9.8 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f09105..b2d8ccaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +## [0.9.8] + +## Fixed + +* Do not use PKCE if IdP does not support it. #317 + ## [0.9.7] ### Added From 1956de3b320037dc84b69f42c21f3c08244e77fe Mon Sep 17 00:00:00 2001 From: timvisee Date: Fri, 26 Aug 2022 16:13:34 +0200 Subject: [PATCH 05/62] Use consistent spacing --- src/OpenIDConnectClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 2bad8b57..3fff8d4f 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -558,7 +558,7 @@ public function setWellKnownConfigParameters(array $params = []){ /** * @param string $url Sets redirect URL for auth flow */ - public function setRedirectURL ($url) { + public function setRedirectURL($url) { if (parse_url($url,PHP_URL_HOST) !== false) { $this->redirectURL = $url; } From ffb8d383a3e9431dd8bea3583948625686009768 Mon Sep 17 00:00:00 2001 From: Andrei Popa <49556356+andreipopa-who@users.noreply.github.com> Date: Thu, 15 Sep 2022 13:49:04 +0300 Subject: [PATCH 06/62] linting: Update src/OpenIDConnectClient.php Co-authored-by: Rick Lambrechts --- src/OpenIDConnectClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index a84b8a09..fcd03763 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -811,7 +811,7 @@ protected function requestTokens($code, $headers = array()) { $token_params['client_assertion_type'] = $client_assertion_type; $token_params['client_assertion'] = $client_assertion; unset($token_params['client_secret']); - } + } $ccm = $this->getCodeChallengeMethod(); $cv = $this->getCodeVerifier(); From e8db27450b7c999736a94cd74baa47c336b46934 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Thu, 15 Sep 2022 14:16:31 +0200 Subject: [PATCH 07/62] use correct types --- src/OpenIDConnectClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 81567375..312899fc 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1010,7 +1010,7 @@ private function verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $s /** * @param string $hashtype - * @param object $key + * @param string $key * @param $payload * @param $signature * @return bool @@ -1068,7 +1068,7 @@ public function verifyJWTsignature($jwt) { if (isset($header->jwk)) { $jwk = $header->jwk; } else { - $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri'))); + $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri')), false); if ($jwks === NULL) { throw new OpenIDConnectClientException('Error decoding JSON from jwks_uri'); } From 1b4b69fea60dd28e735659c4ca453a75ec3e556a Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Thu, 15 Sep 2022 14:17:07 +0200 Subject: [PATCH 08/62] set response as jwt when not jwe --- src/OpenIDConnectClient.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 312899fc..5ea7fada 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1227,6 +1227,9 @@ public function requestUserInfo($attribute = null) { // Handle JWE $jwt = $this->jweResponseHandler->handleJweResponse($response); + } else { + // If the response is not encrypted then it must be signed + $jwt = $response; } // Verify the signature From 5ed9bd9ba2d89a16f22274813a3b757ab38c660d Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Thu, 15 Sep 2022 22:38:30 +0200 Subject: [PATCH 09/62] Added id token jwe decryption --- src/OpenIDConnectClient.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 5ea7fada..bacd1399 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -343,13 +343,25 @@ public function authenticate() { throw new OpenIDConnectClientException('User did not authorize openid scope.'); } - $claims = $this->decodeJWT($token_json->id_token, 1); + $id_token = $token_json->id_token; + $idTokenHeaders = $this->decodeJWT($id_token); + if (isset($idTokenHeaders->enc)) { + // If we don't have a JWE handler then throw error + if ($this->jweResponseHandler === null) { + throw new OpenIDConnectClientException('JWE response handler not set'); + } + + // Handle JWE + $id_token = $this->jweResponseHandler->handleJweResponse($id_token); + } + + $claims = $this->decodeJWT($id_token, 1); // Verify the signature - $this->verifySignatures($token_json->id_token); + $this->verifySignatures($id_token); // Save the id token - $this->idToken = $token_json->id_token; + $this->idToken = $id_token; // Save the access token $this->accessToken = $token_json->access_token; From 5832c8e3a8ba41a0f73cf7db329e3e78c791a8f2 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Thu, 15 Sep 2022 23:00:26 +0200 Subject: [PATCH 10/62] Added support for private_key_jwt authentication method --- src/OpenIDConnectClient.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 2bad8b57..034a8696 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -215,6 +215,11 @@ class OpenIDConnectClient */ private $issuerValidator; + /** + * @var callable|null generator function for private key jwt client authentication + */ + private $privateKeyJwtGenerator; + /** * @var bool Allow OAuth 2 implicit flow; see http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth */ @@ -798,6 +803,12 @@ protected function requestTokens($code, $headers = array()) { unset($token_params['client_id']); } + // When there is a private key jwt generator and it is supported then use it as client authentication + if ($this->privateKeyJwtGenerator !== null && in_array('private_key_jwt', $token_endpoint_auth_methods_supported, true)) { + $token_params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; + $token_params['client_assertion'] = $this->privateKeyJwtGenerator($token_endpoint); + } + $ccm = $this->getCodeChallengeMethod(); $cv = $this->getCodeVerifier(); if (!empty($ccm) && !empty($cv)) { @@ -1453,6 +1464,18 @@ public function setIssuerValidator($issuerValidator) { $this->issuerValidator = $issuerValidator; } + /** + * Use this for private_key_jwt client authentication + * The given function should accept the token_endpoint string as the only argument + * and return a jwt signed with your private key according to: + * https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + * + * @param callable $privateKeyJwtGenerator + */ + public function setPrivateKeyJwtGenerator($privateKeyJwtGenerator) { + $this->privateKeyJwtGenerator = $privateKeyJwtGenerator; + } + /** * @param bool $allowImplicitFlow */ @@ -1922,6 +1945,14 @@ public function getIssuerValidator() { return $this->issuerValidator; } + + /** + * @return callable + */ + public function getPrivateKeyJwtGenerator() { + return $this->privateKeyJwtGenerator; + } + /** * @return int */ From e535cbc49e0eab8e9923136ea24c9cd428a4f115 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Thu, 15 Sep 2022 23:04:47 +0200 Subject: [PATCH 11/62] use __invoke for supporting older php versions --- src/OpenIDConnectClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 034a8696..dbb2c696 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -806,7 +806,7 @@ protected function requestTokens($code, $headers = array()) { // When there is a private key jwt generator and it is supported then use it as client authentication if ($this->privateKeyJwtGenerator !== null && in_array('private_key_jwt', $token_endpoint_auth_methods_supported, true)) { $token_params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; - $token_params['client_assertion'] = $this->privateKeyJwtGenerator($token_endpoint); + $token_params['client_assertion'] = $this->privateKeyJwtGenerator->__invoke($token_endpoint); } $ccm = $this->getCodeChallengeMethod(); From bddb3bf98eac5c1952946fe98df030ec8f3fa275 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Fri, 16 Sep 2022 16:23:22 +0200 Subject: [PATCH 12/62] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d8ccaa..8af3bf6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +* Added support for `private_key_jwt` Client Authentication method #322 ## [0.9.8] From f3196f3e10e5897739a70985db005843984f6142 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Fri, 16 Sep 2022 16:40:12 +0200 Subject: [PATCH 13/62] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d8ccaa..67dde9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +* Support for signed and encrypted UserInfo response. #305 +* Support for signed and encrypted ID Token. #305 ## [0.9.8] From ed0e30a9bb96eb115aa5ded30a016e346c19c37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 26 Sep 2022 08:11:03 +0200 Subject: [PATCH 14/62] fix: harden self signed JWK header --- CHANGELOG.md | 4 ++++ src/OpenIDConnectClient.php | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d8ccaa..e70f1e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +## Fixed + +* Harden self-signed JWK header usage. #323 + ## [0.9.8] ## Fixed diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 2bad8b57..7c358307 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1066,6 +1066,7 @@ public function verifyJWTsignature($jwt) { if (isset($header->jwk)) { $jwk = $header->jwk; + $this->verifyJWKHeader($jwk); } else { $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri'))); if ($jwks === NULL) { @@ -1942,4 +1943,12 @@ public function getCodeChallengeMethod() { public function setCodeChallengeMethod($codeChallengeMethod) { $this->codeChallengeMethod = $codeChallengeMethod; } + + /** + * @throws OpenIDConnectClientException + */ + protected function verifyJWKHeader($jwk) + { + throw new OpenIDConnectClientException('Self signed JWK header is not valid'); + } } From eeb23ddc71fcf5d57523497d845ab287eb2b7cca Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 28 Sep 2022 14:30:32 +1000 Subject: [PATCH 15/62] Merge latest and fix conflicts --- src/OpenIDConnectClient.php | 130 ++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index e4a4beab..30deae6f 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -246,6 +246,16 @@ class OpenIDConnectClient */ private $pkceAlgs = ['S256' => 'sha256', 'plain' => false]; + /** + * @var string if we acquire a sid in back-channel logout it will be stored here + */ + private $backChannelSid; + + /** + * @var string if we acquire a sub in back-channel logout it will be stored here + */ + private $backChannelSubject; + /** * @param $provider_url string optional * @@ -458,6 +468,112 @@ public function signOut($idToken, $redirect) { $this->redirect($signout_endpoint); } + + /** + * Decode and then verify a logout token sent as part of + * back-channel logout flows. + * + * This function should be evaluated as a boolean check + * in your route that receives the POST request for back- + * channel logout executed from the OP. + * + * @return bool + * @throws OpenIDConnectClientException + */ + public function verifyLogoutToken() + { + if (isset($_REQUEST['logout_token'])) { + $logout_token = $_REQUEST['logout_token']; + + $claims = $this->decodeJWT($logout_token, 1); + + // Verify the signature + if ($this->canVerifySignatures()) { + if (!$this->getProviderConfigValue('jwks_uri')) { + throw new OpenIDConnectClientException('Back-channel logout: Unable to verify signature due to no jwks_uri being defined'); + } + if (!$this->verifyJWTsignature($logout_token)) { + throw new OpenIDConnectClientException('Back-channel logout: Unable to verify JWT signature'); + } + } + else { + user_error('Warning: JWT signature verification unavailable'); + } + + // Verify Logout Token Claims + if ($this->verifyLogoutTokenClaims($claims, $logout_token)) { + $this->logoutToken = $logout_token; + $this->verifiedClaims = $claims; + return true; + } + else { + return false; + } + } + else { + throw new OpenIDConnectClientException('Back-channel logout: There was no logout_token in the request'); + } + } + + /** + * Verify each claim in the logout token according to the + * spec for back-channel authentication. + * + * @param object $claims + * @return bool + */ + public function verifyLogoutTokenClaims($claims) + { + // Verify that the Logout Token doesn't contain a nonce Claim. + if (isset($claims->nonce)) { + return false; + } + + // Verify that the logout token contains a sub or sid, or both + if (!isset($claims->sid) && !isset($claims->sub)) { + return false; + } + // Set the sid, which could be used to map to a session in + // the RP, and therefore be used to help destroy the RP's + // session. + if (isset($claims->sid)) { + $this->backChannelSid = $claims->sid; + } + + // Set the sub, which could be used to map to a session in + // the RP, and therefore be used to help destroy the RP's + // session. + if (isset($claims->sub)) { + $this->backChannelSubject = $claims->sub; + } + + // Verify that the Logout Token contains an events Claim whose + // value is a JSON object containing the member name + // http://schemas.openid.net/event/backchannel-logout + if (isset($claims->events)) { + $events = (array) $claims->events; + if (!isset($events['http://schemas.openid.net/event/backchannel-logout']) || + !is_object($events['http://schemas.openid.net/event/backchannel-logout'])) { + return false; + } + } + + // Validate the iss + if (!$this->validateIssuer($claims->iss)) { + return false; + } + // Validate the aud + if ((!$claims->aud === $this->clientID) || (!in_array($this->clientID, $claims->aud, true))) { + return false; + } + // Validate the iat. At this point we can return true if it is ok + if (isset($claims->iat) && ((gettype($claims->iat) === 'integer') && ($claims->iat <= time() + $this->leeway))) { + return true; + } else { + return false; + } + } + /** * @param array $scope - example: openid, given_name, etc... */ @@ -1951,4 +2067,18 @@ protected function verifyJWKHeader($jwk) { throw new OpenIDConnectClientException('Self signed JWK header is not valid'); } + + /* + * @return string + */ + public function getSidFromBackChannel() { + return $this->backChannelSid; + } + + /** + * @return string + */ + public function getSubjectFromBackChannel() { + return $this->backChannelSubject; + } } From c7c6aca5ec9cfe23205ca3b280a069cb8a4c1a1e Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 28 Sep 2022 14:49:41 +1000 Subject: [PATCH 16/62] Add changelog entry and documentation in the README for back-channel logout --- CHANGELOG.md | 1 + README.md | 47 +++++++++++++++++++++++++++++++++++++ src/OpenIDConnectClient.php | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70f1e6c..2a6617df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Fixed * Harden self-signed JWK header usage. #323 +* Added support for back-channel logout. #302 ## [0.9.8] diff --git a/README.md b/README.md index 6689f46d..d4ae347d 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,53 @@ $name = $oidc->requestUserInfo('given_name'); ``` +## Example 9: Back-channel logout ## + +Back-channel authentication assumes you can end a session on the server side on behalf of the user (without relying +on their browser). The request is a POST from the OP direct to your RP. In this way, the use of this library can +ensure your RP performs 'single sign out' for the user even if they didn't have your RP open in a browser or other +device, but still had an active session there. + +Either the sid or the sub may be accessible from the logout token sent from the OP. You can use either +`getSidFromBackChannel()` or `getSubFromBackChannel()` to retrieve them if it is helpful to match them to a session +in order to destroy it. + +The below ensures the use of this library to ensure validation of the back-channel logout token, but is afterward +just a hypothetical way of finding such a session and destroying it. Adjust it to the needs of your RP. + +```php + +function handleLogout() { + // NOTE: assumes that $this->oidc is an instance of OpenIDConnectClient() + if ($this->oidc->verifyLogoutToken()) { + $sid = $this->oidc->getSidFromBackChannel(); + + if (isset($sid)) { + // Somehow find the session based on the $sid and + // destroy it. This depends on your RP's design, + // there is nothing in the OIDC spec to mandate how. + // + // In this example, we find a Redis key, which was + // previously stored using the sid we obtained from + // the access token after login. + // + // The value of the Redis key is that of the user's + // session ID specific to this hypothetical RP app. + // + // We then switch to that session and destroy it. + $this->redis->connect('127.0.0.1', 6379); + $session_id_to_destroy = $this->redis->get($sid); + if ($session_id_to_destroy) { + session_commit(); + session_id($session_id_to_destroy); // switches to that session + session_start(); + $_SESSION = array(); // effectively ends the session + } + } + } +} + +``` ## Development Environments ## In some cases you may need to disable SSL security on your development systems. diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 30deae6f..a180c0c8 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -517,7 +517,7 @@ public function verifyLogoutToken() /** * Verify each claim in the logout token according to the - * spec for back-channel authentication. + * spec for back-channel logout. * * @param object $claims * @return bool From 1161b7771bd64843c6fef423f8f1e49bfaec29c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:52:19 +0200 Subject: [PATCH 17/62] fix: $this->enc_type -> $this->encType --- src/OpenIDConnectClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 5dfcc4a2..7ab04939 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -647,7 +647,7 @@ protected function generateRandString() { throw new OpenIDConnectClientException('Random token generation failed.'); } catch (Exception $e) { throw new OpenIDConnectClientException('Random token generation failed.'); - }; + } } /** @@ -872,7 +872,7 @@ public function requestTokenExchange($subjectToken, $subjectTokenType, $audience } // Convert token params to string format - $post_params = http_build_query($post_data, null, '&', $this->enc_type); + $post_params = http_build_query($post_data, null, '&', $this->encType); return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); } From db9a25c3cf1e751190523b6b2dc0b9c4c0ed747f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 28 Sep 2022 09:03:49 +0200 Subject: [PATCH 18/62] chore: code cleanup of back-channel PR #302 --- src/OpenIDConnectClient.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 2485184c..e96316df 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -378,7 +378,6 @@ public function authenticate() { // Success! return true; - } throw new OpenIDConnectClientException ('Unable to verify JWT claims'); @@ -479,8 +478,8 @@ public function signOut($idToken, $redirect) { * back-channel logout flows. * * This function should be evaluated as a boolean check - * in your route that receives the POST request for back- - * channel logout executed from the OP. + * in your route that receives the POST request for back-channel + * logout executed from the OP. * * @return bool * @throws OpenIDConnectClientException @@ -506,18 +505,15 @@ public function verifyLogoutToken() } // Verify Logout Token Claims - if ($this->verifyLogoutTokenClaims($claims, $logout_token)) { - $this->logoutToken = $logout_token; + if ($this->verifyLogoutTokenClaims($claims)) { $this->verifiedClaims = $claims; return true; } - else { - return false; - } - } - else { - throw new OpenIDConnectClientException('Back-channel logout: There was no logout_token in the request'); + + return false; } + + throw new OpenIDConnectClientException('Back-channel logout: There was no logout_token in the request'); } /** @@ -526,6 +522,7 @@ public function verifyLogoutToken() * * @param object $claims * @return bool + * @throws OpenIDConnectClientException */ public function verifyLogoutTokenClaims($claims) { @@ -572,11 +569,11 @@ public function verifyLogoutTokenClaims($claims) return false; } // Validate the iat. At this point we can return true if it is ok - if (isset($claims->iat) && ((gettype($claims->iat) === 'integer') && ($claims->iat <= time() + $this->leeway))) { + if (isset($claims->iat) && ((is_int($claims->iat)) && ($claims->iat <= time() + $this->leeway))) { return true; - } else { - return false; } + + return false; } /** @@ -770,6 +767,7 @@ protected function generateRandString() { * Start Here * @return void * @throws OpenIDConnectClientException + * @throws \Exception */ private function requestAuthorization() { From f69b40f755f6a38ce4c1be2e347aa3faa7c82d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 28 Sep 2022 09:33:32 +0200 Subject: [PATCH 19/62] Release 0.9.9 --- CHANGELOG.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8af6a2..54aac495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [unreleased] +## [0.9.9] + +### Added + +* Added support for back-channel logout. #302 * Added support for `private_key_jwt` Client Authentication method #322 ## Fixed * Harden self-signed JWK header usage. #323 -* Added support for back-channel logout. #302 ## [0.9.8] @@ -64,13 +67,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * it is now possible to disable upgrading from HTTP to HTTPS for development purposes by calling `setHttpUpgradeInsecureRequests(false)` #241 * bugfix in getSessionKey when _SESSION key does not exist #251 * Added scope parameter to refresh token request #225 -* bugfix in verifyJWTclaims when $accessToken is empty and $claims->at_hash is not #276 +* bugfix in `verifyJWTclaims` when $accessToken is empty and $claims->at_hash is not #276 * bugfix with the `empty` function in PHP 5.4 #267 ## [0.9.2] ### Added -* Support for [PKCE](https://tools.ietf.org/html/rfc7636). Currently the supported methods are 'plain' and 'S256'. +* Support for [PKCE](https://tools.ietf.org/html/rfc7636). Currently, the supported methods are 'plain' and 'S256'. ## [0.9.1] @@ -133,7 +136,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * Add option to send additional registration parameters like post_logout_redirect_uris. #140 ### Changed -* disabled autoload for Crypt_RSA + makre refreshToken() method tolerant for errors #137 +* disabled autoload for Crypt_RSA + make refreshToken() method tolerant for errors #137 ### Removed * @@ -143,7 +146,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added * Added five minutes leeway due to clock skew between openidconnect server and client. * Fix save access_token from request in implicit flow authentication #129 -* verifyJWTsignature() method private -> public #126 +* `verifyJWTsignature()` method private -> public #126 * Support for providers where provider/login URL is not the same as the issuer URL. #125 * Support for providers that has a different login URL from the issuer URL, for instance Azure Active Directory. Here, the provider URL is on the format: https://login.windows.net/(tenant-id), while the issuer claim actually is on the format: https://sts.windows.net/(tenant-id). From 89bdf7c067c4db67964a9b16eacc7c42d37ea4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:04:48 +0200 Subject: [PATCH 20/62] fix: client_secret_jwt and private_key_jwt support is disabled by default --- README.md | 29 +++++++++++++++++--- src/OpenIDConnectClient.php | 44 +++++++++++++++++++++++++------ tests/OpenIDConnectClientTest.php | 26 ++++++++++++++++++ 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d4ae347d..a44b8cb0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ PHP OpenID Connect Basic Client ======================== A simple library that allows an application to authenticate a user through the basic OpenID Connect flow. This library hopes to encourage OpenID Connect use by making it simple enough for a developer with little knowledge of -the OpenID Connect protocol to setup authentication. +the OpenID Connect protocol to set up authentication. A special thanks goes to Justin Richer and Amanda Anganes for their help and support of the protocol. @@ -12,11 +12,12 @@ A special thanks goes to Justin Richer and Amanda Anganes for their help and sup 3. JSON extension ## Install ## - 1. Install library using composer +1. Install library using composer ``` composer require jumbojett/openid-connect-php ``` - 2. Include composer autoloader + +2. Include composer autoloader ```php require __DIR__ . '/vendor/autoload.php'; ``` @@ -191,6 +192,28 @@ function handleLogout() { ``` +## Example 10: Enable Token Endpoint Auth Methods ## + +By default, only `client_secret_basic` is enabled on client side which was the only supported for a long time. +Recently `client_secret_jwt` and `private_key_jwt` have been added, but they remain disabled until explicitly enabled. + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + null); +# enable 'client_secret_basic' and 'client_secret_jwt' +$oidc->setTokenEndpointAuthMethodsSupported(['client_secret_basic', 'client_secret_jwt']); + +# for 'private_key_jwt' in addition also the generator function has to be set. +$oidc->setTokenEndpointAuthMethodsSupported(['private_key_jwt']); +$oidc->setPrivateKeyJwtGenerator(function(string $token_endpoint) { + # TODO: what ever is necessary +}) +``` + + ## Development Environments ## In some cases you may need to disable SSL security on your development systems. Note: This is not recommended on production systems. diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index e0c279be..bac366cf 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -261,6 +261,11 @@ class OpenIDConnectClient */ private $backChannelSubject; + /** + * @var array list of supported auth methods + */ + private $token_endpoint_auth_methods_supported = ['client_secret_basic']; + /** * @param $provider_url string optional * @@ -597,6 +602,14 @@ public function addRegistrationParam($param) { $this->registrationParams = array_merge($this->registrationParams, (array)$param); } + /** + * @param array $token_endpoint_auth_methods_supported + */ + public function setTokenEndpointAuthMethodsSupported($token_endpoint_auth_methods_supported) + { + $this->token_endpoint_auth_methods_supported = $token_endpoint_auth_methods_supported; + } + /** * @param $jwk object - example: (object) ['kid' => ..., 'nbf' => ..., 'use' => 'sig', 'kty' => "RSA", 'e' => "", 'n' => ""] */ @@ -872,7 +885,7 @@ public function requestResourceOwnerToken($bClientAuth = FALSE) { //For client authentication include the client values if($bClientAuth) { $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); - if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) { $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; } else { $post_data['client_id'] = $this->clientID; @@ -911,19 +924,19 @@ protected function requestTokens($code, $headers = array()) { $authorizationHeader = null; # Consider Basic authentication if provider config is set this way - if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) { $authorizationHeader = 'Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret)); unset($token_params['client_secret']); unset($token_params['client_id']); } // When there is a private key jwt generator and it is supported then use it as client authentication - if ($this->privateKeyJwtGenerator !== null && in_array('private_key_jwt', $token_endpoint_auth_methods_supported, true)) { + if ($this->privateKeyJwtGenerator !== null && $this->supportsAuthMethod('private_key_jwt', $token_endpoint_auth_methods_supported)) { $token_params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; $token_params['client_assertion'] = $this->privateKeyJwtGenerator->__invoke($token_endpoint); } - if (in_array('client_secret_jwt', $token_endpoint_auth_methods_supported, true)) { + if ($this->supportsAuthMethod('client_secret_jwt', $token_endpoint_auth_methods_supported)) { $client_assertion_type = $this->getProviderConfigValue('client_assertion_type'); if(isset($this->providerConfig['client_assertion'])){ @@ -994,7 +1007,7 @@ public function requestTokenExchange($subjectToken, $subjectTokenType, $audience } # Consider Basic authentication if provider config is set this way - if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) { $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; unset($post_data['client_secret']); unset($post_data['client_id']); @@ -1031,13 +1044,13 @@ public function refreshToken($refresh_token) { ]; # Consider Basic authentication if provider config is set this way - if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) { $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; unset($token_params['client_secret']); unset($token_params['client_id']); } - if (in_array('client_secret_jwt', $token_endpoint_auth_methods_supported, true)) { + if ($this->supportsAuthMethod('client_secret_jwt', $token_endpoint_auth_methods_supported)) { $client_assertion_type = $this->getProviderConfigValue('client_assertion_type'); $client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('token_endpoint')); @@ -1728,7 +1741,7 @@ public function introspectToken($token, $token_type_hint = '', $clientId = null, $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), 'Accept: application/json']; - if (in_array('client_secret_jwt', $token_endpoint_auth_methods_supported, true)) { + if ($this->supportsAuthMethod('client_secret_jwt', $token_endpoint_auth_methods_supported)) { $client_assertion_type = $this->getProviderConfigValue('client_assertion_type'); $client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('introspection_endpoint')); @@ -2188,4 +2201,19 @@ public function getSidFromBackChannel() { public function getSubjectFromBackChannel() { return $this->backChannelSubject; } + + /** + * @param string $auth_method + * @param array $token_endpoint_auth_methods_supported + * @return bool + */ + public function supportsAuthMethod($auth_method, $token_endpoint_auth_methods_supported) + { + # client_secret_jwt has to explicitly be enabled + if (!in_array($auth_method, $this->token_endpoint_auth_methods_supported, true)) { + return false; + } + + return in_array($auth_method, $token_endpoint_auth_methods_supported, true); + } } diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index 7abf3116..dd3321bb 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -62,4 +62,30 @@ public function testSerialize() $serialized = serialize($client); $this->assertInstanceOf('Jumbojett\OpenIDConnectClient', unserialize($serialized)); } + + /** + * @dataProvider provider + */ + public function testAuthMethodSupport($expected, $authMethod, $clientAuthMethods, $idpAuthMethods) + { + $client = new OpenIDConnectClient(); + if ($clientAuthMethods !== null) { + $client->setTokenEndpointAuthMethodsSupported($clientAuthMethods); + } + $this->assertEquals($expected, $client->supportsAuthMethod($authMethod, $idpAuthMethods)); + } + + public function provider() + { + return [ + 'client_secret_basic - default config' => [true, 'client_secret_basic', null, ['client_secret_basic']], + + 'client_secret_jwt - default config' => [false, 'client_secret_jwt', null, ['client_secret_basic', 'client_secret_jwt']], + 'client_secret_jwt - explicitly enabled' => [true, 'client_secret_jwt', ['client_secret_jwt'], ['client_secret_basic', 'client_secret_jwt']], + + 'private_key_jwt - default config' => [false, 'private_key_jwt', null, ['client_secret_basic', 'client_secret_jwt', 'private_key_jwt']], + 'private_key_jwt - explicitly enabled' => [true, 'private_key_jwt', ['private_key_jwt'], ['client_secret_basic', 'client_secret_jwt', 'private_key_jwt']], + + ]; + } } From 45aac47b525f0483dd4db3324bb1f1cab4666061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:34:46 +0200 Subject: [PATCH 21/62] Release v0.9.10 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54aac495..eacd7d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.9.10] + +## Fixed + +* `private_key_jwt` and `client_secret_jwt` need to explicitly be enabled #331 + + ## [0.9.9] ### Added * Added support for back-channel logout. #302 * Added support for `private_key_jwt` Client Authentication method #322 +* Added support for `client_secret_jwt` Client Authentication method #324 +* Added PS512 encryption support #342 ## Fixed From 4338e8535e64cc2551af7df23cb696b87ec117d4 Mon Sep 17 00:00:00 2001 From: rvogel Date: Fri, 30 Sep 2022 15:49:26 +0200 Subject: [PATCH 22/62] Fix LogoutToken verification for single value `aud` claims ... and add UnitTests for `verifyLogoutTokenClaims`. See https://github.com/jumbojett/OpenID-Connect-PHP/issues/333 --- src/OpenIDConnectClient.php | 4 +- tests/OpenIDConnectClientTest.php | 147 ++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index bac366cf..0d720c86 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -570,7 +570,9 @@ public function verifyLogoutTokenClaims($claims) return false; } // Validate the aud - if ((!$claims->aud === $this->clientID) || (!in_array($this->clientID, $claims->aud, true))) { + $auds = $claims->aud; + $auds = is_array( $auds ) ? $auds : [ $auds ]; + if (!in_array($this->clientID, $auds, true)) { return false; } // Validate the iat. At this point we can return true if it is ok diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index dd3321bb..fd022116 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -88,4 +88,151 @@ public function provider() ]; } + + /** + * @covers Jumbojett\\OpenIDConnectClient::verifyLogoutTokenClaims + * @dataProvider provideTestVerifyLogoutTokenClaimsData + */ + public function testVerifyLogoutTokenClaims( $claims, $expectedResult ) + { + /** @var OpenIDConnectClient | MockObject $client */ + $client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT', 'getProviderConfigValue', 'verifyJWTsignature'])->getMock(); + + $client->setClientID('fake-client-id'); + $client->setIssuer('fake-issuer'); + $client->setIssuerValidator(function() { + return true; + }); + $client->setProviderURL('https://jwt.io/'); + + $actualResult = $client->verifyLogoutTokenClaims( $claims ); + + $this->assertEquals( $expectedResult, $actualResult ); + } + + /** + * @return array + */ + public function provideTestVerifyLogoutTokenClaimsData() { + return [ + 'valid-single-aud' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => 'fake-client-id', + 'sid' => 'fake-client-sid', + 'sub' => 'fake-client-sub', + 'iat' => time(), + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ], + ], + true + ], + 'valid-multiple-auds' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sid' => 'fake-client-sid', + 'sub' => 'fake-client-sub', + 'iat' => time(), + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ], + ], + true + ], + 'invalid-no-sid-and-no-sub' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'iat' => time(), + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ], + ], + false + ], + 'valid-no-sid' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sub' => 'fake-client-sub', + 'iat' => time(), + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ], + ], + true + ], + 'valid-no-sub' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sid' => 'fake-client-sid', + 'iat' => time(), + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ], + ], + true + ], + 'invalid-with-nonce' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sid' => 'fake-client-sid', + 'iat' => time(), + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ], + 'nonce' => 'must-not-be-set' + ], + false + ], + 'invalid-no-events' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sid' => 'fake-client-sid', + 'iat' => time(), + 'nonce' => 'must-not-be-set' + ], + false + ], + 'invalid-no-backchannel-event' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sid' => 'fake-client-sid', + 'iat' => time(), + 'events' => (object) [], + 'nonce' => 'must-not-be-set' + ], + false + ], + 'invalid-no-iat' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sid' => 'fake-client-sid', + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ] + ], + false + ], + 'invalid-bad-iat' => [ + (object)[ + 'iss' => 'fake-issuer', + 'aud' => [ 'fake-client-id', 'some-other-aud' ], + 'sid' => 'fake-client-sid', + 'iat' => time() + 301, + 'events' => (object) [ + 'http://schemas.openid.net/event/backchannel-logout' => (object)[] + ] + ], + false + ], + ]; + } } From 7cb759016ea9217154b2dab901125419d1a3d509 Mon Sep 17 00:00:00 2001 From: rvogel Date: Fri, 30 Sep 2022 15:53:11 +0200 Subject: [PATCH 23/62] Add Changelog entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eacd7d3c..86de431d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.9.11] + +## Fixed + +* `verifyLogoutToken` fails with single-value `aud` claim #333 + ## [0.9.10] ## Fixed From 05964b3ab3b26677d8465ca5da9091181d1a94f8 Mon Sep 17 00:00:00 2001 From: rvogel Date: Fri, 30 Sep 2022 15:57:29 +0200 Subject: [PATCH 24/62] Remove unnecessary method overrides in UnitTest --- tests/OpenIDConnectClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index fd022116..d82234ed 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -96,7 +96,7 @@ public function provider() public function testVerifyLogoutTokenClaims( $claims, $expectedResult ) { /** @var OpenIDConnectClient | MockObject $client */ - $client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT', 'getProviderConfigValue', 'verifyJWTsignature'])->getMock(); + $client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT'])->getMock(); $client->setClientID('fake-client-id'); $client->setIssuer('fake-issuer'); From e3c3f9a647647d97feabc6098f1fe6e1e7687e0a Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Thu, 6 Oct 2022 16:40:42 +0200 Subject: [PATCH 25/62] Removed interface and added function that can be extended to add the jwe functionality --- src/OpenIDConnectClient.php | 38 ++++++------------- src/interfaces/HandleJweResponseInterface.php | 13 ------- 2 files changed, 12 insertions(+), 39 deletions(-) delete mode 100644 src/interfaces/HandleJweResponseInterface.php diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index c325ed77..28670e18 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -22,8 +22,6 @@ namespace Jumbojett; -use Jumbojett\Interfaces\HandleJweResponseInterface; - /** * * JWT signature verification support by Jonathan Reed @@ -273,11 +271,6 @@ class OpenIDConnectClient */ private $token_endpoint_auth_methods_supported = ['client_secret_basic']; - /** - * @var HandleJweResponseInterface|null - */ - private $jweResponseHandler; - /** * @param $provider_url string optional * @@ -318,13 +311,6 @@ public function setResponseTypes($response_types) { $this->responseTypes = array_merge($this->responseTypes, (array)$response_types); } - /** - * @param HandleJweResponseInterface $jwe_response_handler - */ - public function setJweResponseHandler($jwe_response_handler) { - $this->jweResponseHandler = $jwe_response_handler; - } - /** * @return bool * @throws OpenIDConnectClientException @@ -366,13 +352,8 @@ public function authenticate() { $id_token = $token_json->id_token; $idTokenHeaders = $this->decodeJWT($id_token); if (isset($idTokenHeaders->enc)) { - // If we don't have a JWE handler then throw error - if ($this->jweResponseHandler === null) { - throw new OpenIDConnectClientException('JWE response handler not set'); - } - // Handle JWE - $id_token = $this->jweResponseHandler->handleJweResponse($id_token); + $id_token = $this->handleJweResponse($id_token); } $claims = $this->decodeJWT($id_token, 1); @@ -1401,13 +1382,8 @@ public function requestUserInfo($attribute = null) { // Check if the response is encrypted $jwtHeaders = $this->decodeJWT($response); if (isset($jwtHeaders->enc)) { - // If we don't have a JWE handler then throw error - if ($this->jweResponseHandler === null) { - throw new OpenIDConnectClientException('JWE response handler not set'); - } - // Handle JWE - $jwt = $this->jweResponseHandler->handleJweResponse($response); + $jwt = $this->handleJweResponse($response); } else { // If the response is not encrypted then it must be signed $jwt = $response; @@ -2265,6 +2241,16 @@ protected function verifyJWKHeader($jwk) throw new OpenIDConnectClientException('Self signed JWK header is not valid'); } + /** + * @param string $jwe The JWE to decrypt + * @return string the JWT payload + * @throws OpenIDConnectClientException + */ + protected function handleJweResponse($jwe) + { + throw new OpenIDConnectClientException('JWE response is not supported, please extend the class and implement this method'); + } + /* * @return string */ diff --git a/src/interfaces/HandleJweResponseInterface.php b/src/interfaces/HandleJweResponseInterface.php deleted file mode 100644 index 3eef5b2c..00000000 --- a/src/interfaces/HandleJweResponseInterface.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Tue, 22 Nov 2022 22:38:35 +0100 Subject: [PATCH 26/62] docs: fix getSubjectFromBackChannel in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a44b8cb0..79318e50 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ ensure your RP performs 'single sign out' for the user even if they didn't have device, but still had an active session there. Either the sid or the sub may be accessible from the logout token sent from the OP. You can use either -`getSidFromBackChannel()` or `getSubFromBackChannel()` to retrieve them if it is helpful to match them to a session +`getSidFromBackChannel()` or `getSubjectFromBackChannel()` to retrieve them if it is helpful to match them to a session in order to destroy it. The below ensures the use of this library to ensure validation of the back-channel logout token, but is afterward From 7a7dbec1b9cc2fd957de02641e2d171049608cad Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 12 Dec 2022 17:33:42 +0530 Subject: [PATCH 27/62] Fix return type --- src/OpenIDConnectClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index bac366cf..24e15399 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1882,7 +1882,7 @@ public function getIdTokenPayload() { } /** - * @return string + * @return object */ public function getTokenResponse() { return $this->tokenResponse; From a4776d1749b6bf319ae2da02f914b4675f67a9fd Mon Sep 17 00:00:00 2001 From: Anthimidis Nikos Date: Tue, 3 Jan 2023 16:40:14 +0200 Subject: [PATCH 28/62] Add an extra check on $_REQUEST['state'] --- src/OpenIDConnectClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 28670e18..5046ceb1 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -338,7 +338,7 @@ public function authenticate() { } // Do an OpenID Connect session check - if ($_REQUEST['state'] !== $this->getState()) { + if (isset($_REQUEST['state']) && ($_REQUEST['state'] !== $this->getState())) { throw new OpenIDConnectClientException('Unable to determine state'); } @@ -401,7 +401,7 @@ public function authenticate() { } // Do an OpenID Connect session check - if ($_REQUEST['state'] !== $this->getState()) { + if (isset($_REQUEST['state']) && ($_REQUEST['state'] !== $this->getState())) { throw new OpenIDConnectClientException('Unable to determine state'); } From ed5ccd933f646797bd4d50073ecdeafbb7553159 Mon Sep 17 00:00:00 2001 From: Anthimidis Nikos Date: Tue, 10 Jan 2023 10:32:03 +0200 Subject: [PATCH 29/62] Fix if statement to throw error instead of bypass Fix code to throw error instead of by passing the if statement, after @LauJosefen notice. --- src/OpenIDConnectClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 5046ceb1..a7bacf94 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -338,7 +338,7 @@ public function authenticate() { } // Do an OpenID Connect session check - if (isset($_REQUEST['state']) && ($_REQUEST['state'] !== $this->getState())) { + if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { throw new OpenIDConnectClientException('Unable to determine state'); } @@ -401,7 +401,7 @@ public function authenticate() { } // Do an OpenID Connect session check - if (isset($_REQUEST['state']) && ($_REQUEST['state'] !== $this->getState())) { + if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { throw new OpenIDConnectClientException('Unable to determine state'); } From 8a80c1a0b8071216f26f058803ea7c9d6bdeac5c Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 12 Jan 2023 23:55:03 +0530 Subject: [PATCH 30/62] Correct variable docstring --- src/OpenIDConnectClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 24e15399..1df58878 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -145,7 +145,7 @@ class OpenIDConnectClient protected $idToken; /** - * @var string stores the token response + * @var object stores the token response */ private $tokenResponse; From 13e86af95132aaf94278e96f628928cd21c57f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20B=C5=99ou=C5=A1ek?= Date: Fri, 13 Jan 2023 07:44:36 +0100 Subject: [PATCH 31/62] docs: fix changelog format correct Keep a changelog style --- CHANGELOG.md | 226 ++++++++++++++++++++++++--------------------------- 1 file changed, 104 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2908a89c..12952f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,189 +1,171 @@ # Changelog All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/) -and this project adheres to [Semantic Versioning](http://semver.org/). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] -* Support for signed and encrypted UserInfo response. #305 -* Support for signed and encrypted ID Token. #305 +## [Unreleased] -## [0.9.10] - -## Fixed +### Added +- Support for signed and encrypted UserInfo response. #305 +- Support for signed and encrypted ID Token. #305 -* `private_key_jwt` and `client_secret_jwt` need to explicitly be enabled #331 +## [0.9.10] - 2022-09-30 +### Fixed +- `private_key_jwt` and `client_secret_jwt` need to explicitly be enabled #331 -## [0.9.9] +## [0.9.9] - 2022-09-28 ### Added +- Added support for back-channel logout. #302 +- Added support for `private_key_jwt` Client Authentication method #322 +- Added support for `client_secret_jwt` Client Authentication method #324 +- Added PS512 encryption support #342 -* Added support for back-channel logout. #302 -* Added support for `private_key_jwt` Client Authentication method #322 -* Added support for `client_secret_jwt` Client Authentication method #324 -* Added PS512 encryption support #342 - -## Fixed - -* Harden self-signed JWK header usage. #323 - -## [0.9.8] +### Fixed +- Harden self-signed JWK header usage. #323 -## Fixed +## [0.9.8] - 2022-08-05 -* Do not use PKCE if IdP does not support it. #317 +### Fixed +- Do not use PKCE if IdP does not support it. #317 -## [0.9.7] +## [0.9.7] - 2022-07-13 ### Added - -* Support for Self-Contained JWTs. #308 -* Support for RFC8693 Token Exchange Request. #275 +- Support for Self-Contained JWTs. #308 +- Support for RFC8693 Token Exchange Request. #275 ### Fixed +- PHP 5.4 compatibility. #304 +- Use session_status(). #306 -* PHP 5.4 compatibility. #304 -* Use session_status(). #306 - -## [0.9.6] +## [0.9.6] - 2022-05-08 ### Added - -* Support for [phpseclib/phpseclib](https://phpseclib.com/) version **3**. #260 -* Support client_secret on token endpoint with PKCE. #293 -* Added new parameter to `requestTokens()` to pass custom HTTP headers #297 +- Support for [phpseclib/phpseclib](https://phpseclib.com/) version **3**. #260 +- Support client_secret on token endpoint with PKCE. #293 +- Added new parameter to `requestTokens()` to pass custom HTTP headers #297 ### Changed +- Allow serializing `OpenIDConnectClient` using `serialize()` #295 -* Allow serializing `OpenIDConnectClient` using `serialize()` #295 - -## [0.9.5] +## [0.9.5] - 2021-11-24 ### Changed +- signOut() Method parameter $accessToken -> $idToken to prevent confusion about access and id tokens usage. #127 +- Fixed issue where missing nonce within the claims was causing an exception. #280 -* signOut() Method parameter $accessToken -> $idToken to prevent confusion about access and id tokens usage. #127 -* Fixed issue where missing nonce within the claims was causing an exception. #280 - -## [0.9.4] +## [0.9.4] - 2021-11-21 ### Added +- Enabled `client_secret_basic` authentication on `refreshToken()` #215 +- Basic auth support for requestResourceOwnerToken #271 -* Enabled `client_secret_basic` authentication on `refreshToken()` #215 -* Basic auth support for requestResourceOwnerToken #271 - -## [0.9.3] +## [0.9.3] - 2021-11-20 ### Added +- getRedirectURL() will not log a warning for PHP 7.1+ #179 +- it is now possible to disable upgrading from HTTP to HTTPS for development purposes by calling `setHttpUpgradeInsecureRequests(false)` #241 +- bugfix in getSessionKey when _SESSION key does not exist #251 +- Added scope parameter to refresh token request #225 +- bugfix in `verifyJWTclaims` when $accessToken is empty and $claims->at_hash is not #276 +- bugfix with the `empty` function in PHP 5.4 #267 -* getRedirectURL() will not log a warning for PHP 7.1+ #179 -* it is now possible to disable upgrading from HTTP to HTTPS for development purposes by calling `setHttpUpgradeInsecureRequests(false)` #241 -* bugfix in getSessionKey when _SESSION key does not exist #251 -* Added scope parameter to refresh token request #225 -* bugfix in `verifyJWTclaims` when $accessToken is empty and $claims->at_hash is not #276 -* bugfix with the `empty` function in PHP 5.4 #267 - -## [0.9.2] +## [0.9.2] - 2020-11-16 ### Added -* Support for [PKCE](https://tools.ietf.org/html/rfc7636). Currently, the supported methods are 'plain' and 'S256'. +- Support for [PKCE](https://tools.ietf.org/html/rfc7636). Currently, the supported methods are 'plain' and 'S256'. -## [0.9.1] +## [0.9.1] - 2020-08-27 ### Added -* Add support for MS Azure Active Directory B2C user flows +- Add support for MS Azure Active Directory B2C user flows ### Changed -* Fix at_hash verification #200 -* Getters for public parameters #204 -* Removed client ID query parameter when making a token request using Basic Auth -* Use of `random_bytes()` for token generation instead of `uniqid()`; polyfill for PHP < 7.0 provided. +- Fix at_hash verification #200 +- Getters for public parameters #204 +- Removed client ID query parameter when making a token request using Basic Auth +- Use of `random_bytes()` for token generation instead of `uniqid()`; polyfill for PHP < 7.0 provided. ### Removed -* Removed explicit content-length header - caused issues with proxy servers - +- Removed explicit content-length header - caused issues with proxy servers -## [0.9.0] +## [0.9.0] - 2020-03-09 ### Added -* php 7.4 deprecates array_key_exists on objects, use property_exists in getVerifiedClaims and requestUserInfo -* Adding a header to indicate JSON as the return type for userinfo endpoint #151 -* ~Updated OpenIDConnectClient to conditionally verify nonce #146~ -* Add possibility to change enc_type parameter for http_build_query #155 -* Adding OAuth 2.0 Token Introspection #156 -* Add optional parameters clientId/clientSecret for introspection #157 & #158 -* Adding OAuth 2.0 Token Revocation #160 -* Adding issuer validator #145 -* Adding signing algorithm PS256 #180 -* Check http status of request user info #186 -* URL encode clientId and clientSecret when using basic authentication, according to https://tools.ietf.org/html/rfc6749#section-2.3.1 #192 -* Adjust PHPDoc to state that null is also allowed #193 +- php 7.4 deprecates array_key_exists on objects, use property_exists in getVerifiedClaims and requestUserInfo +- Adding a header to indicate JSON as the return type for userinfo endpoint #151 +- ~Updated OpenIDConnectClient to conditionally verify nonce #146~ +- Add possibility to change enc_type parameter for http_build_query #155 +- Adding OAuth 2.0 Token Introspection #156 +- Add optional parameters clientId/clientSecret for introspection #157 & #158 +- Adding OAuth 2.0 Token Revocation #160 +- Adding issuer validator #145 +- Adding signing algorithm PS256 #180 +- Check http status of request user info #186 +- URL encode clientId and clientSecret when using basic authentication, according to https://tools.ietf.org/html/rfc6749#section-2.3.1 #192 +- Adjust PHPDoc to state that null is also allowed #193 ### Changed -* Bugfix/code cleanup #152 - * Cleanup PHPDoc #46e5b59 - * Replace unnecessary double quotes with single quotes #2a76b57 - * Use original function names instead of aliases #1f37892 - * Remove unnecessary default values #5ab801e - * Explicit declare field $redirectURL #9187c0b - * Remove unused code #1e65384 - * Fix indent #e9cdf56 - * Cleanup conditional code flow for better readability #107f3fb - * Added strict type comparisons #167 -* Bugfix: required `openid` scope was omitted when additional scopes were registered using `addScope` method. This resulted in failing OpenID process. - -## [0.8.0] +- Bugfix/code cleanup #152 +- Cleanup PHPDoc #46e5b59 +- Replace unnecessary double quotes with single quotes #2a76b57 +- Use original function names instead of aliases #1f37892 +- Remove unnecessary default values #5ab801e +- Explicit declare field $redirectURL #9187c0b +- Remove unused code #1e65384 +- Fix indent #e9cdf56 +- Cleanup conditional code flow for better readability #107f3fb +- Added strict type comparisons #167 +- Bugfix: required `openid` scope was omitted when additional scopes were registered using `addScope` method. This resulted in failing OpenID process. + +## [0.8.0] - 2019-01-02 ### Added -* Fix `verifyJWTsignature()`: verify JWT to prevent php errors and warnings on invalid token +- Fix `verifyJWTsignature()`: verify JWT to prevent php errors and warnings on invalid token ### Changed -* Decouple session manipulation, it's allow use of other session libraries #134 -* Broaden version requirements of the phpseclib/phpseclib package. #144 +- Decouple session manipulation, it's allow use of other session libraries #134 +- Broaden version requirements of the phpseclib/phpseclib package. #144 -## [0.7.0] +## [0.7.0] - 2018-10-15 ### Added -* Add "license" field to composer.json #138 -* Ensure key_alg is set when getting key #139 -* Add option to send additional registration parameters like post_logout_redirect_uris. #140 +- Add "license" field to composer.json #138 +- Ensure key_alg is set when getting key #139 +- Add option to send additional registration parameters like post_logout_redirect_uris. #140 ### Changed -* disabled autoload for Crypt_RSA + make refreshToken() method tolerant for errors #137 +- disabled autoload for Crypt_RSA + make refreshToken() method tolerant for errors #137 -### Removed -* - -## [0.6.0] +## [0.6.0] - 2018-07-17 ### Added -* Added five minutes leeway due to clock skew between openidconnect server and client. -* Fix save access_token from request in implicit flow authentication #129 -* `verifyJWTsignature()` method private -> public #126 -* Support for providers where provider/login URL is not the same as the issuer URL. #125 -* Support for providers that has a different login URL from the issuer URL, for instance Azure Active Directory. Here, the provider URL is on the format: https://login.windows.net/(tenant-id), while the issuer claim actually is on the format: https://sts.windows.net/(tenant-id). +- Added five minutes leeway due to clock skew between openidconnect server and client. +- Fix save access_token from request in implicit flow authentication #129 +- `verifyJWTsignature()` method private -> public #126 +- Support for providers where provider/login URL is not the same as the issuer URL. #125 +- Support for providers that has a different login URL from the issuer URL, for instance Azure Active Directory. Here, the provider URL is on the format: https://login.windows.net/(tenant-id), while the issuer claim actually is on the format: https://sts.windows.net/(tenant-id). ### Changed -* refreshToken method update #124 - -### Removed -* - -## [0.5.0] -## Added -* Implement Azure AD B2C Implicit Workflow +- refreshToken method update #124 -## [0.4.1] -## Changed -* Documentation updates for include path. +## [0.5.0] - 2018-04-09 -## [0.4] ### Added -* Timeout is configurable via setTimeout method. This addresses issue #94. -* Add the ability to authenticate using the Resource Owner flow (with or without the Client ID and ClientSecret). This addresses issue #98 -* Add support for HS256, HS512 and HS384 signatures -* Removed unused calls to $this->getProviderConfigValue("token_endpoint_… +- Implement Azure AD B2C Implicit Workflow + +## [0.4.1] - 2018-02-16 ### Changed +- Documentation updates for include path. -### Removed +## [0.4.0] - 2018-02-15 + +### Added +- Timeout is configurable via setTimeout method. This addresses issue #94. +- Add the ability to authenticate using the Resource Owner flow (with or without the Client ID and ClientSecret). This addresses issue #98 +- Add support for HS256, HS512 and HS384 signatures +- Removed unused calls to $this->getProviderConfigValue("token_endpoint_… From b6cc8138cf529b8c062b1a8f5083bd8438f0848a Mon Sep 17 00:00:00 2001 From: Anthimidis Nikos Date: Thu, 19 Jan 2023 16:50:55 +0200 Subject: [PATCH 32/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2908a89c..c866c179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +* Fixed issue on authentication for php8. #354 * Support for signed and encrypted UserInfo response. #305 * Support for signed and encrypted ID Token. #305 From e94b9eb331504c9c6c130b9186f2984480f8de78 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Wed, 29 Mar 2023 10:25:53 +0200 Subject: [PATCH 33/62] chore: Update construct typehint in docblock (#364) * Update construct typehint in docblock * Update changelog --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a60e9d..924e5889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fixed issue on authentication for php8. #354 * Support for signed and encrypted UserInfo response. #305 * Support for signed and encrypted ID Token. #305 +* Update construct typehint in docblock. #364 ### Added - Support for signed and encrypted UserInfo response. #305 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 34af3753..71667a33 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -272,11 +272,10 @@ class OpenIDConnectClient private $token_endpoint_auth_methods_supported = ['client_secret_basic']; /** - * @param $provider_url string optional - * - * @param $client_id string optional - * @param $client_secret string optional - * @param null $issuer + * @param string|null $provider_url optional + * @param string|null $client_id optional + * @param string|null $client_secret optional + * @param string|null $issuer */ public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null) { $this->setProviderURL($provider_url); From 20b51cb3c7334b5575777cc69cfeb704a10b1721 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Wed, 29 Mar 2023 10:26:32 +0200 Subject: [PATCH 34/62] chore: Update visibility of getWellKnownConfigValue to protected (#363) * Update visibility of getWellKnownConfigValue to protected * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 924e5889..af8da92b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] +* Update visibility of getWellKnownConfigValue to protected. #363 * Fixed issue on authentication for php8. #354 * Support for signed and encrypted UserInfo response. #305 * Support for signed and encrypted ID Token. #305 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 71667a33..b9f969f4 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -639,7 +639,7 @@ protected function getProviderConfigValue($param, $default = null) { * @return string * */ - private function getWellKnownConfigValue($param, $default = null) { + protected function getWellKnownConfigValue($param, $default = null) { // If the configuration value is not available, attempt to fetch it from a well known config endpoint // This is also known as auto "discovery" From e6eab93a5f15f0edf44d30f6b997407fedfec00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 3 May 2023 15:42:43 +0200 Subject: [PATCH 35/62] feat: php7.0 minimum requirement (#327) --- .github/workflows/build.yml | 2 +- composer.json | 7 +- src/OpenIDConnectClient.php | 662 ++++++++++-------------------- tests/OpenIDConnectClientTest.php | 16 +- tests/TokenVerificationTest.php | 7 +- 5 files changed, 237 insertions(+), 457 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2033968d..515cd64c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] + php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] steps: - name: Checkout diff --git a/composer.json b/composer.json index 6d218ccf..3fa6d231 100644 --- a/composer.json +++ b/composer.json @@ -3,14 +3,13 @@ "description": "Bare-bones OpenID Connect client", "license": "Apache-2.0", "require": { - "php": ">=5.4", - "phpseclib/phpseclib" : "~2.0 || ^3.0", + "php": ">=7.0", "ext-json": "*", "ext-curl": "*", - "paragonie/random_compat": ">=2" + "phpseclib/phpseclib": "~3.0" }, "require-dev": { - "roave/security-advisories": "dev-master", + "roave/security-advisories": "dev-latest", "yoast/phpunit-polyfills": "^1.0" }, "archive" : { diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index b83d9d3a..ba4fddef 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -22,18 +22,16 @@ namespace Jumbojett; -/** - * - * JWT signature verification support by Jonathan Reed - * Licensed under the same license as the rest of this file. - * - * phpseclib is required to validate the signatures of some tokens. - * It can be downloaded from: http://phpseclib.sourceforge.net/ - */ -if (!class_exists('\phpseclib3\Crypt\RSA') && !class_exists('\phpseclib\Crypt\RSA') && !class_exists('Crypt_RSA')) { - user_error('Unable to find phpseclib Crypt/RSA.php. Ensure phpseclib is installed and in include_path before you include this file'); -} +use Error; +use Exception; +use phpseclib3\Crypt\PublicKeyLoader; +use phpseclib3\Crypt\RSA; +use phpseclib3\Math\BigInteger; +use stdClass; +use function bin2hex; +use function is_object; +use function random_bytes; /** * A wrapper around base64_decode which decodes Base64URL-encoded data, @@ -41,7 +39,7 @@ * @param string $base64url * @return bool|string */ -function base64url_decode($base64url) { +function base64url_decode(string $base64url) { return base64_decode(b64url2b64($base64url)); } @@ -53,7 +51,8 @@ function base64url_decode($base64url) { * @param string $base64url * @return string */ -function b64url2b64($base64url) { +function b64url2b64(string $base64url): string +{ // "Shouldn't" be necessary, but why not $padding = strlen($base64url) % 4; if ($padding > 0) { @@ -66,19 +65,8 @@ function b64url2b64($base64url) { /** * OpenIDConnect Exception Class */ -class OpenIDConnectClientException extends \Exception +class OpenIDConnectClientException extends Exception { - -} - -/** - * Require the CURL and JSON PHP extensions to be installed - */ -if (!function_exists('curl_init')) { - throw new OpenIDConnectClientException('OpenIDConnect needs the CURL PHP extension.'); -} -if (!function_exists('json_decode')) { - throw new OpenIDConnectClientException('OpenIDConnect needs the JSON PHP extension.'); } /** @@ -169,11 +157,6 @@ class OpenIDConnectClient */ private $responseTypes = []; - /** - * @var array holds a cache of info returned from the user info endpoint - */ - private $userInfo = []; - /** * @var array holds authentication parameters */ @@ -190,7 +173,7 @@ class OpenIDConnectClient private $wellKnown = false; /** - * @var mixed holds well-known opendid configuration parameters, like policy for MS Azure AD B2C User Flow + * @var mixed holds well-known openid configuration parameters, like policy for MS Azure AD B2C User Flow * @see https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview */ private $wellKnownConfigParameters = []; @@ -211,7 +194,7 @@ class OpenIDConnectClient private $additionalJwks = []; /** - * @var array holds verified jwt claims + * @var object holds verified jwt claims */ protected $verifiedClaims = []; @@ -277,7 +260,7 @@ class OpenIDConnectClient * @param string|null $client_secret optional * @param string|null $issuer */ - public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null) { + public function __construct(string $provider_url = null, string $client_id = null, string $client_secret = null, string $issuer = null) { $this->setProviderURL($provider_url); if ($issuer === null) { $this->setIssuer($provider_url); @@ -314,8 +297,8 @@ public function setResponseTypes($response_types) { * @return bool * @throws OpenIDConnectClientException */ - public function authenticate() { - + public function authenticate(): bool + { // Do a preemptive check to see if the provider has thrown an error from a previous redirect if (isset($_REQUEST['error'])) { $desc = isset($_REQUEST['error_description']) ? ' Description: ' . $_REQUEST['error_description'] : ''; @@ -367,7 +350,7 @@ public function authenticate() { $this->accessToken = $token_json->access_token; // If this is a valid claim - if ($this->verifyJWTclaims($claims, $token_json->access_token)) { + if ($this->verifyJWTClaims($claims, $token_json->access_token)) { // Clean up the session a little $this->unsetNonce(); @@ -394,10 +377,7 @@ public function authenticate() { // if we have no code but an id_token use that $id_token = $_REQUEST['id_token']; - $accessToken = null; - if (isset($_REQUEST['access_token'])) { - $accessToken = $_REQUEST['access_token']; - } + $accessToken = $_REQUEST['access_token'] ?? null; // Do an OpenID Connect session check if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { @@ -416,7 +396,7 @@ public function authenticate() { $this->idToken = $id_token; // If this is a valid claim - if ($this->verifyJWTclaims($claims, $accessToken)) { + if ($this->verifyJWTClaims($claims, $accessToken)) { // Clean up the session a little $this->unsetNonce(); @@ -453,10 +433,9 @@ public function authenticate() { * * @throws OpenIDConnectClientException */ - public function signOut($idToken, $redirect) { - $signout_endpoint = $this->getProviderConfigValue('end_session_endpoint'); + public function signOut(string $idToken, $redirect) { + $sign_out_endpoint = $this->getProviderConfigValue('end_session_endpoint'); - $signout_params = null; if($redirect === null){ $signout_params = ['id_token_hint' => $idToken]; } @@ -466,8 +445,8 @@ public function signOut($idToken, $redirect) { 'post_logout_redirect_uri' => $redirect]; } - $signout_endpoint .= (strpos($signout_endpoint, '?') === false ? '?' : '&') . http_build_query( $signout_params, '', '&', $this->encType); - $this->redirect($signout_endpoint); + $sign_out_endpoint .= (strpos($sign_out_endpoint, '?') === false ? '?' : '&') . http_build_query( $signout_params, '', '&', $this->encType); + $this->redirect($sign_out_endpoint); } @@ -482,7 +461,7 @@ public function signOut($idToken, $redirect) { * @return bool * @throws OpenIDConnectClientException */ - public function verifyLogoutToken() + public function verifyLogoutToken(): bool { if (isset($_REQUEST['logout_token'])) { $logout_token = $_REQUEST['logout_token']; @@ -490,16 +469,11 @@ public function verifyLogoutToken() $claims = $this->decodeJWT($logout_token, 1); // Verify the signature - if ($this->canVerifySignatures()) { - if (!$this->getProviderConfigValue('jwks_uri')) { - throw new OpenIDConnectClientException('Back-channel logout: Unable to verify signature due to no jwks_uri being defined'); - } - if (!$this->verifyJWTsignature($logout_token)) { - throw new OpenIDConnectClientException('Back-channel logout: Unable to verify JWT signature'); - } + if (!$this->getProviderConfigValue('jwks_uri')) { + throw new OpenIDConnectClientException('Back-channel logout: Unable to verify signature due to no jwks_uri being defined'); } - else { - user_error('Warning: JWT signature verification unavailable'); + if (!$this->verifyJWTSignature($logout_token)) { + throw new OpenIDConnectClientException('Back-channel logout: Unable to verify JWT signature'); } // Verify Logout Token Claims @@ -522,7 +496,7 @@ public function verifyLogoutToken() * @return bool * @throws OpenIDConnectClientException */ - public function verifyLogoutTokenClaims($claims) + public function verifyLogoutTokenClaims($claims): bool { // Verify that the Logout Token doesn't contain a nonce Claim. if (isset($claims->nonce)) { @@ -579,28 +553,25 @@ public function verifyLogoutTokenClaims($claims) /** * @param array $scope - example: openid, given_name, etc... */ - public function addScope($scope) { - $this->scopes = array_merge($this->scopes, (array)$scope); + public function addScope(array $scope) { + $this->scopes = array_merge($this->scopes, $scope); } /** * @param array $param - example: prompt=login */ - public function addAuthParam($param) { - $this->authParams = array_merge($this->authParams, (array)$param); + public function addAuthParam(array $param) { + $this->authParams = array_merge($this->authParams, $param); } /** * @param array $param - example: post_logout_redirect_uris=[http://example.com/successful-logout] */ - public function addRegistrationParam($param) { - $this->registrationParams = array_merge($this->registrationParams, (array)$param); + public function addRegistrationParam(array $param) { + $this->registrationParams = array_merge($this->registrationParams, $param); } - /** - * @param array $token_endpoint_auth_methods_supported - */ - public function setTokenEndpointAuthMethodsSupported($token_endpoint_auth_methods_supported) + public function setTokenEndpointAuthMethodsSupported(array $token_endpoint_auth_methods_supported) { $this->token_endpoint_auth_methods_supported = $token_endpoint_auth_methods_supported; } @@ -613,15 +584,15 @@ protected function addAdditionalJwk($jwk) { } /** - * Get's anything that we need configuration wise including endpoints, and other values + * Gets anything that we need configuration wise including endpoints, and other values * * @param string $param - * @param string $default optional - * @throws OpenIDConnectClientException + * @param string|array|null $default optional * @return string|array * + *@throws OpenIDConnectClientException */ - protected function getProviderConfigValue($param, $default = null) { + protected function getProviderConfigValue(string $param, $default = null) { // If the configuration value is not available, attempt to fetch it from a well known config endpoint // This is also known as auto "discovery" @@ -633,15 +604,16 @@ protected function getProviderConfigValue($param, $default = null) { } /** - * Get's anything that we need configuration wise including endpoints, and other values + * Gets anything that we need configuration wise including endpoints, and other values * * @param string $param - * @param string $default optional - * @throws OpenIDConnectClientException + * @param string|null $default optional * @return string * + *@throws OpenIDConnectClientException */ - protected function getWellKnownConfigValue($param, $default = null) { + protected function getWellKnownConfigValue(string $param, string $default = null): string + { // If the configuration value is not available, attempt to fetch it from a well known config endpoint // This is also known as auto "discovery" @@ -650,13 +622,10 @@ protected function getWellKnownConfigValue($param, $default = null) { if (count($this->wellKnownConfigParameters) > 0){ $well_known_config_url .= '?' . http_build_query($this->wellKnownConfigParameters) ; } - $this->wellKnown = json_decode($this->fetchURL($well_known_config_url)); + $this->wellKnown = json_decode($this->fetchURL($well_known_config_url), false); } - $value = false; - if(isset($this->wellKnown->{$param})){ - $value = $this->wellKnown->{$param}; - } + $value = $this->wellKnown->{$param} ?? false; if ($value) { return $value; @@ -667,14 +636,11 @@ protected function getWellKnownConfigValue($param, $default = null) { return $default; } - throw new OpenIDConnectClientException("The provider {$param} could not be fetched. Make sure your provider has a well known configuration available."); + throw new OpenIDConnectClientException("The provider $param could not be fetched. Make sure your provider has a well known configuration available."); } /** - * Set optionnal parameters for .well-known/openid-configuration - * - * @param string $param - * + * Set optional parameters for .well-known/openid-configuration */ public function setWellKnownConfigParameters(array $params = []){ $this->wellKnownConfigParameters=$params; @@ -684,7 +650,7 @@ public function setWellKnownConfigParameters(array $params = []){ /** * @param string $url Sets redirect URL for auth flow */ - public function setRedirectURL($url) { + public function setRedirectURL(string $url) { if (parse_url($url,PHP_URL_HOST) !== false) { $this->redirectURL = $url; } @@ -695,8 +661,8 @@ public function setRedirectURL($url) { * * @return string */ - public function getRedirectURL() { - + public function getRedirectURL(): string + { // If the redirect URL has been set then return it. if (property_exists($this, 'redirectURL') && $this->redirectURL) { return $this->redirectURL; @@ -728,9 +694,9 @@ public function getRedirectURL() { } if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) { - $port = intval($_SERVER['HTTP_X_FORWARDED_PORT']); + $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT']; } elseif (isset($_SERVER['SERVER_PORT'])) { - $port = intval($_SERVER['SERVER_PORT']); + $port = $_SERVER['SERVER_PORT']; } elseif ($protocol === 'https') { $port = 443; } else { @@ -759,11 +725,10 @@ public function getRedirectURL() { * @return string * @throws OpenIDConnectClientException */ - protected function generateRandString() { - // Error and Exception need to be catched in this order, see https://github.com/paragonie/random_compat/blob/master/README.md - // random_compat polyfill library should be removed if support for PHP versions < 7 is dropped + protected function generateRandString(): string + { try { - return \bin2hex(\random_bytes(16)); + return bin2hex(random_bytes(16)); } catch (Error $e) { throw new OpenIDConnectClientException('Random token generation failed.'); } catch (Exception $e) { @@ -775,7 +740,7 @@ protected function generateRandString() { * Start Here * @return void * @throws OpenIDConnectClientException - * @throws \Exception + * @throws Exception */ private function requestAuthorization() { @@ -852,7 +817,7 @@ public function requestClientCredentialsToken() { // Convert token params to string format $post_params = http_build_query($post_data, '', '&', $this->encType); - return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); + return json_decode($this->fetchURL($token_endpoint, $post_params, $headers), false); } /** @@ -863,7 +828,7 @@ public function requestClientCredentialsToken() { * @return mixed * @throws OpenIDConnectClientException */ - public function requestResourceOwnerToken($bClientAuth = FALSE) { + public function requestResourceOwnerToken(bool $bClientAuth = false) { $token_endpoint = $this->getProviderConfigValue('token_endpoint'); $headers = []; @@ -891,7 +856,7 @@ public function requestResourceOwnerToken($bClientAuth = FALSE) { // Convert token params to string format $post_params = http_build_query($post_data, '', '&', $this->encType); - return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); + return json_decode($this->fetchURL($token_endpoint, $post_params, $headers), false); } @@ -903,7 +868,7 @@ public function requestResourceOwnerToken($bClientAuth = FALSE) { * @return mixed * @throws OpenIDConnectClientException */ - protected function requestTokens($code, $headers = array()) { + protected function requestTokens(string $code, array $headers = []) { $token_endpoint = $this->getProviderConfigValue('token_endpoint'); $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); @@ -921,11 +886,10 @@ protected function requestTokens($code, $headers = array()) { # Consider Basic authentication if provider config is set this way if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) { $authorizationHeader = 'Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret)); - unset($token_params['client_secret']); - unset($token_params['client_id']); + unset($token_params['client_secret'], $token_params['client_id']); } - // When there is a private key jwt generator and it is supported then use it as client authentication + // When there is a private key jwt generator, and it is supported then use it as client authentication if ($this->privateKeyJwtGenerator !== null && $this->supportsAuthMethod('private_key_jwt', $token_endpoint_auth_methods_supported)) { $token_params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; $token_params['client_assertion'] = $this->privateKeyJwtGenerator->__invoke($token_endpoint); @@ -967,7 +931,7 @@ protected function requestTokens($code, $headers = array()) { $headers[] = $authorizationHeader; } - $this->tokenResponse = json_decode($this->fetchURL($token_endpoint, $token_params, $headers)); + $this->tokenResponse = json_decode($this->fetchURL($token_endpoint, $token_params, $headers), false); return $this->tokenResponse; } @@ -982,7 +946,7 @@ protected function requestTokens($code, $headers = array()) { * @return mixed * @throws OpenIDConnectClientException */ - public function requestTokenExchange($subjectToken, $subjectTokenType, $audience = '') { + public function requestTokenExchange(string $subjectToken, string $subjectTokenType, string $audience = '') { $token_endpoint = $this->getProviderConfigValue('token_endpoint'); $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); $headers = []; @@ -1004,14 +968,13 @@ public function requestTokenExchange($subjectToken, $subjectTokenType, $audience # Consider Basic authentication if provider config is set this way if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) { $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; - unset($post_data['client_secret']); - unset($post_data['client_id']); + unset($post_data['client_secret'], $post_data['client_id']); } // Convert token params to string format $post_params = http_build_query($post_data, null, '&', $this->encType); - return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); + return json_decode($this->fetchURL($token_endpoint, $post_params, $headers), false); } @@ -1022,7 +985,7 @@ public function requestTokenExchange($subjectToken, $subjectTokenType, $audience * @return mixed * @throws OpenIDConnectClientException */ - public function refreshToken($refresh_token) { + public function refreshToken(string $refresh_token) { $token_endpoint = $this->getProviderConfigValue('token_endpoint'); $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); @@ -1041,8 +1004,7 @@ public function refreshToken($refresh_token) { # Consider Basic authentication if provider config is set this way if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) { $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; - unset($token_params['client_secret']); - unset($token_params['client_id']); + unset($token_params['client_secret'], $token_params['client_id']); } if ($this->supportsAuthMethod('client_secret_jwt', $token_endpoint_auth_methods_supported)) { @@ -1057,13 +1019,12 @@ public function refreshToken($refresh_token) { $token_params['client_assertion_type']=$client_assertion_type; $token_params['client_assertion'] = $client_assertion; - unset($token_params['client_secret']); - unset($token_params['client_id']); + unset($token_params['client_secret'], $token_params['client_id']); } // Convert token params to string format $token_params = http_build_query($token_params, '', '&', $this->encType); - $json = json_decode($this->fetchURL($token_endpoint, $token_params, $headers)); + $json = json_decode($this->fetchURL($token_endpoint, $token_params, $headers), false); if (isset($json->access_token)) { $this->accessToken = $json->access_token; @@ -1077,21 +1038,16 @@ public function refreshToken($refresh_token) { } /** - * @param array $keys - * @param array $header * @throws OpenIDConnectClientException - * @return object */ - private function getKeyForHeader($keys, $header) { + private function getKeyForHeader(array $keys, stdClass $header) { foreach ($keys as $key) { if ($key->kty === 'RSA') { if (!isset($header->kid) || $key->kid === $header->kid) { return $key; } - } else { - if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { - return $key; - } + } else if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { + return $key; } } if ($this->additionalJwks) { @@ -1100,10 +1056,8 @@ private function getKeyForHeader($keys, $header) { if (!isset($header->kid) || $key->kid === $header->kid) { return $key; } - } else { - if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { - return $key; - } + } else if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { + return $key; } } } @@ -1114,94 +1068,43 @@ private function getKeyForHeader($keys, $header) { throw new OpenIDConnectClientException('Unable to find a key for RSA'); } - /** - * @param string $hashtype - * @param object $key - * @param $payload - * @param $signature - * @param $signatureType - * @return bool * @throws OpenIDConnectClientException */ - private function verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $signatureType) { - if (!class_exists('\phpseclib3\Crypt\RSA') && !class_exists('\phpseclib\Crypt\RSA') && !class_exists('Crypt_RSA')) { - throw new OpenIDConnectClientException('Crypt_RSA support unavailable.'); - } + private function verifyRSAJWTSignature(string $hashType, stdClass $key, $payload, $signature, $signatureType): bool + { if (!(property_exists($key, 'n') && property_exists($key, 'e'))) { throw new OpenIDConnectClientException('Malformed key object'); } - /* We already have base64url-encoded data, so re-encode it as - regular base64 and use the XML key format for simplicity. - */ - $public_key_xml = "\r\n". - ' ' . b64url2b64($key->n) . "\r\n" . - ' ' . b64url2b64($key->e) . "\r\n" . - ''; - if (class_exists('\phpseclib3\Crypt\RSA', false)) { - $key = \phpseclib3\Crypt\PublicKeyLoader::load($public_key_xml) - ->withHash($hashtype); - if ($signatureType === 'PSS') { - $key = $key->withMGFHash($hashtype) - ->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS); - } else { - $key = $key->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PKCS1); - } - return $key->verify($payload, $signature); - } elseif (class_exists('Crypt_RSA', false)) { - $rsa = new Crypt_RSA(); - $rsa->setHash($hashtype); - if ($signatureType === 'PSS') { - $rsa->setMGFHash($hashtype); - } - $rsa->loadKey($public_key_xml, Crypt_RSA::PUBLIC_FORMAT_XML); - $rsa->setSignatureMode($signatureType === 'PSS' ? Crypt_RSA::SIGNATURE_PSS : Crypt_RSA::SIGNATURE_PKCS1); - return $rsa->verify($payload, $signature); + $key = RSA::load([ + 'publicExponent' => new BigInteger(base64_decode(b64url2b64($key->e)), 256), + 'modulus' => new BigInteger(base64_decode(b64url2b64($key->n)), 256), + 'isPublicKey' => true, + ]) + ->withHash($hashType); + if ($signatureType === 'PSS') { + $key = $key->withMGFHash($hashType) + ->withPadding(RSA::SIGNATURE_PSS); } else { - $rsa = new \phpseclib\Crypt\RSA(); - $rsa->setHash($hashtype); - if ($signatureType === 'PSS') { - $rsa->setMGFHash($hashtype); - } - $rsa->loadKey($public_key_xml, \phpseclib\Crypt\RSA::PUBLIC_FORMAT_XML); - $rsa->setSignatureMode($signatureType === 'PSS' ? \phpseclib\Crypt\RSA::SIGNATURE_PSS : \phpseclib\Crypt\RSA::SIGNATURE_PKCS1); - return $rsa->verify($payload, $signature); + $key = $key->withPadding(RSA::SIGNATURE_PKCS1); } + return $key->verify($payload, $signature); } - /** - * @param string $hashtype - * @param string $key - * @param $payload - * @param $signature - * @return bool - * @throws OpenIDConnectClientException - */ - private function verifyHMACJWTsignature($hashtype, $key, $payload, $signature) + private function verifyHMACJWTSignature(string $hashType, string $key, string $payload, string $signature): bool { - if (!function_exists('hash_hmac')) { - throw new OpenIDConnectClientException('hash_hmac support unavailable.'); - } - - $expected=hash_hmac($hashtype, $payload, $key, true); - - if (function_exists('hash_equals')) { - return hash_equals($signature, $expected); - } - - return self::hashEquals($signature, $expected); + $expected = hash_hmac($hashType, $payload, $key, true); + return hash_equals($signature, $expected); } /** * @param string $jwt encoded JWT - * @throws OpenIDConnectClientException * @return bool + * @throws OpenIDConnectClientException */ - public function verifyJWTsignature($jwt) { - if (!\is_string($jwt)) { - throw new OpenIDConnectClientException('Error token is not a string'); - } + public function verifyJWTSignature(string $jwt): bool + { $parts = explode('.', $jwt); if (!isset($parts[0])) { throw new OpenIDConnectClientException('Error missing part 0 in token'); @@ -1210,8 +1113,8 @@ public function verifyJWTsignature($jwt) { if (false === $signature || '' === $signature) { throw new OpenIDConnectClientException('Error decoding signature from token'); } - $header = json_decode(base64url_decode($parts[0])); - if (null === $header || !\is_object($header)) { + $header = json_decode(base64url_decode($parts[0]), false); + if (!is_object($header)) { throw new OpenIDConnectClientException('Error decoding JSON from token header'); } if (!isset($header->alg)) { @@ -1225,7 +1128,7 @@ public function verifyJWTsignature($jwt) { case 'PS512': case 'RS384': case 'RS512': - $hashtype = 'sha' . substr($header->alg, 2); + $hashType = 'sha' . substr($header->alg, 2); $signatureType = $header->alg === 'PS256' || $header->alg === 'PS512' ? 'PSS' : ''; if (isset($header->jwk)) { $jwk = $header->jwk; @@ -1238,15 +1141,15 @@ public function verifyJWTsignature($jwt) { $jwk = $this->getKeyForHeader($jwks->keys, $header); } - $verified = $this->verifyRSAJWTsignature($hashtype, + $verified = $this->verifyRSAJWTSignature($hashType, $jwk, $payload, $signature, $signatureType); break; case 'HS256': case 'HS512': case 'HS384': - $hashtype = 'SHA' . substr($header->alg, 2); - $verified = $this->verifyHMACJWTsignature($hashtype, $this->getClientSecret(), $payload, $signature); + $hashType = 'SHA' . substr($header->alg, 2); + $verified = $this->verifyHMACJWTSignature($hashType, $this->getClientSecret(), $payload, $signature); break; default: throw new OpenIDConnectClientException('No support for signature type: ' . $header->alg); @@ -1259,17 +1162,13 @@ public function verifyJWTsignature($jwt) { * @return void * @throws OpenIDConnectClientException */ - public function verifySignatures($jwt) + public function verifySignatures(string $jwt) { - if ($this->canVerifySignatures()) { - if (!$this->getProviderConfigValue('jwks_uri')) { - throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); - } - if (!$this->verifyJWTsignature($jwt)) { - throw new OpenIDConnectClientException ('Unable to verify signature'); - } - } else { - user_error('Warning: JWT signature verification unavailable.'); + if (!$this->getProviderConfigValue('jwks_uri')) { + throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); + } + if (!$this->verifyJWTSignature($jwt)) { + throw new OpenIDConnectClientException ('Unable to verify signature'); } } @@ -1278,7 +1177,8 @@ public function verifySignatures($jwt) * @return bool * @throws OpenIDConnectClientException */ - protected function validateIssuer($iss) { + protected function validateIssuer(string $iss): bool + { if ($this->issuerValidator !== null) { return $this->issuerValidator->__invoke($iss); } @@ -1290,9 +1190,11 @@ protected function validateIssuer($iss) { * @param object $claims * @param string|null $accessToken * @return bool + * @throws OpenIDConnectClientException */ - protected function verifyJWTclaims($claims, $accessToken = null) { - if(isset($claims->at_hash) && isset($accessToken)) { + protected function verifyJWTClaims($claims, string $accessToken = null): bool + { + if(isset($claims->at_hash, $accessToken)) { if(isset($this->getIdTokenHeader()->alg) && $this->getIdTokenHeader()->alg !== 'none') { $bit = substr($this->getIdTokenHeader()->alg, 2, 3); } else { @@ -1305,21 +1207,17 @@ protected function verifyJWTclaims($claims, $accessToken = null) { return (($this->validateIssuer($claims->iss)) && (($claims->aud === $this->clientID) || in_array($this->clientID, $claims->aud, true)) && (!isset($claims->nonce) || $claims->nonce === $this->getNonce()) - && ( !isset($claims->exp) || ((gettype($claims->exp) === 'integer') && ($claims->exp >= time() - $this->leeway))) - && ( !isset($claims->nbf) || ((gettype($claims->nbf) === 'integer') && ($claims->nbf <= time() + $this->leeway))) + && ( !isset($claims->exp) || ((is_int($claims->exp)) && ($claims->exp >= time() - $this->leeway))) + && ( !isset($claims->nbf) || ((is_int($claims->nbf)) && ($claims->nbf <= time() + $this->leeway))) && ( !isset($claims->at_hash) || !isset($accessToken) || $claims->at_hash === $expected_at_hash ) ); } - /** - * @param string $str - * @return string - */ - protected function urlEncode($str) { + protected function urlEncode(string $str): string + { $enc = base64_encode($str); $enc = rtrim($enc, '='); - $enc = strtr($enc, '+/', '-_'); - return $enc; + return strtr($enc, '+/', '-_'); } /** @@ -1327,10 +1225,10 @@ protected function urlEncode($str) { * @param int $section the section we would like to decode * @return object */ - protected function decodeJWT($jwt, $section = 0) { + protected function decodeJWT(string $jwt, int $section = 0): stdClass { $parts = explode('.', $jwt); - return json_decode(base64url_decode($parts[$section])); + return json_decode(base64url_decode($parts[$section]), false); } /** @@ -1355,13 +1253,13 @@ protected function decodeJWT($jwt, $section = 0) { * 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. * 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. * 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. - * 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. + * updated_time string Time the End-User's information was last updated, represented as an RFC 3339 [RFC3339] datetime. For example, 2011-01-03T23:58:42+0000. * * @return mixed * * @throws OpenIDConnectClientException */ - public function requestUserInfo($attribute = null) { + public function requestUserInfo(string $attribute = null) { $user_info_endpoint = $this->getProviderConfigValue('userinfo_endpoint'); $schema = 'openid'; @@ -1370,11 +1268,11 @@ public function requestUserInfo($attribute = null) { //The accessToken has to be sent in the Authorization header. // Accept json to indicate response type - $headers = ["Authorization: Bearer {$this->accessToken}", + $headers = ["Authorization: Bearer $this->accessToken", 'Accept: application/json']; $response = $this->fetchURL($user_info_endpoint,null,$headers); - if ($this->getResponseCode() <> 200) { + if ($this->getResponseCode() !== 200) { throw new OpenIDConnectClientException('The communication to retrieve user data has failed with status code '.$this->getResponseCode()); } @@ -1397,23 +1295,23 @@ public function requestUserInfo($attribute = null) { $claims = $this->decodeJWT($jwt, 1); // Verify the JWT claims - if (!$this->verifyJWTclaims($claims)) { + if (!$this->verifyJWTClaims($claims)) { throw new OpenIDConnectClientException('Invalid JWT signature'); } $user_json = $claims; } else { - $user_json = json_decode($response); + $user_json = json_decode($response, false); } - $this->userInfo = $user_json; + $userInfo = $user_json; if($attribute === null) { - return $this->userInfo; + return $userInfo; } - if (property_exists($this->userInfo, $attribute)) { - return $this->userInfo->$attribute; + if (property_exists($userInfo, $attribute)) { + return $userInfo->$attribute; } return null; @@ -1432,13 +1330,13 @@ public function requestUserInfo($attribute = null) { * aud string Audience * nonce string nonce * iat int Issued At - * auth_time int Authenatication time + * auth_time int Authentication time * oid string Object id * * @return mixed * */ - public function getVerifiedClaims($attribute = null) { + public function getVerifiedClaims(string $attribute = null) { if($attribute === null) { return $this->verifiedClaims; @@ -1454,11 +1352,11 @@ public function getVerifiedClaims($attribute = null) { /** * @param string $url * @param string | null $post_body string If this is set the post type will be POST - * @param array $headers Extra headers to be send with the request. Format as 'NameHeader: ValueHeader' + * @param array $headers Extra headers to be sent with the request. Format as 'NameHeader: ValueHeader' + * @return bool|string * @throws OpenIDConnectClientException - * @return mixed */ - protected function fetchURL($url, $post_body = null, $headers = []) { + protected function fetchURL(string $url, string $post_body = null, array $headers = []) { // OK cool - then let's create a new cURL resource handle $ch = curl_init(); @@ -1466,7 +1364,7 @@ protected function fetchURL($url, $post_body = null, $headers = []) { // Determine whether this is a GET or POST if ($post_body !== null) { // curl_setopt($ch, CURLOPT_POST, 1); - // Alows to keep the POST method even after redirect + // Allows to keep the POST method even after redirect curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body); @@ -1474,13 +1372,12 @@ protected function fetchURL($url, $post_body = null, $headers = []) { $content_type = 'application/x-www-form-urlencoded'; // Determine if this is a JSON payload and add the appropriate content type - if (is_object(json_decode($post_body))) { + if (is_object(json_decode($post_body, false))) { $content_type = 'application/json'; } // Add POST-specific headers - $headers[] = "Content-Type: {$content_type}"; - + $headers[] = "Content-Type: $content_type"; } // If we set some headers include them @@ -1546,20 +1443,18 @@ protected function fetchURL($url, $post_body = null, $headers = []) { } /** - * @param bool $appendSlash - * @return string * @throws OpenIDConnectClientException */ - public function getWellKnownIssuer($appendSlash = false) { - + public function getWellKnownIssuer(bool $appendSlash = false): string + { return $this->getWellKnownConfigValue('issuer') . ($appendSlash ? '/' : ''); } /** - * @return string * @throws OpenIDConnectClientException */ - public function getIssuer() { + public function getIssuer(): string + { if (!isset($this->providerConfig['issuer'])) { throw new OpenIDConnectClientException('The issuer has not been set'); @@ -1580,25 +1475,16 @@ public function getProviderURL() { return $this->providerConfig['providerUrl']; } - /** - * @param string $url - */ - public function redirect($url) { + public function redirect(string $url) { header('Location: ' . $url); exit; } - /** - * @param string $httpProxy - */ - public function setHttpProxy($httpProxy) { + public function setHttpProxy(string $httpProxy) { $this->httpProxy = $httpProxy; } - /** - * @param string $certPath - */ - public function setCertPath($certPath) { + public function setCertPath(string $certPath) { $this->certPath = $certPath; } @@ -1609,48 +1495,33 @@ public function getCertPath() { return $this->certPath; } - /** - * @param bool $verifyPeer - */ - public function setVerifyPeer($verifyPeer) { + public function setVerifyPeer(bool $verifyPeer) { $this->verifyPeer = $verifyPeer; } - /** - * @param bool $verifyHost - */ - public function setVerifyHost($verifyHost) { + public function setVerifyHost(bool $verifyHost) { $this->verifyHost = $verifyHost; } - /** * Controls whether http header HTTP_UPGRADE_INSECURE_REQUESTS should be considered * defaults to true - * @param bool $httpUpgradeInsecureRequests */ - public function setHttpUpgradeInsecureRequests($httpUpgradeInsecureRequests) { + public function setHttpUpgradeInsecureRequests(bool $httpUpgradeInsecureRequests) { $this->httpUpgradeInsecureRequests = $httpUpgradeInsecureRequests; } - /** - * @return bool - */ - public function getVerifyHost() { + public function getVerifyHost(): bool + { return $this->verifyHost; } - /** - * @return bool - */ - public function getVerifyPeer() { + public function getVerifyPeer(): bool + { return $this->verifyPeer; } - /** - * @return bool - */ - public function getHttpUpgradeInsecureRequests() + public function getHttpUpgradeInsecureRequests(): bool { return $this->httpUpgradeInsecureRequests; } @@ -1659,10 +1530,8 @@ public function getHttpUpgradeInsecureRequests() * Use this for custom issuer validation * The given function should accept the issuer string from the JWT claim as the only argument * and return true if the issuer is valid, otherwise return false - * - * @param callable $issuerValidator */ - public function setIssuerValidator($issuerValidator) { + public function setIssuerValidator(callable $issuerValidator) { $this->issuerValidator = $issuerValidator; } @@ -1671,24 +1540,20 @@ public function setIssuerValidator($issuerValidator) { * The given function should accept the token_endpoint string as the only argument * and return a jwt signed with your private key according to: * https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication - * - * @param callable $privateKeyJwtGenerator */ - public function setPrivateKeyJwtGenerator($privateKeyJwtGenerator) { + public function setPrivateKeyJwtGenerator(callable $privateKeyJwtGenerator) { $this->privateKeyJwtGenerator = $privateKeyJwtGenerator; } - /** - * @param bool $allowImplicitFlow - */ - public function setAllowImplicitFlow($allowImplicitFlow) { + public function setAllowImplicitFlow(bool $allowImplicitFlow) { $this->allowImplicitFlow = $allowImplicitFlow; } /** * @return bool */ - public function getAllowImplicitFlow() { + public function getAllowImplicitFlow(): bool + { return $this->allowImplicitFlow; } @@ -1699,21 +1564,15 @@ public function getAllowImplicitFlow() { * @param array $array * simple key => value */ - public function providerConfigParam($array) { + public function providerConfigParam(array $array) { $this->providerConfig = array_merge($this->providerConfig, $array); } - /** - * @param string $clientSecret - */ - public function setClientSecret($clientSecret) { + public function setClientSecret(string $clientSecret) { $this->clientSecret = $clientSecret; } - /** - * @param string $clientID - */ - public function setClientID($clientID) { + public function setClientID(string $clientID) { $this->clientID = $clientID; } @@ -1734,7 +1593,7 @@ public function register() { $response = $this->fetchURL($registration_endpoint, json_encode($send_object)); - $json_response = json_decode($response); + $json_response = json_decode($response, false); // Throw some errors if we encounter them if ($json_response === false) { @@ -1752,10 +1611,8 @@ public function register() { if (isset($json_response->{'client_secret'})) { $this->setClientSecret($json_response->{'client_secret'}); } else { - throw new OpenIDConnectClientException('Error registering: - Please contact the OpenID Connect provider and obtain a Client ID and Secret directly from them'); + throw new OpenIDConnectClientException('Error registering: Please contact the OpenID Connect provider and obtain a Client ID and Secret directly from them'); } - } /** @@ -1768,8 +1625,9 @@ public function register() { * @param string|null $clientSecret * @return mixed * @throws OpenIDConnectClientException + * @throws Exception */ - public function introspectToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) { + public function introspectToken(string $token, string $token_type_hint = '', string $clientId = null, string $clientSecret = null) { $introspection_endpoint = $this->getProviderConfigValue('introspection_endpoint'); $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); @@ -1778,8 +1636,8 @@ public function introspectToken($token, $token_type_hint = '', $clientId = null, if ($token_type_hint) { $post_data['token_type_hint'] = $token_type_hint; } - $clientId = $clientId !== null ? $clientId : $this->clientID; - $clientSecret = $clientSecret !== null ? $clientSecret : $this->clientSecret; + $clientId = $clientId ?? $this->clientID; + $clientSecret = $clientSecret ?? $this->clientSecret; // Convert token params to string format $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), @@ -1796,7 +1654,7 @@ public function introspectToken($token, $token_type_hint = '', $clientId = null, $post_params = http_build_query($post_data, '', '&'); - return json_decode($this->fetchURL($introspection_endpoint, $post_params, $headers)); + return json_decode($this->fetchURL($introspection_endpoint, $post_params, $headers), false); } /** @@ -1810,7 +1668,7 @@ public function introspectToken($token, $token_type_hint = '', $clientId = null, * @return mixed * @throws OpenIDConnectClientException */ - public function revokeToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) { + public function revokeToken(string $token, string $token_type_hint = '', string $clientId = null, string $clientSecret = null) { $revocation_endpoint = $this->getProviderConfigValue('revocation_endpoint'); $post_data = ['token' => $token]; @@ -1818,28 +1676,23 @@ public function revokeToken($token, $token_type_hint = '', $clientId = null, $cl if ($token_type_hint) { $post_data['token_type_hint'] = $token_type_hint; } - $clientId = $clientId !== null ? $clientId : $this->clientID; - $clientSecret = $clientSecret !== null ? $clientSecret : $this->clientSecret; + $clientId = $clientId ?? $this->clientID; + $clientSecret = $clientSecret ?? $this->clientSecret; // Convert token params to string format $post_params = http_build_query($post_data, '', '&'); $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), 'Accept: application/json']; - return json_decode($this->fetchURL($revocation_endpoint, $post_params, $headers)); + return json_decode($this->fetchURL($revocation_endpoint, $post_params, $headers), false); } - /** - * @return string - */ - public function getClientName() { + public function getClientName(): string + { return $this->clientName; } - /** - * @param string $clientName - */ - public function setClientName($clientName) { + public function setClientName(string $clientName) { $this->clientName = $clientName; } @@ -1857,43 +1710,27 @@ public function getClientSecret() { return $this->clientSecret; } - /** - * @return bool - */ - public function canVerifySignatures() { - return class_exists('\phpseclib3\Crypt\RSA') || class_exists('\phpseclib\Crypt\RSA') || class_exists('Crypt_RSA'); - } - /** * Set the access token. * * May be required for subclasses of this Client. - * - * @param string $accessToken - * @return void */ - public function setAccessToken($accessToken) { + public function setAccessToken(string $accessToken) { $this->accessToken = $accessToken; } - /** - * @return string - */ - public function getAccessToken() { + public function getAccessToken(): string + { return $this->accessToken; } - /** - * @return string - */ - public function getRefreshToken() { + public function getRefreshToken(): string + { return $this->refreshToken; } - /** - * @return string - */ - public function getIdToken() { + public function getIdToken(): string + { return $this->idToken; } @@ -1934,11 +1771,9 @@ public function getTokenResponse() { /** * Stores nonce - * - * @param string $nonce - * @return string */ - protected function setNonce($nonce) { + protected function setNonce(string $nonce): string + { $this->setSessionKey('openid_connect_nonce', $nonce); return $nonce; } @@ -1963,11 +1798,9 @@ protected function unsetNonce() { /** * Stores $state - * - * @param string $state - * @return string */ - protected function setState($state) { + protected function setState(string $state): string + { $this->setSessionKey('openid_connect_state', $state); return $state; } @@ -1992,11 +1825,9 @@ protected function unsetState() { /** * Stores $codeVerifier - * - * @param string $codeVerifier - * @return string */ - protected function setCodeVerifier($codeVerifier) { + protected function setCodeVerifier(string $codeVerifier): string + { $this->setSessionKey('openid_connect_code_verifier', $codeVerifier); return $codeVerifier; } @@ -2024,7 +1855,8 @@ protected function unsetCodeVerifier() { * * @return int */ - public function getResponseCode() { + public function getResponseCode(): int + { return $this->responseCode; } @@ -2043,50 +1875,15 @@ public function getResponseContentType() * * @param int $timeout */ - public function setTimeout($timeout) { + public function setTimeout(int $timeout) { $this->timeOut = $timeout; } - /** - * @return int - */ - public function getTimeout() { + public function getTimeout(): int + { return $this->timeOut; } - /** - * Safely calculate length of binary string - * @param string $str - * @return int - */ - private static function safeLength($str) { - if (function_exists('mb_strlen')) { - return mb_strlen($str, '8bit'); - } - return strlen($str); - } - - /** - * Where hash_equals is not available, this provides a timing-attack safe string comparison - * @param string $str1 - * @param string $str2 - * @return bool - */ - private static function hashEquals($str1, $str2) { - $len1=static::safeLength($str1); - $len2=static::safeLength($str2); - - //compare strings without any early abort... - $len = min($len1, $len2); - $status = 0; - for ($i = 0; $i < $len; $i++) { - $status |= (ord($str1[$i]) ^ ord($str2[$i])); - } - //if strings were different lengths, we fail - $status |= ($len1 ^ $len2); - return ($status === 0); - } - /** * Use session to manage a nonce */ @@ -2102,7 +1899,7 @@ protected function commitSession() { session_write_close(); } - protected function getSessionKey($key) { + protected function getSessionKey(string $key) { $this->startSession(); if (array_key_exists($key, $_SESSION)) { @@ -2111,19 +1908,23 @@ protected function getSessionKey($key) { return false; } - protected function setSessionKey($key, $value) { + protected function setSessionKey(string $key, $value) { $this->startSession(); $_SESSION[$key] = $value; } - protected function unsetSessionKey($key) { + protected function unsetSessionKey(string $key) { $this->startSession(); unset($_SESSION[$key]); } - protected function getJWTClientAssertion($aud) { + /** + * @throws Exception + */ + protected function getJWTClientAssertion($aud): string + { $jti = hash('sha256',bin2hex(random_bytes(64))); $now = time(); @@ -2155,9 +1956,7 @@ protected function getJWTClientAssertion($aud) { // Encode Signature to Base64Url String $base64UrlSignature = $this->urlEncode($signature); - $jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; - - return $jwt; + return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; } public function setUrlEncoding($curEncoding) { @@ -2174,27 +1973,20 @@ public function setUrlEncoding($curEncoding) { default: break; } - } - /** - * @return array - */ - public function getScopes() { + public function getScopes(): array + { return $this->scopes; } - /** - * @return array - */ - public function getResponseTypes() { + public function getResponseTypes(): array + { return $this->responseTypes; } - /** - * @return array - */ - public function getAuthParams() { + public function getAuthParams(): array + { return $this->authParams; } @@ -2213,10 +2005,8 @@ public function getPrivateKeyJwtGenerator() { return $this->privateKeyJwtGenerator; } - /** - * @return int - */ - public function getLeeway() { + public function getLeeway(): int + { return $this->leeway; } @@ -2227,10 +2017,7 @@ public function getCodeChallengeMethod() { return $this->codeChallengeMethod; } - /** - * @param string $codeChallengeMethod - */ - public function setCodeChallengeMethod($codeChallengeMethod) { + public function setCodeChallengeMethod(string $codeChallengeMethod) { $this->codeChallengeMethod = $codeChallengeMethod; } @@ -2247,31 +2034,22 @@ protected function verifyJWKHeader($jwk) * @return string the JWT payload * @throws OpenIDConnectClientException */ - protected function handleJweResponse($jwe) + protected function handleJweResponse(string $jwe): string { throw new OpenIDConnectClientException('JWE response is not supported, please extend the class and implement this method'); } - /* - * @return string - */ - public function getSidFromBackChannel() { + public function getSidFromBackChannel(): string + { return $this->backChannelSid; } - /** - * @return string - */ - public function getSubjectFromBackChannel() { + public function getSubjectFromBackChannel(): string + { return $this->backChannelSubject; } - /** - * @param string $auth_method - * @param array $token_endpoint_auth_methods_supported - * @return bool - */ - public function supportsAuthMethod($auth_method, $token_endpoint_auth_methods_supported) + public function supportsAuthMethod(string $auth_method, array $token_endpoint_auth_methods_supported): bool { # client_secret_jwt has to explicitly be enabled if (!in_array($auth_method, $this->token_endpoint_auth_methods_supported, true)) { diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index d82234ed..a16be71b 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -23,7 +23,7 @@ public function testGetRedirectURL() public function testAuthenticateDoesNotThrowExceptionIfClaimsIsMissingNonce() { - $fakeClaims = new \StdClass(); + $fakeClaims = new StdClass(); $fakeClaims->iss = 'fake-issuer'; $fakeClaims->aud = 'fake-client-id'; $fakeClaims->nonce = null; @@ -33,10 +33,10 @@ public function testAuthenticateDoesNotThrowExceptionIfClaimsIsMissingNonce() $_SESSION['openid_connect_state'] = false; /** @var OpenIDConnectClient | MockObject $client */ - $client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT', 'getProviderConfigValue', 'verifyJWTsignature'])->getMock(); + $client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT', 'getProviderConfigValue', 'verifyJWTSignature'])->getMock(); $client->method('decodeJWT')->willReturn($fakeClaims); $client->method('getProviderConfigValue')->with('jwks_uri')->willReturn(true); - $client->method('verifyJWTsignature')->willReturn(true); + $client->method('verifyJWTSignature')->willReturn(true); $client->setClientID('fake-client-id'); $client->setIssuer('fake-issuer'); @@ -60,7 +60,7 @@ public function testSerialize() { $client = new OpenIDConnectClient('https://example.com', 'foo', 'bar', 'baz'); $serialized = serialize($client); - $this->assertInstanceOf('Jumbojett\OpenIDConnectClient', unserialize($serialized)); + $this->assertInstanceOf(OpenIDConnectClient::class, unserialize($serialized)); } /** @@ -75,7 +75,7 @@ public function testAuthMethodSupport($expected, $authMethod, $clientAuthMethods $this->assertEquals($expected, $client->supportsAuthMethod($authMethod, $idpAuthMethods)); } - public function provider() + public function provider(): array { return [ 'client_secret_basic - default config' => [true, 'client_secret_basic', null, ['client_secret_basic']], @@ -90,8 +90,9 @@ public function provider() } /** - * @covers Jumbojett\\OpenIDConnectClient::verifyLogoutTokenClaims + * @covers Jumbojett\\OpenIDConnectClient::verifyLogoutTokenClaims * @dataProvider provideTestVerifyLogoutTokenClaimsData + * @throws OpenIDConnectClientException */ public function testVerifyLogoutTokenClaims( $claims, $expectedResult ) { @@ -113,7 +114,8 @@ public function testVerifyLogoutTokenClaims( $claims, $expectedResult ) /** * @return array */ - public function provideTestVerifyLogoutTokenClaimsData() { + public function provideTestVerifyLogoutTokenClaimsData(): array + { return [ 'valid-single-aud' => [ (object)[ diff --git a/tests/TokenVerificationTest.php b/tests/TokenVerificationTest.php index 58449924..0715911e 100644 --- a/tests/TokenVerificationTest.php +++ b/tests/TokenVerificationTest.php @@ -2,6 +2,7 @@ use Jumbojett\OpenIDConnectClient; +use Jumbojett\OpenIDConnectClientException; use PHPUnit\Framework\MockObject\MockObject; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -10,7 +11,7 @@ class TokenVerificationTest extends TestCase /** * @param $alg * @param $jwt - * @throws \Jumbojett\OpenIDConnectClientException + * @throws OpenIDConnectClientException * @dataProvider providesTokens */ public function testTokenVerification($alg, $jwt) @@ -20,12 +21,12 @@ public function testTokenVerification($alg, $jwt) $client->method('fetchUrl')->willReturn(file_get_contents(__DIR__ . "/data/jwks-$alg.json")); $client->setProviderURL('https://jwt.io/'); $client->providerConfigParam(['jwks_uri' => 'https://jwt.io/.well-known/jwks.json']); - $verified = $client->verifyJWTsignature($jwt); + $verified = $client->verifyJWTSignature($jwt); self::assertTrue($verified); $client->setAccessToken($jwt); } - public function providesTokens() + public function providesTokens(): array { return [ 'PS256' => ['ps256', 'eyJhbGciOiJQUzI1NiIsImtpZCI6Imtvbm5lY3RkLXRva2Vucy1zaWduaW5nLWtleSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrcG9wLWh0dHBzOi8va29wYW5vLmRlbW8vbWVldC8iLCJleHAiOjE1NjgzNzE0NjEsImp0aSI6IkpkR0tDbEdOTXl2VXJpcmlRRUlWUXZCVmttT2FfQkRjIiwiaWF0IjoxNTY4MzcxMjIxLCJpc3MiOiJodHRwczovL2tvcGFuby5kZW1vIiwic3ViIjoiUHpUVWp3NHBlXzctWE5rWlBILXJxVHE0MTQ1Z3lDdlRvQmk4V1E5bFBrcW5rbEc1aktvRU5LM21Qb0I1WGY1ZTM5dFRMR2RKWXBMNEJubXFnelpaX0FAa29ubmVjdCIsImtjLmlzQWNjZXNzVG9rZW4iOnRydWUsImtjLmF1dGhvcml6ZWRTY29wZXMiOlsicHJvZmlsZSIsImVtYWlsIiwia29wYW5vL2t3bSIsImtvcGFuby9nYyIsImtvcGFuby9rdnMiLCJvcGVuaWQiXSwia2MuYXV0aG9yaXplZENsYWltcyI6eyJpZF90b2tlbiI6eyJuYW1lIjpudWxsfX0sImtjLmlkZW50aXR5Ijp7ImtjLmkuZG4iOiJKb25hcyBCcmVra2UiLCJrYy5pLmlkIjoiQUFBQUFLd2hxVkJBMCs1SXN4bjdwMU13UkNVQkFBQUFCZ0FBQUJzQUFBQk5VVDA5QUFBQUFBPT0iLCJrYy5pLnVuIjoidXNlcjEiLCJrYy5pLnVzIjoiTVEifSwia2MucHJvdmlkZXIiOiJpZGVudGlmaWVyLWtjIn0.hGRuXvul2kOiALHexwYp5MBEJVwz1YV3ehyM3AOuwCoK2w5sJxdciqqY_TfXCKyO6nAEbYLK3J0CBOjfup_IG0aCZcwzjto8khYlc4ezXkGnFsbJBNQdDGkpHtWnioWx-OJ3cXvY9F8aOvjaq0gw11ZDAcqQl0g7LTbJ9-J_yx0pmy3NGai2JB30Fh1OgSDzYfxWnE0RRgZG-x68e65RXfSBaEGW85OUh4wihxO2zdTGAHJ3Iq_-QAG4yRbXZtLx3ZspG7LNmqG-YE3huy3Rd8u3xrJNhmUOfEnz3x07q7VW0cj9NedX98BAbj3iNvksQsE0oG0J_f_Tu8Ai8VbWB72sJuXZWxANDKdz0BBYLzXhsjXkNByRq9x3zqDVsX-cVHei_XudxEOVRBjhkvW2MmIjcAHNKCKsdar865-gFG9McP4PCcBlY28tC0Cvnzyi83LBfpGRXdl6MJunnUsKQ1C79iCoVI1doK1erFN959Q-TGJfJA3Tr5LNpuGawB5rpe1nDGWvmYhg3uYfNl8uTTyvNgvvejcflEb2DURuXdqABuSiP7RkDWYtzx6mq49G0tRxelBbvyjQ2id2QjmRRdQ6dHEZ2NCJ51b8OFoDJBtxN1CD62TTxa3FUqCdZAPAUR3hHn_69vYq82MR514s-Gb67A6j2PbMPFATQP2UdK8'] From 8ec206b60f87b4a1886c9c3eefa5f5d81d1ccc35 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Thu, 4 May 2023 11:32:06 +0200 Subject: [PATCH 36/62] feat: set useragent (#370) * Set useragent * Update CHANGELOG.md * Set default useragent --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdbfd038..106f5dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Support for signed and encrypted ID Token. #305 * Update construct typehint in docblock. #364 * Fixed LogoutToken verification for single value aud claims #334 +* Added function to set useragent #370 ### Added - Support for signed and encrypted UserInfo response. #305 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index ba4fddef..8081134f 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1367,6 +1367,7 @@ protected function fetchURL(string $url, string $post_body = null, array $header // Allows to keep the POST method even after redirect curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body); + curl_setopt($ch, CURLOPT_USERAGENT, $this->getUserAgent()); // Default content type is form encoded $content_type = 'application/x-www-form-urlencoded'; @@ -2058,4 +2059,9 @@ public function supportsAuthMethod(string $auth_method, array $token_endpoint_au return in_array($auth_method, $token_endpoint_auth_methods_supported, true); } + + protected function getUserAgent(): string + { + return "jumbojett/OpenID-Connect-PHP"; + } } From c146b716a7ec0955175377b7675a457d4402fb95 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Wed, 26 Jul 2023 13:58:05 +0200 Subject: [PATCH 37/62] fix: Update well known config value function response types (#376) * Fix: Update well known config value function response types * Update CHANGELOG * Update wellknown typing array to be string[] * Update wellknown typing null can be default but would never be returned --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 106f5dba..68264e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Update construct typehint in docblock. #364 * Fixed LogoutToken verification for single value aud claims #334 * Added function to set useragent #370 +* Update well known config value function response types #376 ### Added - Support for signed and encrypted UserInfo response. #305 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 8081134f..c0015c83 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -587,13 +587,13 @@ protected function addAdditionalJwk($jwk) { * Gets anything that we need configuration wise including endpoints, and other values * * @param string $param - * @param string|array|null $default optional - * @return string|array + * @param string|string[]|bool|null $default optional + * @return string|string[]|bool * - *@throws OpenIDConnectClientException + * @throws OpenIDConnectClientException */ - protected function getProviderConfigValue(string $param, $default = null) { - + protected function getProviderConfigValue(string $param, $default = null) + { // If the configuration value is not available, attempt to fetch it from a well known config endpoint // This is also known as auto "discovery" if (!isset($this->providerConfig[$param])) { @@ -607,12 +607,12 @@ protected function getProviderConfigValue(string $param, $default = null) { * Gets anything that we need configuration wise including endpoints, and other values * * @param string $param - * @param string|null $default optional - * @return string + * @param string|string[]|bool|null $default optional + * @return string|string[]|bool * - *@throws OpenIDConnectClientException + * @throws OpenIDConnectClientException */ - protected function getWellKnownConfigValue(string $param, string $default = null): string + protected function getWellKnownConfigValue(string $param, $default = null) { // If the configuration value is not available, attempt to fetch it from a well known config endpoint From 5d69bcf15478bf11f32f7344afaa2f2640b9bd2a Mon Sep 17 00:00:00 2001 From: mig5 Date: Tue, 1 Aug 2023 17:57:54 +1000 Subject: [PATCH 38/62] Set the User-Agent regardless of GET or POST (#382) --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68264e32..81f127d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] +* User-Agent is set for any HTTP method in fetchURL() (not just POST). #382 * Update visibility of getWellKnownConfigValue to protected. #363 * Fixed issue on authentication for php8. #354 * Support for signed and encrypted UserInfo response. #305 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index c0015c83..6aa80b17 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1367,7 +1367,6 @@ protected function fetchURL(string $url, string $post_body = null, array $header // Allows to keep the POST method even after redirect curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body); - curl_setopt($ch, CURLOPT_USERAGENT, $this->getUserAgent()); // Default content type is form encoded $content_type = 'application/x-www-form-urlencoded'; @@ -1381,6 +1380,9 @@ protected function fetchURL(string $url, string $post_body = null, array $header $headers[] = "Content-Type: $content_type"; } + // Set the User-Agent + curl_setopt($ch, CURLOPT_USERAGENT, $this->getUserAgent()); + // If we set some headers include them if(count($headers) > 0) { curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); From 7be38be967dd7845d0700924a69af34ef1c4077f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:03:27 +0100 Subject: [PATCH 39/62] release: 1.0.0 (#402) --- CHANGELOG.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f127d2..d4be9ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,20 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] -* User-Agent is set for any HTTP method in fetchURL() (not just POST). #382 -* Update visibility of getWellKnownConfigValue to protected. #363 -* Fixed issue on authentication for php8. #354 -* Support for signed and encrypted UserInfo response. #305 -* Support for signed and encrypted ID Token. #305 -* Update construct typehint in docblock. #364 -* Fixed LogoutToken verification for single value aud claims #334 -* Added function to set useragent #370 -* Update well known config value function response types #376 +## [1.0.0] - 2023-12-13 ### Added -- Support for signed and encrypted UserInfo response. #305 -- Support for signed and encrypted ID Token. #305 +- PHP 7.0 is required. #327 +- Support for signed and encrypted UserInfo response and ID Token. #305 +- Allow to set User-Agent header. #370 + +### Fixed +- User-Agent is set for any HTTP method in fetchURL() (not just POST). #382 +- Update visibility of getWellKnownConfigValue to protected. #363 +- Fixed issue on authentication for php8. #354 +- Update construct typehint in docblock. #364 +- Fixed LogoutToken verification for single value aud claims. #334 +- Update well known config value function response types. #376 ## [0.9.10] - 2022-09-30 From 4e325951c3e9edf7bcb3e12dfa9f9548513bd1d5 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Mon, 22 Apr 2024 11:25:52 +0200 Subject: [PATCH 40/62] chore: Update ci to support php 8.3 and add dependabot (#407) * Add php 8.3 to test matrix in github actions and updated actions * Updated readme PHP requirement to PHP 7.0+ * Added dependabot for GitHub Actions --- .github/dependabot.yml | 13 +++++++++++++ .github/workflows/build.yml | 6 +++--- CHANGELOG.md | 5 +++++ README.md | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..0f11f4a3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 + +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 515cd64c..a3081708 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] steps: - name: Checkout @@ -25,9 +25,9 @@ jobs: php-version: ${{ matrix.php }} - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d4be9ff7..dd05c792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +[unreleased] +- Updated CI to also test on PHP 8.3 #407 +- Updated readme PHP requirement to PHP 7.0+ #407 +- Added dependabot for GitHub Actions #407 + ## [1.0.0] - 2023-12-13 ### Added diff --git a/README.md b/README.md index 79318e50..173c2e4b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ the OpenID Connect protocol to set up authentication. A special thanks goes to Justin Richer and Amanda Anganes for their help and support of the protocol. # Requirements # - 1. PHP 5.4 or greater + 1. PHP 7.0 or greater 2. CURL extension 3. JSON extension From 73af840733b418fd03d6f3697d5074a20e81f0c3 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Mon, 22 Apr 2024 05:27:03 -0400 Subject: [PATCH 41/62] docs: Update README.md to correct addScope parameter type in 1.0.0 (#405) * Update README.md Correct the calls to addScope which now requires an array, not a string * Replaced usage of array() with [] * remove redundant addScope call from documentation --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 173c2e4b..904b83ec 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ use Jumbojett\OpenIDConnectClient; $oidc = new OpenIDConnectClient('https://id.provider.com', 'ClientIDHere', 'ClientSecretHere'); -$oidc->providerConfigParam(array('token_endpoint'=>'https://id.provider.com/connect/token')); -$oidc->addScope('my_scope'); +$oidc->providerConfigParam(['token_endpoint'=>'https://id.provider.com/connect/token']); +$oidc->addScope(['my_scope']); // this assumes success (to validate check if the access_token property is there and a valid JWT) : $clientCredentialsToken = $oidc->requestClientCredentialsToken()->access_token; @@ -85,12 +85,12 @@ use Jumbojett\OpenIDConnectClient; $oidc = new OpenIDConnectClient('https://id.provider.com', 'ClientIDHere', 'ClientSecretHere'); -$oidc->providerConfigParam(array('token_endpoint'=>'https://id.provider.com/connect/token')); -$oidc->addScope('my_scope'); +$oidc->providerConfigParam(['token_endpoint'=>'https://id.provider.com/connect/token']); +$oidc->addScope(['my_scope']); //Add username and password -$oidc->addAuthParam(array('username'=>'')); -$oidc->addAuthParam(array('password'=>'')); +$oidc->addAuthParam(['username'=>'']); +$oidc->addAuthParam(['password'=>'']); //Perform the auth and return the token (to validate check if the access_token property is there and a valid JWT) : $token = $oidc->requestResourceOwnerToken(TRUE)->access_token; @@ -105,10 +105,9 @@ use Jumbojett\OpenIDConnectClient; $oidc = new OpenIDConnectClient('https://id.provider.com', 'ClientIDHere', 'ClientSecretHere'); -$oidc->setResponseTypes(array('id_token')); -$oidc->addScope(array('openid')); +$oidc->setResponseTypes(['id_token']); $oidc->setAllowImplicitFlow(true); -$oidc->addAuthParam(array('response_mode' => 'form_post')); +$oidc->addAuthParam(['response_mode' => 'form_post']); $oidc->setCertPath('/path/to/my.cert'); $oidc->authenticate(); $sub = $oidc->getVerifiedClaims('sub'); @@ -184,7 +183,7 @@ function handleLogout() { session_commit(); session_id($session_id_to_destroy); // switches to that session session_start(); - $_SESSION = array(); // effectively ends the session + $_SESSION = []; // effectively ends the session } } } From 6ac3ed427f9386a433b7d7506dc7bc7d0a59ea74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:30:43 +0200 Subject: [PATCH 42/62] chore(deps): bump actions/checkout from 2 to 4 (#416) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3081708..52f9e1d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 with: From f5fadf1436c33777fdded7e1226da9baaf87f76e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:31:03 +0200 Subject: [PATCH 43/62] chore(deps): bump actions/cache from 3 to 4 (#417) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52f9e1d3..c27126e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} From e31ec338d36c7c942df6dec4a0903e1563fa2074 Mon Sep 17 00:00:00 2001 From: Tim Smid Date: Mon, 22 Apr 2024 11:37:47 +0200 Subject: [PATCH 44/62] fix: Cast SERVER_PORT to integer (#404) --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd05c792..05283d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated CI to also test on PHP 8.3 #407 - Updated readme PHP requirement to PHP 7.0+ #407 - Added dependabot for GitHub Actions #407 +- Cast `$_SERVER['SERVER_PORT']` to integer to prevent adding 80 or 443 port to redirect URL. #403 ## [1.0.0] - 2023-12-13 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 6aa80b17..aea060fc 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -696,7 +696,7 @@ public function getRedirectURL(): string if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) { $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT']; } elseif (isset($_SERVER['SERVER_PORT'])) { - $port = $_SERVER['SERVER_PORT']; + $port = (int)$_SERVER['SERVER_PORT']; } elseif ($protocol === 'https') { $port = 443; } else { From 0c8f54dd5f05bacee87f7abd10282a8c881affc3 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Mon, 22 Apr 2024 15:18:47 +0200 Subject: [PATCH 45/62] fix: Check if subject is equal to subject of id token when verifying JWT claims (#406) * Check if subject is equal to subject of id token when verifying JWT claims * Add fake sub in test claims --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 1 + tests/OpenIDConnectClientTest.php | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05283d97..93149158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated readme PHP requirement to PHP 7.0+ #407 - Added dependabot for GitHub Actions #407 - Cast `$_SERVER['SERVER_PORT']` to integer to prevent adding 80 or 443 port to redirect URL. #403 +- Check subject when verifying JWT #406 ## [1.0.0] - 2023-12-13 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index aea060fc..abb37d8e 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1206,6 +1206,7 @@ protected function verifyJWTClaims($claims, string $accessToken = null): bool } return (($this->validateIssuer($claims->iss)) && (($claims->aud === $this->clientID) || in_array($this->clientID, $claims->aud, true)) + && ($claims->sub === $this->getIdTokenPayload()->sub) && (!isset($claims->nonce) || $claims->nonce === $this->getNonce()) && ( !isset($claims->exp) || ((is_int($claims->exp)) && ($claims->exp >= time() - $this->leeway))) && ( !isset($claims->nbf) || ((is_int($claims->nbf)) && ($claims->nbf <= time() + $this->leeway))) diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index a16be71b..f895879c 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -26,6 +26,7 @@ public function testAuthenticateDoesNotThrowExceptionIfClaimsIsMissingNonce() $fakeClaims = new StdClass(); $fakeClaims->iss = 'fake-issuer'; $fakeClaims->aud = 'fake-client-id'; + $fakeClaims->sub = 'fake-sub'; $fakeClaims->nonce = null; $_REQUEST['id_token'] = 'abc.123.xyz'; From 1a468a40175e6d3366328678309623985ca2b150 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Tue, 23 Apr 2024 11:16:21 +0200 Subject: [PATCH 46/62] fix: Removed duplicate check on jwks_uri and only check if jwks_uri exists when needed (#373) * Removed duplicate check on jwks_uri * Update CHANGELOG * Only check jwks_uri when needed * Update changelog --- CHANGELOG.md | 1 + src/OpenIDConnectClient.php | 17 +++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93149158..4055711f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added dependabot for GitHub Actions #407 - Cast `$_SERVER['SERVER_PORT']` to integer to prevent adding 80 or 443 port to redirect URL. #403 - Check subject when verifying JWT #406 +- Removed duplicate check on jwks_uri and only check if jwks_uri exists when needed #373 ## [1.0.0] - 2023-12-13 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index abb37d8e..adabf80c 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -469,12 +469,7 @@ public function verifyLogoutToken(): bool $claims = $this->decodeJWT($logout_token, 1); // Verify the signature - if (!$this->getProviderConfigValue('jwks_uri')) { - throw new OpenIDConnectClientException('Back-channel logout: Unable to verify signature due to no jwks_uri being defined'); - } - if (!$this->verifyJWTSignature($logout_token)) { - throw new OpenIDConnectClientException('Back-channel logout: Unable to verify JWT signature'); - } + $this->verifySignatures($logout_token); // Verify Logout Token Claims if ($this->verifyLogoutTokenClaims($claims)) { @@ -1134,7 +1129,12 @@ public function verifyJWTSignature(string $jwt): bool $jwk = $header->jwk; $this->verifyJWKHeader($jwk); } else { - $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri')), false); + $jwksUri = $this->getProviderConfigValue('jwks_uri'); + if (!$jwksUri) { + throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); + } + + $jwks = json_decode($this->fetchURL($jwksUri), false); if ($jwks === NULL) { throw new OpenIDConnectClientException('Error decoding JSON from jwks_uri'); } @@ -1164,9 +1164,6 @@ public function verifyJWTSignature(string $jwt): bool */ public function verifySignatures(string $jwt) { - if (!$this->getProviderConfigValue('jwks_uri')) { - throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); - } if (!$this->verifyJWTSignature($jwt)) { throw new OpenIDConnectClientException ('Unable to verify signature'); } From 1e854438f32075f0670b7e48c9ab3587c9913a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:53:08 +0200 Subject: [PATCH 47/62] fix: method signatures after 1.0 release (#427) --- src/OpenIDConnectClient.php | 36 ++++++++++++++++++++----------- tests/OpenIDConnectClientTest.php | 35 +++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index adabf80c..60f89bd8 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -3,7 +3,7 @@ * * Copyright MITRE 2020 * - * OpenIDConnectClient for PHP5 + * OpenIDConnectClient for PHP7+ * Author: Michael Jett * * Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -25,7 +25,6 @@ use Error; use Exception; -use phpseclib3\Crypt\PublicKeyLoader; use phpseclib3\Crypt\RSA; use phpseclib3\Math\BigInteger; use stdClass; @@ -380,7 +379,7 @@ public function authenticate(): bool $accessToken = $_REQUEST['access_token'] ?? null; // Do an OpenID Connect session check - if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { + if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { throw new OpenIDConnectClientException('Unable to determine state'); } @@ -691,7 +690,7 @@ public function getRedirectURL(): string if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) { $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT']; } elseif (isset($_SERVER['SERVER_PORT'])) { - $port = (int)$_SERVER['SERVER_PORT']; + $port = $_SERVER['SERVER_PORT']; } elseif ($protocol === 'https') { $port = 443; } else { @@ -1221,10 +1220,9 @@ protected function urlEncode(string $str): string /** * @param string $jwt encoded JWT * @param int $section the section we would like to decode - * @return object + * @return object|null */ - protected function decodeJWT(string $jwt, int $section = 0): stdClass { - + protected function decodeJWT(string $jwt, int $section = 0) { $parts = explode('.', $jwt); return json_decode(base64url_decode($parts[$section]), false); } @@ -1688,7 +1686,10 @@ public function revokeToken(string $token, string $token_type_hint = '', string return json_decode($this->fetchURL($revocation_endpoint, $post_params, $headers), false); } - public function getClientName(): string + /** + * @return string|null + */ + public function getClientName() { return $this->clientName; } @@ -1698,14 +1699,14 @@ public function setClientName(string $clientName) { } /** - * @return string + * @return string|null */ public function getClientID() { return $this->clientID; } /** - * @return string + * @return string|null */ public function getClientSecret() { return $this->clientSecret; @@ -1720,17 +1721,26 @@ public function setAccessToken(string $accessToken) { $this->accessToken = $accessToken; } - public function getAccessToken(): string + /** + * @return string|null + */ + public function getAccessToken() { return $this->accessToken; } - public function getRefreshToken(): string + /** + * @return string|null + */ + public function getRefreshToken() { return $this->refreshToken; } - public function getIdToken(): string + /** + * @return string|null + */ + public function getIdToken() { return $this->idToken; } diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index f895879c..88d98989 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -7,9 +7,38 @@ class OpenIDConnectClientTest extends TestCase { - /** - * @return void - */ + public function testJWTDecode() + { + $client = new OpenIDConnectClient(); + $client->setAccessToken(''); + $header = $client->getAccessTokenHeader(); + self::assertEquals('', $header); + } + + public function testGetNull() + { + $client = new OpenIDConnectClient(); + self::assertNull($client->getAccessToken()); + self::assertNull($client->getRefreshToken()); + self::assertNull($client->getIdToken()); + self::assertNull($client->getClientName()); + self::assertNull($client->getClientID()); + self::assertNull($client->getClientSecret()); + self::assertNull($client->getCertPath()); + } + + public function testResponseTypes() + { + $client = new OpenIDConnectClient(); + self::assertEquals([], $client->getResponseTypes()); + + $client->setResponseTypes('foo'); + self::assertEquals(['foo'], $client->getResponseTypes()); + + $client->setResponseTypes(['bar', 'ipsum']); + self::assertEquals(['foo', 'bar', 'ipsum'], $client->getResponseTypes()); + } + public function testGetRedirectURL() { $client = new OpenIDConnectClient(); From 0509be83cae7984e011ad3ee26f2dae4af67edf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:29:25 +0200 Subject: [PATCH 48/62] fix: handle JWT decode of non JWT tokens (#428) --- CHANGELOG.md | 2 ++ src/OpenIDConnectClient.php | 14 +++++++++----- tests/OpenIDConnectClientTest.php | 11 +++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4055711f..fa7fddf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [unreleased] +- Fix JWT decode of non JWT tokens #428 +- Fix method signatures #427 - Updated CI to also test on PHP 8.3 #407 - Updated readme PHP requirement to PHP 7.0+ #407 - Added dependabot for GitHub Actions #407 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 60f89bd8..16c3d656 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1220,11 +1220,11 @@ protected function urlEncode(string $str): string /** * @param string $jwt encoded JWT * @param int $section the section we would like to decode - * @return object|null + * @return object|string|null */ protected function decodeJWT(string $jwt, int $section = 0) { $parts = explode('.', $jwt); - return json_decode(base64url_decode($parts[$section]), false); + return json_decode(base64url_decode($parts[$section] ?? ''), false); } /** @@ -1737,6 +1737,10 @@ public function getRefreshToken() return $this->refreshToken; } + public function setIdToken(string $idToken) { + $this->idToken = $idToken; + } + /** * @return string|null */ @@ -1753,21 +1757,21 @@ public function getAccessTokenHeader() { } /** - * @return object + * @return object|string|null */ public function getAccessTokenPayload() { return $this->decodeJWT($this->accessToken, 1); } /** - * @return object + * @return object|string|null */ public function getIdTokenHeader() { return $this->decodeJWT($this->idToken); } /** - * @return object + * @return object|string|null */ public function getIdTokenPayload() { return $this->decodeJWT($this->idToken, 1); diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index 88d98989..3dc4709f 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -10,9 +10,20 @@ class OpenIDConnectClientTest extends TestCase public function testJWTDecode() { $client = new OpenIDConnectClient(); + # access token $client->setAccessToken(''); $header = $client->getAccessTokenHeader(); self::assertEquals('', $header); + $payload = $client->getAccessTokenPayload(); + self::assertEquals('', $payload); + + # id token + $client->setIdToken(''); + $header = $client->getIdTokenHeader(); + self::assertEquals('', $header); + $payload = $client->getIdTokenPayload(); + self::assertEquals('', $payload); + } public function testGetNull() From 0fbf8f2533b4529c9fd14b1bf3cb2d29cb1ef061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:33:44 +0200 Subject: [PATCH 49/62] chore: enable dependabot for composer (#429) --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0f11f4a3..a54473b4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,9 @@ updates: directory: "/" schedule: interval: "weekly" + + # Maintain dependencies for composer + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" From 036530bb69c32849478a6bde2fe6b4c89e60b3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:39:11 +0200 Subject: [PATCH 50/62] ci: run GitHub workflows on pull requests and pushes to master (#431) --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c27126e2..31d0ed11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,13 @@ --- name: build -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + branches: + - master env: DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi" From e316397c876a602ff87d62bee2adb96cdb497f9a Mon Sep 17 00:00:00 2001 From: Artem Boyko Date: Thu, 5 Sep 2024 17:43:09 +0300 Subject: [PATCH 51/62] chore(deps): update phpseclib/phpseclib requirement from ~3.0 to ^3.0.7 * Update phpseclib/phpseclib to minimum 2.0.31 or 3.0.7 * Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3fa6d231..3cd6fc38 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "php": ">=7.0", "ext-json": "*", "ext-curl": "*", - "phpseclib/phpseclib": "~3.0" + "phpseclib/phpseclib": "^3.0.7" }, "require-dev": { "roave/security-advisories": "dev-latest", From 22560300d349b32c17e073198ec3b2f6dd2c0230 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:55:24 +0200 Subject: [PATCH 52/62] chore(deps-dev): update yoast/phpunit-polyfills requirement from ^1.0 to ^2.0 (#430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps-dev): update yoast/phpunit-polyfills requirement Updates the requirements on [yoast/phpunit-polyfills](https://github.com/Yoast/PHPUnit-Polyfills) to permit the latest version. - [Release notes](https://github.com/Yoast/PHPUnit-Polyfills/releases) - [Changelog](https://github.com/Yoast/PHPUnit-Polyfills/blob/2.x/CHANGELOG.md) - [Commits](https://github.com/Yoast/PHPUnit-Polyfills/compare/1.0.0...2.0.1) --- updated-dependencies: - dependency-name: yoast/phpunit-polyfills dependency-type: direct:development ... Signed-off-by: dependabot[bot] * fix: remove --verbose from phpunit * fix: force usage of phpunit < 10 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- composer.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31d0ed11..38080af1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,4 +41,4 @@ jobs: - name: Install dependencies run: composer update $DEFAULT_COMPOSER_FLAGS - name: Run unit tests - run: vendor/bin/phpunit --verbose --colors=always tests + run: vendor/bin/phpunit --colors=always tests diff --git a/composer.json b/composer.json index 3cd6fc38..64825884 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,9 @@ "phpseclib/phpseclib": "^3.0.7" }, "require-dev": { + "phpunit/phpunit": "<10", "roave/security-advisories": "dev-latest", - "yoast/phpunit-polyfills": "^1.0" + "yoast/phpunit-polyfills": "^2.0" }, "archive" : { "exclude" : [ From 765ddbd65643d69edce461617ea3c620d582714e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 6 Sep 2024 08:16:24 +0200 Subject: [PATCH 53/62] fix: protected $responseCode to allow proper overloading of fetchURL() (#433) --- src/OpenIDConnectClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 16c3d656..e12ad9c6 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -144,7 +144,7 @@ class OpenIDConnectClient /** * @var int|null Response code from the server */ - private $responseCode; + protected $responseCode; /** * @var string|null Content type from the server From 75693117e99c209b9aef70fc7b279fe561304cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 6 Sep 2024 08:22:32 +0200 Subject: [PATCH 54/62] release: v1.0.1 (#432) --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7fddf6..1b39eead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -[unreleased] +## [1.0.1] - 2024-09-05 + +### Fixed - Fix JWT decode of non JWT tokens #428 - Fix method signatures #427 -- Updated CI to also test on PHP 8.3 #407 -- Updated readme PHP requirement to PHP 7.0+ #407 -- Added dependabot for GitHub Actions #407 - Cast `$_SERVER['SERVER_PORT']` to integer to prevent adding 80 or 443 port to redirect URL. #403 - Check subject when verifying JWT #406 - Removed duplicate check on jwks_uri and only check if jwks_uri exists when needed #373 From db1ed8b5f1664db3af99feba114de34542a10a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 13 Sep 2024 08:52:32 +0200 Subject: [PATCH 55/62] fix: bring back #404 (#437) --- src/OpenIDConnectClient.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index e12ad9c6..e3f9d3f2 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -690,7 +690,8 @@ public function getRedirectURL(): string if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) { $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT']; } elseif (isset($_SERVER['SERVER_PORT'])) { - $port = $_SERVER['SERVER_PORT']; + # keep this case - even if some tool claim it is unnecessary + $port = (int)$_SERVER['SERVER_PORT']; } elseif ($protocol === 'https') { $port = 443; } else { From a5994e793a72b19747da880b6428b55638a3ebcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:07:45 +0200 Subject: [PATCH 56/62] test: add unit test for SERVER_PORT type cast (#438) --- tests/OpenIDConnectClientTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index 3dc4709f..45adc7b3 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -58,7 +58,11 @@ public function testGetRedirectURL() $_SERVER['SERVER_NAME'] = 'domain.test'; $_SERVER['REQUEST_URI'] = '/path/index.php?foo=bar&baz#fragment'; + $_SERVER['SERVER_PORT'] = '443'; self::assertSame('http://domain.test/path/index.php', $client->getRedirectURL()); + + $_SERVER['SERVER_PORT'] = '8888'; + self::assertSame('http://domain.test:8888/path/index.php', $client->getRedirectURL()); } public function testAuthenticateDoesNotThrowExceptionIfClaimsIsMissingNonce() From 9af21bd04f5b564bdd18993a2b15d88b7594f152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:09:33 +0200 Subject: [PATCH 57/62] release: v1.0.2 (#439) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b39eead..0e1c7105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.1] - 2024-09-13 + +### Fixed +- Cast `$_SERVER['SERVER_PORT']` to integer to prevent adding 80 or 443 port to redirect URL. #437 + ## [1.0.1] - 2024-09-05 ### Fixed From 60919af91a29b61d728d8b5a02d8af84271d0eaa Mon Sep 17 00:00:00 2001 From: Robert Vogel <1201528+osnard@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:48:09 +0200 Subject: [PATCH 58/62] Fix TypeError in `verifyJWTClaims` (#442) ... when ClientID does not match Co-authored-by: Robert Vogel --- src/OpenIDConnectClient.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index e3f9d3f2..b38a81cd 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1201,8 +1201,10 @@ protected function verifyJWTClaims($claims, string $accessToken = null): bool $len = ((int)$bit)/16; $expected_at_hash = $this->urlEncode(substr(hash('sha'.$bit, $accessToken, true), 0, $len)); } + $auds = $claims->aud; + $auds = is_array( $auds ) ? $auds : [ $auds ]; return (($this->validateIssuer($claims->iss)) - && (($claims->aud === $this->clientID) || in_array($this->clientID, $claims->aud, true)) + && (in_array($this->clientID, $auds, true)) && ($claims->sub === $this->getIdTokenPayload()->sub) && (!isset($claims->nonce) || $claims->nonce === $this->getNonce()) && ( !isset($claims->exp) || ((is_int($claims->exp)) && ($claims->exp >= time() - $this->leeway))) From 97adbcee4b519700ad14abc9f73962077a4c6b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:06:55 +0200 Subject: [PATCH 59/62] test: unit tests for verifyJWTClaims and different aud claims (#443) --- tests/OpenIDConnectClientTest.php | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index 45adc7b3..4b46923d 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -7,6 +7,48 @@ class OpenIDConnectClientTest extends TestCase { + public function testValidateClaims() + { + $client = new class extends OpenIDConnectClient { + public function testVerifyJWTClaims($claims): bool + { + return $this->verifyJWTClaims($claims); + } + public function getIdTokenPayload() + { + return (object)[ + 'sub' => 'sub' + ]; + } + }; + $client->setClientID('client-id'); + $client->setIssuer('issuer'); + $client->setIdToken(''); + + # simple aud + $valid = $client->testVerifyJWTClaims((object)[ + 'aud' => 'client-id', + 'iss' => 'issuer', + 'sub' => 'sub', + ]); + self::assertTrue($valid); + + # array aud + $valid = $client->testVerifyJWTClaims((object)[ + 'aud' => ['client-id'], + 'iss' => 'issuer', + 'sub' => 'sub', + ]); + self::assertTrue($valid); + + # aud not matching + $valid = $client->testVerifyJWTClaims((object)[ + 'aud' => ['ipsum'], + 'iss' => 'issuer', + 'sub' => 'sub', + ]); + self::assertFalse($valid); + } public function testJWTDecode() { $client = new OpenIDConnectClient(); @@ -23,7 +65,6 @@ public function testJWTDecode() self::assertEquals('', $header); $payload = $client->getIdTokenPayload(); self::assertEquals('', $payload); - } public function testGetNull() From f7c91b9079bf96323b5eb3ab4383f297f21d98d1 Mon Sep 17 00:00:00 2001 From: Rick Lambrechts Date: Fri, 27 Sep 2024 13:01:32 +0200 Subject: [PATCH 60/62] fix: protected responseContentType to allow overloading of fetchUrl function (#446) --- src/OpenIDConnectClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index b38a81cd..07c00540 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -149,7 +149,7 @@ class OpenIDConnectClient /** * @var string|null Content type from the server */ - private $responseContentType; + protected $responseContentType; /** * @var array holds response types From dc0ef658aacaab48fcbbba9953217f301b92dd71 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Mon, 3 Mar 2025 22:01:51 +0000 Subject: [PATCH 61/62] Create .gitattributes (#459) --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e989f244 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/.* export-ignore +/tests/ export-ignore +/phpunit.xml.dist export-ignore From 808027b63d36e0b37e2bd905ac800e27a7df447f Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Mon, 3 Mar 2025 23:06:10 +0100 Subject: [PATCH 62/62] Stop adding ?schema=openid to userinfo endpoint URL. (#449) --- CHANGELOG.md | 5 +++++ src/OpenIDConnectClient.php | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1c7105..a6ef83d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +### Changed +- Stop adding ?schema=openid to userinfo endpoint URL. #449 + ## [1.0.1] - 2024-09-13 ### Fixed diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 07c00540..7b859c56 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -1261,9 +1261,6 @@ protected function decodeJWT(string $jwt, int $section = 0) { public function requestUserInfo(string $attribute = null) { $user_info_endpoint = $this->getProviderConfigValue('userinfo_endpoint'); - $schema = 'openid'; - - $user_info_endpoint .= '?schema=' . $schema; //The accessToken has to be sent in the Authorization header. // Accept json to indicate response type