diff --git a/README.md b/README.md index 904b83ec..91e8dae4 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,35 @@ $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(); +$oidc->redirect($auth_endpoint); +``` + +```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. diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index e12ad9c6..37a11f98 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -293,6 +293,8 @@ public function setResponseTypes($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 */ @@ -300,124 +302,141 @@ 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'] : ''; - throw new OpenIDConnectClientException('Error: ' . $_REQUEST['error'] .$desc); + // 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); + } - $code = $_REQUEST['code']; - $token_json = $this->requestTokens($code); + if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { + return $this->handleClaims($_REQUEST['id_token'], $_REQUEST['access_token'] ?? null, $_REQUEST['state'] ?? null); + } - // 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); - } + $auth_endpoint = $this->requestAuthorization(); + $this->redirect($auth_endpoint); - // Do an OpenID Connect session check - if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { - throw new OpenIDConnectClientException('Unable to determine state'); - } + return false; + } - // Cleanup state - $this->unsetState(); + /** + * @throws OpenIDConnectClientException + */ + public function handleError(string $error, string $errorDescription = null) + { + $desc = $errorDescription !== null ? ' Description: ' . $errorDescription : ''; + throw new OpenIDConnectClientException('Error: ' . $error . $desc); + } - if (!property_exists($token_json, 'id_token')) { - throw new OpenIDConnectClientException('User did not authorize openid scope.'); - } + /** + * @throws OpenIDConnectClientException + */ + public function handleCode(string $code, string $state = null): bool + { + $token_json = $this->requestTokens($code); - $id_token = $token_json->id_token; - $idTokenHeaders = $this->decodeJWT($id_token); - if (isset($idTokenHeaders->enc)) { - // Handle JWE - $id_token = $this->handleJweResponse($id_token); + // 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); + } - $claims = $this->decodeJWT($id_token, 1); + // Do an OpenID Connect session check + if ($state === null || ($state !== $this->getState())) { + throw new OpenIDConnectClientException('Unable to determine state'); + } - // Verify the signature - $this->verifySignatures($id_token); + // Cleanup state + $this->unsetState(); - // Save the id token - $this->idToken = $id_token; + if (!property_exists($token_json, 'id_token')) { + throw new OpenIDConnectClientException('User did not authorize openid scope.'); + } - // Save the access token - $this->accessToken = $token_json->access_token; + $id_token = $token_json->id_token; + $idTokenHeaders = $this->decodeJWT($id_token); + if (isset($idTokenHeaders->enc)) { + // Handle JWE + $id_token = $this->handleJweResponse($id_token); + } - // If this is a valid claim - if ($this->verifyJWTClaims($claims, $token_json->access_token)) { + $claims = $this->decodeJWT($id_token, 1); - // Clean up the session a little - $this->unsetNonce(); + // Verify the signature + $this->verifySignatures($id_token); - // Save the full response - $this->tokenResponse = $token_json; + // Save the id token + $this->idToken = $id_token; - // 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; - } + // Save the access token + $this->accessToken = $token_json->access_token; + // If this is a valid claim + if (!$this->verifyJWTClaims($claims, $token_json->access_token)) { 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']; + // Clean up the session a little + $this->unsetNonce(); - $accessToken = $_REQUEST['access_token'] ?? null; + // Save the full response + $this->tokenResponse = $token_json; - // Do an OpenID Connect session check - if (!isset($_REQUEST['state']) || ($_REQUEST['state'] !== $this->getState())) { - throw new OpenIDConnectClientException('Unable to determine state'); - } + // Save the verified claims + $this->verifiedClaims = $claims; - // Cleanup state - $this->unsetState(); + // Save the refresh token, if we got one + if (isset($token_json->refresh_token)) { + $this->refreshToken = $token_json->refresh_token; + } - $claims = $this->decodeJWT($id_token, 1); + // Success! + return true; + } - // Verify the signature - $this->verifySignatures($id_token); + /** + * @throws OpenIDConnectClientException + */ + public function handleClaims(string $id_token, string $accessToken = null, string $state = null): bool + { + // Do an OpenID Connect session check + if ($state === null || ($state !== $this->getState())) { + throw new OpenIDConnectClientException('Unable to determine state'); + } - // Save the id token - $this->idToken = $id_token; + // Cleanup state + $this->unsetState(); - // If this is a valid claim - if ($this->verifyJWTClaims($claims, $accessToken)) { + $claims = $this->decodeJWT($id_token, 1); - // Clean up the session a little - $this->unsetNonce(); + // Verify the signature + $this->verifySignatures($id_token); - // Save the verified claims - $this->verifiedClaims = $claims; + // Save the id token + $this->idToken = $id_token; - // Save the access token - if ($accessToken) { - $this->accessToken = $accessToken; - } + // If this is a valid claim + if (!$this->verifyJWTClaims($claims, $accessToken)) { + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + } - // Success! - return true; + // Clean up the session a little + $this->unsetNonce(); - } + // Save the verified claims + $this->verifiedClaims = $claims; - throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + // Save the access token + if ($accessToken) { + $this->accessToken = $accessToken; } - $this->requestAuthorization(); - return false; + // Success! + return true; } /** @@ -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; } /** 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();