From dd65e4a567017e83f68f2e9570f96d2cdefac6e8 Mon Sep 17 00:00:00 2001 From: Bart Gloudemans Date: Thu, 12 Sep 2024 13:49:16 +0200 Subject: [PATCH 1/8] allow caller to choose between "magic handle-it-all-at-one-end-point" approach or an approach where more control is required and underlaying actions need to be called seperately --- src/OpenIDConnectClient.php | 240 +++++++++++++++++++----------------- 1 file changed, 130 insertions(+), 110 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index e12ad9c6..86efcd58 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -291,133 +291,152 @@ public function setIssuer($issuer) { public function setResponseTypes($response_types) { $this->responseTypes = array_merge($this->responseTypes, (array)$response_types); } + + /** + * use this method to magically handle all incoming OIDC requests at one endpoint + * if you need more control, use the handleError(), handleCode(), handleClaims(), requestAuthorisation() and redirect() methods + * @return bool + * @throws OpenIDConnectClientException + */ + 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'])) { + // always throws an exception, routine will end here + $this->handleError($_REQUEST['error'], $_REQUEST['error_description'] ?? null); + } + + // If we have an authorization code then proceed to request a token + if (isset($_REQUEST['code'])) { + return $this->handleCode($_REQUEST['code']); + } + + if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { + return $this->handleClaims($_REQUEST['id_token'], $_REQUEST['access_token'] ?? null, $_REQUEST['state'] ?? null); + } + + $auth_endpoint = $this->requestAuthorization(); + $this->redirect($auth_endpoint); + + return false; + } + + /** + * @throws OpenIDConnectClientException + */ + public function handleError(string $error, string $errorDescription = null) { + $desc = $errorDescription !== null ? ' Description: '.$errorDescription : ''; + throw new OpenIDConnectClientException('Error: '.$error.$desc); + } + + /** + * @throws OpenIDConnectClientException + */ + public function handleCode(string $code): bool { + $token_json = $this->requestTokens($code); + + // Throw an error if the server returns one + if (isset($token_json->error)) { + if (isset($token_json->error_description)) { + throw new OpenIDConnectClientException($token_json->error_description); + } + throw new OpenIDConnectClientException('Got response: '.$token_json->error); + } + + // Do an OpenID Connect session check + if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { + throw new OpenIDConnectClientException('Unable to determine state'); + } + + // Cleanup state + $this->unsetState(); + + if (!property_exists($token_json, 'id_token')) { + throw new OpenIDConnectClientException('User did not authorize openid scope.'); + } + + $id_token = $token_json->id_token; + $idTokenHeaders = $this->decodeJWT($id_token); + if (isset($idTokenHeaders->enc)) { + // Handle JWE + $id_token = $this->handleJweResponse($id_token); + } + + $claims = $this->decodeJWT($id_token, 1); + + // Verify the signature + $this->verifySignatures($id_token); + + // Save the id token + $this->idToken = $id_token; + + // Save the access token + $this->accessToken = $token_json->access_token; + + // If this is a valid claim + if ($this->verifyJWTClaims($claims, $token_json->access_token)) { + + // Clean up the session a little + $this->unsetNonce(); + + // Save the full response + $this->tokenResponse = $token_json; + + // Save the verified claims + $this->verifiedClaims = $claims; + + // Save the refresh token, if we got one + if (isset($token_json->refresh_token)) { + $this->refreshToken = $token_json->refresh_token; + } + + // Success! + return true; + } + + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + } /** - * @return bool * @throws OpenIDConnectClientException */ - public function authenticate(): bool + public function handleClaims(string $id_token, string $accessToken = null, string $state = null): 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'] : ''; - throw new OpenIDConnectClientException('Error: ' . $_REQUEST['error'] .$desc); + // Do an OpenID Connect session check + if ($state === null || ($state !== $this->getState())) { + throw new OpenIDConnectClientException('Unable to determine state'); } - // If we have an authorization code then proceed to request a token - if (isset($_REQUEST['code'])) { - - $code = $_REQUEST['code']; - $token_json = $this->requestTokens($code); - - // Throw an error if the server returns one - if (isset($token_json->error)) { - if (isset($token_json->error_description)) { - throw new OpenIDConnectClientException($token_json->error_description); - } - throw new OpenIDConnectClientException('Got response: ' . $token_json->error); - } - - // Do an OpenID Connect session check - if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { - throw new OpenIDConnectClientException('Unable to determine state'); - } + // Cleanup state + $this->unsetState(); - // Cleanup state - $this->unsetState(); + $claims = $this->decodeJWT($id_token, 1); - if (!property_exists($token_json, 'id_token')) { - throw new OpenIDConnectClientException('User did not authorize openid scope.'); - } + // Verify the signature + $this->verifySignatures($id_token); - $id_token = $token_json->id_token; - $idTokenHeaders = $this->decodeJWT($id_token); - if (isset($idTokenHeaders->enc)) { - // Handle JWE - $id_token = $this->handleJweResponse($id_token); - } + // Save the id token + $this->idToken = $id_token; - $claims = $this->decodeJWT($id_token, 1); + // If this is a valid claim + if ($this->verifyJWTClaims($claims, $accessToken)) { - // Verify the signature - $this->verifySignatures($id_token); + // Clean up the session a little + $this->unsetNonce(); - // Save the id token - $this->idToken = $id_token; + // Save the verified claims + $this->verifiedClaims = $claims; // Save the access token - $this->accessToken = $token_json->access_token; - - // If this is a valid claim - if ($this->verifyJWTClaims($claims, $token_json->access_token)) { - - // Clean up the session a little - $this->unsetNonce(); - - // Save the full response - $this->tokenResponse = $token_json; - - // Save the verified claims - $this->verifiedClaims = $claims; - - // Save the refresh token, if we got one - if (isset($token_json->refresh_token)) { - $this->refreshToken = $token_json->refresh_token; - } - - // Success! - return true; + if ($accessToken) { + $this->accessToken = $accessToken; } - throw new OpenIDConnectClientException ('Unable to verify JWT claims'); - } - - if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { - // if we have no code but an id_token use that - $id_token = $_REQUEST['id_token']; - - $accessToken = $_REQUEST['access_token'] ?? null; - - // Do an OpenID Connect session check - if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { - throw new OpenIDConnectClientException('Unable to determine state'); - } - - // Cleanup state - $this->unsetState(); - - $claims = $this->decodeJWT($id_token, 1); - - // Verify the signature - $this->verifySignatures($id_token); - - // Save the id token - $this->idToken = $id_token; - - // If this is a valid claim - if ($this->verifyJWTClaims($claims, $accessToken)) { - - // Clean up the session a little - $this->unsetNonce(); - - // Save the verified claims - $this->verifiedClaims = $claims; - - // Save the access token - if ($accessToken) { - $this->accessToken = $accessToken; - } - - // Success! - return true; - - } + // Success! + return true; - throw new OpenIDConnectClientException ('Unable to verify JWT claims'); } - $this->requestAuthorization(); - return false; + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); } /** @@ -732,11 +751,11 @@ protected function generateRandString(): string /** * Start Here - * @return void + * @return string * @throws OpenIDConnectClientException * @throws Exception */ - private function requestAuthorization() { + public function requestAuthorization(): string { $auth_endpoint = $this->getProviderConfigValue('authorization_endpoint'); $response_type = 'code'; @@ -786,7 +805,8 @@ private function requestAuthorization() { $auth_endpoint .= (strpos($auth_endpoint, '?') === false ? '?' : '&') . http_build_query($auth_params, '', '&', $this->encType); $this->commitSession(); - $this->redirect($auth_endpoint); + + return $auth_endpoint; } /** From 501bb2ce006904d9cdc89726d9d70e514aec969a Mon Sep 17 00:00:00 2001 From: Bart Gloudemans Date: Thu, 12 Sep 2024 17:32:29 +0200 Subject: [PATCH 2/8] better phrasing --- src/OpenIDConnectClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 86efcd58..2e5d64eb 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -293,8 +293,8 @@ public function setResponseTypes($response_types) { } /** - * use this method to magically handle all incoming OIDC requests at one endpoint - * if you need more control, use the handleError(), handleCode(), handleClaims(), requestAuthorisation() and redirect() methods + * use this method to magically handle all incoming OIDC requests + * if you need more control per request, use the methods handleError(), handleCode(), handleClaims(), requestAuthorisation() and redirect() * @return bool * @throws OpenIDConnectClientException */ From 2ca41497cea047a72fcbe0c272f208e9e1517a1f Mon Sep 17 00:00:00 2001 From: Bart Gloudemans Date: Thu, 12 Sep 2024 17:58:46 +0200 Subject: [PATCH 3/8] update README with example of using the client in a per-action way --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 904b83ec..1d5d8869 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,37 @@ $oidc->setPrivateKeyJwtGenerator(function(string $token_endpoint) { }) ``` +## Example 11: Basic Client splitting up the process in individual actions + +```php +// controllers/oidc_request_authorization.php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + 'ClientSecretHere'); +$oidc->setCertPath('/path/to/my.cert'); + +$auth_endpoint = $oidc->requestAuthorization(); +$redirectUrl = $oidc->redirect($auth_endpoint); + +$oidc->redirect($redirectUrl); +``` + +```php +// controllers/oidc_convert_code_into_tokens.php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + 'ClientSecretHere'); +$oidc->setCertPath('/path/to/my.cert'); + +$oidc->handleCode($_REQUEST['code']); + +$idToken = $oidc->getIdToken(); +$accessToken = $oidc->getAccessToken(); +``` ## Development Environments ## In some cases you may need to disable SSL security on your development systems. From e41bafe60d7f90289bd82626e2118a0cfb567d5e Mon Sep 17 00:00:00 2001 From: Bart Gloudemans Date: Mon, 16 Sep 2024 12:50:51 +0200 Subject: [PATCH 4/8] for transparency, state is a REQUEST variable that needs to be passed on as well --- src/OpenIDConnectClient.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 2e5d64eb..8c696eca 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -307,9 +307,9 @@ public function authenticate(): bool { // If we have an authorization code then proceed to request a token if (isset($_REQUEST['code'])) { - return $this->handleCode($_REQUEST['code']); + return $this->handleCode($_REQUEST['code'], $_REQUEST['state'] ?? null); } - + if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { return $this->handleClaims($_REQUEST['id_token'], $_REQUEST['access_token'] ?? null, $_REQUEST['state'] ?? null); } @@ -331,7 +331,7 @@ public function handleError(string $error, string $errorDescription = null) { /** * @throws OpenIDConnectClientException */ - public function handleCode(string $code): bool { + public function handleCode(string $code, string $state = null): bool { $token_json = $this->requestTokens($code); // Throw an error if the server returns one @@ -343,7 +343,7 @@ public function handleCode(string $code): bool { } // Do an OpenID Connect session check - if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { + if ($state === null || ($state !== $this->getState())) { throw new OpenIDConnectClientException('Unable to determine state'); } From 0864caa5fb7972a9e028a9f584cdcd3b546658a2 Mon Sep 17 00:00:00 2001 From: Bart Gloudemans Date: Mon, 16 Sep 2024 12:51:48 +0200 Subject: [PATCH 5/8] strict typing does not allow boolean --- tests/OpenIDConnectClientTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/OpenIDConnectClientTest.php b/tests/OpenIDConnectClientTest.php index 3dc4709f..b6fae482 100644 --- a/tests/OpenIDConnectClientTest.php +++ b/tests/OpenIDConnectClientTest.php @@ -70,8 +70,8 @@ public function testAuthenticateDoesNotThrowExceptionIfClaimsIsMissingNonce() $fakeClaims->nonce = null; $_REQUEST['id_token'] = 'abc.123.xyz'; - $_REQUEST['state'] = false; - $_SESSION['openid_connect_state'] = false; + $_REQUEST['state'] = 'false'; + $_SESSION['openid_connect_state'] = 'false'; /** @var OpenIDConnectClient | MockObject $client */ $client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT', 'getProviderConfigValue', 'verifyJWTSignature'])->getMock(); From 7bb01d4b7892d5901fe8eb4b3f468552c14d53f4 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 16 Sep 2024 15:35:55 +0200 Subject: [PATCH 6/8] Update README.md typo Co-authored-by: Rick Lambrechts --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 1d5d8869..91e8dae4 100644 --- a/README.md +++ b/README.md @@ -224,9 +224,7 @@ $oidc = new OpenIDConnectClient('https://id.provider.com', $oidc->setCertPath('/path/to/my.cert'); $auth_endpoint = $oidc->requestAuthorization(); -$redirectUrl = $oidc->redirect($auth_endpoint); - -$oidc->redirect($redirectUrl); +$oidc->redirect($auth_endpoint); ``` ```php From 03fe3044f002d0fa69f927d8e4e89ef7c0e33a78 Mon Sep 17 00:00:00 2001 From: Bart Gloudemans Date: Mon, 16 Sep 2024 15:43:08 +0200 Subject: [PATCH 7/8] CS --- src/OpenIDConnectClient.php | 211 ++++++++++++++++++------------------ 1 file changed, 107 insertions(+), 104 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 8c696eca..d9d01d02 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -291,110 +291,113 @@ public function setIssuer($issuer) { public function setResponseTypes($response_types) { $this->responseTypes = array_merge($this->responseTypes, (array)$response_types); } - - /** - * use this method to magically handle all incoming OIDC requests - * if you need more control per request, use the methods handleError(), handleCode(), handleClaims(), requestAuthorisation() and redirect() - * @return bool - * @throws OpenIDConnectClientException - */ - 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'])) { - // always throws an exception, routine will end here - $this->handleError($_REQUEST['error'], $_REQUEST['error_description'] ?? null); - } - - // If we have an authorization code then proceed to request a token - if (isset($_REQUEST['code'])) { - return $this->handleCode($_REQUEST['code'], $_REQUEST['state'] ?? null); - } - - if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { - return $this->handleClaims($_REQUEST['id_token'], $_REQUEST['access_token'] ?? null, $_REQUEST['state'] ?? null); - } - - $auth_endpoint = $this->requestAuthorization(); - $this->redirect($auth_endpoint); - - return false; - } - - /** - * @throws OpenIDConnectClientException - */ - public function handleError(string $error, string $errorDescription = null) { - $desc = $errorDescription !== null ? ' Description: '.$errorDescription : ''; - throw new OpenIDConnectClientException('Error: '.$error.$desc); - } - - /** - * @throws OpenIDConnectClientException - */ - public function handleCode(string $code, string $state = null): bool { - $token_json = $this->requestTokens($code); - - // Throw an error if the server returns one - if (isset($token_json->error)) { - if (isset($token_json->error_description)) { - throw new OpenIDConnectClientException($token_json->error_description); - } - throw new OpenIDConnectClientException('Got response: '.$token_json->error); - } - - // Do an OpenID Connect session check - if ($state === null || ($state !== $this->getState())) { - throw new OpenIDConnectClientException('Unable to determine state'); - } - - // Cleanup state - $this->unsetState(); - - if (!property_exists($token_json, 'id_token')) { - throw new OpenIDConnectClientException('User did not authorize openid scope.'); - } - - $id_token = $token_json->id_token; - $idTokenHeaders = $this->decodeJWT($id_token); - if (isset($idTokenHeaders->enc)) { - // Handle JWE - $id_token = $this->handleJweResponse($id_token); - } - - $claims = $this->decodeJWT($id_token, 1); - - // Verify the signature - $this->verifySignatures($id_token); - - // Save the id token - $this->idToken = $id_token; - - // Save the access token - $this->accessToken = $token_json->access_token; - - // If this is a valid claim - if ($this->verifyJWTClaims($claims, $token_json->access_token)) { - - // Clean up the session a little - $this->unsetNonce(); - - // Save the full response - $this->tokenResponse = $token_json; - - // Save the verified claims - $this->verifiedClaims = $claims; - - // Save the refresh token, if we got one - if (isset($token_json->refresh_token)) { - $this->refreshToken = $token_json->refresh_token; - } - - // Success! - return true; - } - - throw new OpenIDConnectClientException ('Unable to verify JWT claims'); - } + + /** + * use this method to magically handle all incoming OIDC requests + * if you need more control per request, use the methods handleError(), handleCode(), handleClaims(), requestAuthorisation() and redirect() + * @return bool + * @throws OpenIDConnectClientException + */ + 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'])) { + // always throws an exception, routine will end here + $this->handleError($_REQUEST['error'], $_REQUEST['error_description'] ?? null); + } + + // If we have an authorization code then proceed to request a token + if (isset($_REQUEST['code'])) { + return $this->handleCode($_REQUEST['code'], $_REQUEST['state'] ?? null); + } + + if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { + return $this->handleClaims($_REQUEST['id_token'], $_REQUEST['access_token'] ?? null, $_REQUEST['state'] ?? null); + } + + $auth_endpoint = $this->requestAuthorization(); + $this->redirect($auth_endpoint); + + return false; + } + + /** + * @throws OpenIDConnectClientException + */ + public function handleError(string $error, string $errorDescription = null) + { + $desc = $errorDescription !== null ? ' Description: ' . $errorDescription : ''; + throw new OpenIDConnectClientException('Error: ' . $error . $desc); + } + + /** + * @throws OpenIDConnectClientException + */ + public function handleCode(string $code, string $state = null): bool + { + $token_json = $this->requestTokens($code); + + // Throw an error if the server returns one + if (isset($token_json->error)) { + if (isset($token_json->error_description)) { + throw new OpenIDConnectClientException($token_json->error_description); + } + throw new OpenIDConnectClientException('Got response: ' . $token_json->error); + } + + // Do an OpenID Connect session check + if ($state === null || ($state !== $this->getState())) { + throw new OpenIDConnectClientException('Unable to determine state'); + } + + // Cleanup state + $this->unsetState(); + + if (!property_exists($token_json, 'id_token')) { + throw new OpenIDConnectClientException('User did not authorize openid scope.'); + } + + $id_token = $token_json->id_token; + $idTokenHeaders = $this->decodeJWT($id_token); + if (isset($idTokenHeaders->enc)) { + // Handle JWE + $id_token = $this->handleJweResponse($id_token); + } + + $claims = $this->decodeJWT($id_token, 1); + + // Verify the signature + $this->verifySignatures($id_token); + + // Save the id token + $this->idToken = $id_token; + + // Save the access token + $this->accessToken = $token_json->access_token; + + // If this is a valid claim + if ($this->verifyJWTClaims($claims, $token_json->access_token)) { + + // Clean up the session a little + $this->unsetNonce(); + + // Save the full response + $this->tokenResponse = $token_json; + + // Save the verified claims + $this->verifiedClaims = $claims; + + // Save the refresh token, if we got one + if (isset($token_json->refresh_token)) { + $this->refreshToken = $token_json->refresh_token; + } + + // Success! + return true; + } + + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + } /** * @throws OpenIDConnectClientException From 60866f980927c57417560b5b80bb99371de423de Mon Sep 17 00:00:00 2001 From: Bart Gloudemans Date: Mon, 16 Sep 2024 15:44:48 +0200 Subject: [PATCH 8/8] CS early return --- src/OpenIDConnectClient.php | 55 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index d9d01d02..37a11f98 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -376,27 +376,26 @@ public function handleCode(string $code, string $state = null): bool $this->accessToken = $token_json->access_token; // If this is a valid claim - if ($this->verifyJWTClaims($claims, $token_json->access_token)) { - - // Clean up the session a little - $this->unsetNonce(); + if (!$this->verifyJWTClaims($claims, $token_json->access_token)) { + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + } - // Save the full response - $this->tokenResponse = $token_json; + // Clean up the session a little + $this->unsetNonce(); - // Save the verified claims - $this->verifiedClaims = $claims; + // Save the full response + $this->tokenResponse = $token_json; - // Save the refresh token, if we got one - if (isset($token_json->refresh_token)) { - $this->refreshToken = $token_json->refresh_token; - } + // Save the verified claims + $this->verifiedClaims = $claims; - // Success! - return true; + // Save the refresh token, if we got one + if (isset($token_json->refresh_token)) { + $this->refreshToken = $token_json->refresh_token; } - throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + // Success! + return true; } /** @@ -421,25 +420,23 @@ public function handleClaims(string $id_token, string $accessToken = null, strin $this->idToken = $id_token; // If this is a valid claim - if ($this->verifyJWTClaims($claims, $accessToken)) { - - // Clean up the session a little - $this->unsetNonce(); + if (!$this->verifyJWTClaims($claims, $accessToken)) { + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + } - // Save the verified claims - $this->verifiedClaims = $claims; + // Clean up the session a little + $this->unsetNonce(); - // Save the access token - if ($accessToken) { - $this->accessToken = $accessToken; - } - - // Success! - return true; + // Save the verified claims + $this->verifiedClaims = $claims; + // Save the access token + if ($accessToken) { + $this->accessToken = $accessToken; } - throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + // Success! + return true; } /**