From 8ef1281865a9eae03522c8aa945d80d26647229a Mon Sep 17 00:00:00 2001 From: Jeroen Derks Date: Mon, 28 Nov 2022 20:48:42 +0100 Subject: [PATCH 01/13] updated composer.json added replace, updated README.md added notification about project forked from --- README.md | 4 +++- composer.json | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 79318e50..f6d2db45 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +NB: This is a fork from [jumbojett/OpenID-Connect-PHP](https://github.com/jumbojett/OpenID-Connect-PHP) to allow client basic authentication on obtaining the access token. + PHP OpenID Connect Basic Client ======================== A simple library that allows an application to authenticate a user through the basic OpenID Connect flow. @@ -14,7 +16,7 @@ A special thanks goes to Justin Richer and Amanda Anganes for their help and sup ## Install ## 1. Install library using composer ``` -composer require jumbojett/openid-connect-php +composer require magentron/openid-connect-php ``` 2. Include composer autoloader diff --git a/composer.json b/composer.json index 6d218ccf..c8f0f22e 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "jumbojett/openid-connect-php", - "description": "Bare-bones OpenID Connect client", + "name": "magentron/openid-connect-php", + "description": "Bare-bones OpenID Connect client (forked to allow client secret basic authentication on obtaining access token)", "license": "Apache-2.0", "require": { "php": ">=5.4", @@ -13,6 +13,9 @@ "roave/security-advisories": "dev-master", "yoast/phpunit-polyfills": "^1.0" }, + "replace": { + "jumbojett/openid-connect-php": "<=0.9.10" + }, "archive" : { "exclude" : [ ".*" 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 11/13] 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 12/13] 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 13/13] 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'); }