You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
3.4 KiB

<?php
namespace authkit2\Oidc\Flows;
use authkit2\Authkit2;
use authkit2\Oidc\Client;
use authkit2\Oidc\Authentication\ClientAuthentication;
use authkit2\Oidc\Token;
/**
* Oauth three legged auth flow for authenticating users
*/
class UserFlow
{
/**
* OIDC client authenticating as the client
* @var Client
*/
protected $client;
/**
* oauth client id
* @var string
*/
protected $client_id;
/**
* oauth client secret
* @var string
*/
protected $client_secret;
/**
* Initialize a new user login flow
*
* @param string $client_id oauth client id
* @param string $client_secret oauth client secret
*/
public function __construct(string $client_id, string $client_secret)
{
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->client = new Client(new ClientAuthentication($client_id, $client_secret));
}
/**
* Get the URL the user should be redirected to to begin the authorization process
*
* @param string $redirect_uri url the oidc endpoint should redirect the user back to; generally must be authorized in the provider
* @param string[] $scopes the scopes to request in the token
* @return string url to redirect the user to
*/
public function getRedirectUrl(string $redirect_uri, array $scopes = ['email']): string
{
// Generate nonce
// We use 30 because that should generate an even number of base64 characters
// and not require padding. (6 bits / char)
$state = base64_encode(random_bytes(30));
// Keep a list of all valid states we've generated
$states = Authkit2::session_get('userflow.state') ?? [];
array_push($states, $state);
Authkit2::session_set('userflow.state', $states);
return $this->client->getEndpointUrl('authorization').'?'.http_build_query([
'client_id' => $this->client_id,
'redirect_uri' => $redirect_uri,
'scope' => implode(',', $scopes),
'response_type' => 'code',
'state' => $state
]);
}
/**
* Validate that the provided state string is one we have previously generated
*
* @throws \Exception if the state is unrecognized or invalid
* @return void
*/
public function validateState(string $state): void
{
$states = Authkit2::session_get('userflow.state') ?? [];
for ($i=0; $i<sizeof($states); $i++)
{
if ($states[$i] == $state)
{
$states = array_splice($states, $i, 1);
Authkit2::session_set('userflow.state', $states);
return;
}
}
throw new \Exception("Invalid auth nonce");
}
/**
* After the user is redirected back with a authorization code, exchange it
* for an access token
*
* @param string $redirect_uri url the oidc endpoint redirects back to; must match one given in call to getRedirectUrl
* @param string $code code returned by the authorization flow
* @return Token
*/
public function exchangeCodeForToken(string $redirect_uri, string $code): Token
{
$response = $this->client->post('token', [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $redirect_uri
]);
return Token::fromResponse($response);
}
/**
* If we want to log out of the SSO service and all apps, the URL to hit to
* sign out everywhere.
*
* @param string $redirect_uri url to redirect back to after logout completes
* @return string
*/
public function getLogoutUrl(string $redirect_uri): string
{
return $this->client->getEndpointUrl('end_session').'?'.http_build_query([
'redirect_uri' => $redirect_uri
]);
}
}