Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split up authenticate() into individual actions to allow per-action use #436

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
186 changes: 103 additions & 83 deletions src/OpenIDConnectClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -293,131 +293,150 @@ 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
*/
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;
}

/**
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions tests/OpenIDConnectClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down