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" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c27126e2..38080af1 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" @@ -35,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/CHANGELOG.md b/CHANGELOG.md index 4055711f..0e1c7105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,16 @@ 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.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 +- Fix JWT decode of non JWT tokens #428 +- Fix method signatures #427 - 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 diff --git a/composer.json b/composer.json index 3fa6d231..64825884 100644 --- a/composer.json +++ b/composer.json @@ -6,11 +6,12 @@ "php": ">=7.0", "ext-json": "*", "ext-curl": "*", - "phpseclib/phpseclib": "~3.0" + "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" : [ diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 12692d8a..10f0ff83 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; @@ -80,12 +79,12 @@ class OpenIDConnectClient /** * @var string arbitrary id value */ - private $clientID; + public $clientID; /** * @var string arbitrary name value */ - private $clientName; + public $clientName; /** * @var string arbitrary secret value @@ -95,164 +94,164 @@ class OpenIDConnectClient /** * @var array holds the provider configuration */ - private $providerConfig = []; + public $providerConfig = []; /** * @var string http proxy if necessary */ - private $httpProxy; + public $httpProxy; /** * @var string full system path to the SSL certificate */ - private $certPath; + public $certPath; /** * @var bool Verify SSL peer on transactions */ - private $verifyPeer = true; + public $verifyPeer = true; /** * @var bool Verify peer hostname on transactions */ - private $verifyHost = true; + public $verifyHost = true; /** * @var string if we acquire an access token it will be stored here */ - protected $accessToken; + public $accessToken; /** * @var string if we acquire a refresh token it will be stored here */ - private $refreshToken; + public $refreshToken; /** * @var string if we acquire an id token it will be stored here */ - protected $idToken; + public $idToken; /** * @var object stores the token response */ - private $tokenResponse; + public $tokenResponse; /** * @var array holds scopes */ - private $scopes = []; + public $scopes = []; /** * @var int|null Response code from the server */ - private $responseCode; + public $responseCode; /** * @var string|null Content type from the server */ - private $responseContentType; + public $responseContentType; /** * @var array holds response types */ - private $responseTypes = []; + public $responseTypes = []; /** * @var array holds authentication parameters */ - private $authParams = []; + public $authParams = []; /** * @var array holds additional registration parameters for example post_logout_redirect_uris */ - private $registrationParams = []; + public $registrationParams = []; /** * @var mixed holds well-known openid server properties */ - private $wellKnown = false; + public $wellKnown = false; /** * @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 = []; + public $wellKnownConfigParameters = []; /** * @var int timeout (seconds) */ - protected $timeOut = 60; + public $timeOut = 60; /** * @var int leeway (seconds) */ - private $leeway = 300; + public $leeway = 300; /** * @var array holds response types */ - private $additionalJwks = []; + public $additionalJwks = []; /** * @var object holds verified jwt claims */ - protected $verifiedClaims = []; + public $verifiedClaims = []; /** * @var callable|null validator function for issuer claim */ - private $issuerValidator; + public $issuerValidator; /** * @var callable|null generator function for private key jwt client authentication */ - private $privateKeyJwtGenerator; + public $privateKeyJwtGenerator; /** * @var bool Allow OAuth 2 implicit flow; see http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth */ - private $allowImplicitFlow = false; + public $allowImplicitFlow = false; /** * @var string */ - private $redirectURL; + public $redirectURL; /** * @var int defines which URL-encoding http_build_query() uses */ - protected $encType = PHP_QUERY_RFC1738; + public $encType = PHP_QUERY_RFC1738; /** * @var bool Enable or disable upgrading to HTTPS by paying attention to HTTP header HTTP_UPGRADE_INSECURE_REQUESTS */ - protected $httpUpgradeInsecureRequests = true; + public $httpUpgradeInsecureRequests = true; /** * @var string holds code challenge method for PKCE mode * @see https://tools.ietf.org/html/rfc7636 */ - private $codeChallengeMethod = false; + public $codeChallengeMethod = false; /** * @var array holds PKCE supported algorithms */ - private $pkceAlgs = ['S256' => 'sha256', 'plain' => false]; + public $pkceAlgs = ['S256' => 'sha256', 'plain' => false]; /** * @var string if we acquire a sid in back-channel logout it will be stored here */ - private $backChannelSid; + public $backChannelSid; /** * @var string if we acquire a sub in back-channel logout it will be stored here */ - private $backChannelSubject; + public $backChannelSubject; /** * @var array list of supported auth methods */ - private $token_endpoint_auth_methods_supported = ['client_secret_basic']; + public $token_endpoint_auth_methods_supported = ['client_secret_basic']; /** * @param string|null $provider_url optional @@ -320,7 +319,7 @@ public function authenticate(): bool } // 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'); } @@ -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,6 +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'])) { + # keep this case - even if some tool claim it is unnecessary $port = (int)$_SERVER['SERVER_PORT']; } elseif ($protocol === 'https') { $port = 443; @@ -872,9 +872,9 @@ protected function requestTokens(string $code, array $headers = []) { $token_params = [ 'grant_type' => $grant_type, 'code' => $code, - 'redirect_uri' => $this->getRedirectURL(), 'client_id' => $this->clientID, - 'client_secret' => $this->clientSecret + 'client_secret' => $this->clientSecret, + 'redirect_uri' => urlencode($this->getRedirectURL()) ]; $authorizationHeader = null; @@ -925,9 +925,10 @@ protected function requestTokens(string $code, array $headers = []) { if (null !== $authorizationHeader) { $headers[] = $authorizationHeader; } + $headers[] = 'Accept: */*'; + $headers[] = 'Content-Type: application/x-www-form-urlencoded'; $this->tokenResponse = json_decode($this->fetchURL($token_endpoint, $token_params, $headers), false); - return $this->tokenResponse; } @@ -1221,12 +1222,11 @@ 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|string|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); + return json_decode(base64url_decode($parts[$section] ?? ''), false); } /** @@ -1271,7 +1271,7 @@ public function requestUserInfo(string $attribute = null) { $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()); + throw new OpenIDConnectClientException('The communication to retrieve user data has failed with status code '.$this->getResponseCode() . 'and response ' . json_encode($response)); } // When we receive application/jwt, the UserInfo Response is signed and/or encrypted. @@ -1359,6 +1359,8 @@ protected function fetchURL(string $url, string $post_body = null, array $header // OK cool - then let's create a new cURL resource handle $ch = curl_init(); + // curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + // Determine whether this is a GET or POST if ($post_body !== null) { // curl_setopt($ch, CURLOPT_POST, 1); @@ -1376,6 +1378,7 @@ protected function fetchURL(string $url, string $post_body = null, array $header // Add POST-specific headers $headers[] = "Content-Type: $content_type"; + $headers[] = 'Content-Length: ' . strlen($post_body); } // Set the User-Agent @@ -1688,7 +1691,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 +1704,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 +1726,30 @@ 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 + public function setIdToken(string $idToken) { + $this->idToken = $idToken; + } + + /** + * @return string|null + */ + public function getIdToken() { return $this->idToken; } @@ -1743,21 +1762,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 f895879c..45adc7b3 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -7,9 +7,49 @@ class OpenIDConnectClientTest extends TestCase { - /** - * @return void - */ + 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() + { + $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(); @@ -18,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()