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.

293 lines
7.0 KiB

<?php
namespace authkit2\Oidc;
use authkit2\Authkit2;
/**
* OpenId Connect HTTP Client Library
*/
class Client
{
/**
* Authenticator used to authenticate requests we're making
* @var Authentication\Authentication
*/
protected $auth;
/**
* Http client we've initialized with our authentication middleware in
* place
* @var \GuzzleHttp\Client
*/
protected $client;
/**
* OAuth client id
* @var string
*/
protected $client_id;
/**
* Base url of the OIDC realm
* @var string
*/
protected $oidc_url;
/**
* OIDC config fetched from the server or restored from cache
* @var ?array<string,mixed>
*/
protected $oidc_config;
/**
* Keys for validating signed JWT tokens
* @var array<string,mixed>
*/
protected $oidc_jwks;
/**
* Create a new OIDC client using the passed in client credentials
*
* @param string $url
* @param string $client_id
* @param string $client_secret
*/
public function __construct(string $url, string $client_id, string $client_secret)
{
$this->auth = new Authentication\ClientAuthentication($client_id, $client_secret);
$this->client = $this->auth->getClient();
$this->oidc_url = $url;
$this->oidc_config = null;
$this->client_id = $client_id;
}
/**
* Retrieve a HTTP client containing our authentication middleware
*
* @return \GuzzleHttp\Client
*/
public function getClient(): \GuzzleHttp\Client
{
return $this->client;
}
/**
* Retrieve the configured OpenId Connect realm url; null if never set
*
* @return ?string
*/
public function getUrl(): ?string
{
return $this->oidc_url;
}
/**
* Get the OpenId Connect configuration
*
* @return array<string,string|array|bool>
*/
public function getConfiguration(): array
{
if (!isset($this->oidc_config))
{
$url = $this->oidc_url;
$this->oidc_config = Authkit2::cache('oidc.config.'.md5($this->oidc_url),
/**
* @return array<string,string|array|bool>
*/
function() use ($url) {
$response = (new \GuzzleHttp\Client())->get($url.'/.well-known/openid-configuration');
return json_decode($response->getBody(), true);
}
);
}
return $this->oidc_config;
}
/**
* Get the web key set for verifying JWTs
*
* @return array<string,array>
*/
public function getJsonWebKeySet(): array
{
if (!isset($this->oidc_jwks))
{
$client = $this;
$this->oidc_jwks = Authkit2::cache('oidc.config.'.md5($this->oidc_url).'.jwks',
/**
* @return array<string,array>
*/
function() use ($client) {
$response = $client->get($client->getConfiguration()['jwks_uri']);
return json_decode(json_encode($response), true);
}
);
}
return $this->oidc_jwks;
}
/**
* Get the signing algorithms for signing JWTs
*
* @return string[]
*/
public function getTokenSigningAlgorithms(): array
{
return $this->getConfiguration()['id_token_signing_alg_values_supported'];
}
/**
* Fetch a specific OpenId Connect endpoint from the configuration
*
* @param string $endpoint_name
* @return string
*/
public function getEndpointUrl(string $endpoint_name): string
{
return $this->getConfiguration()[$endpoint_name.'_endpoint'];
}
/**
* Make a HTTP get request to a OIDC endpoint or other URL
*
* @param string $url
* @param array<string,scalar> $params query string parameters
* @return object json decoded response
*/
protected function get(string $url, array $params = []): object
{
$response = $this->getClient()->get($url, [
'query' => $params
]);
return json_decode($response->getBody());
}
/**
* Make a HTTP post request to a OIDC endpoint or other URL
*
* If form parameters are provided the request is sent as
* application/x-www-form-urlencoded
*
* @param string $url
* @param array<string,scalar> $params form fields
* @return object json decoded response
*/
protected function post(string $url, array $params = []): object
{
$response = $this->getClient()->post($url, [
'form_params' => $params
]);
return json_decode($response->getBody());
}
/**
* Create a 'service account' token tied to this client's id
*
* @return Token
*/
public function createTokenFromClient(): Token
{
$response = $this->post($this->getEndpointUrl('token'), [
'grant_type' => 'client_credentials'
]);
return Token::fromResponse($this, $response);
}
/**
* Convert a returned authorization code from the three legged flow
* into a token
*
* @param string $code
* @param string $redirect_uri
* @return Token
*/
public function createTokenFromAuthorizationCode(string $code, string $redirect_uri): Token
{
$response = $this->post($this->getEndpointUrl('token'), [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $redirect_uri
]);
// todo: check for response->error, response->error_description
return Token::fromResponse($this, $response);
}
/**
* Create a new access token from a refresh token
*
* @param string $refresh_token
* @return Token
*/
public function createTokenFromRefreshToken(string $refresh_token): Token
{
$response = $this->post($this->getEndpointUrl('token'), [
'grant_type' => 'refresh_token',
'refresh_token' => $refresh_token
]);
return Token::fromResponse($this, $response);
}
/**
* Generate the URL to redirect to in order to initiate the three-legged
* oauth flow
*
* @param string $redirect_uri url to redirect the user to after authentication
* @param string[] $scopes scopes to request from the openid provider
* @param string $state nonce
* @return string fully formed url
*/
public function createAuthorizationRedirectUrl(string $redirect_uri, array $scopes, string $state): string
{
return $this->getEndpointUrl('authorization').'?'.http_build_query([
'client_id' => $this->client_id,
'redirect_uri' => $redirect_uri,
'scope' => implode(',', $scopes),
'response_type' => 'code',
'state' => $state
]);
}
/**
* Generate the URL to redirect to in order to initiate a signout from the
* OIDC provider
*
* @param string $redirect_uri url to redirect the user to after logout
* @return string fully formed url
*/
public function createLogoutUrl(string $redirect_uri): string
{
return $this->getEndpointUrl('end_session').'?'.http_build_query([
'redirect_uri' => $redirect_uri
]);
}
/**
* Refresh a token using a refresh token
*
* @param Token $token expired token that includes a refresh token
* @return Token newly generated token
*/
public function refreshToken(Token $token): Token
{
$refresh_token = $token->getRefreshToken();
if (!isset($refresh_token))
throw new \Exception("Cannot refresh token initialized without refresh token");
return $this->createTokenFromRefreshToken($refresh_token);
}
/**
* Fetch the available information on the user from the OIDC provider
*
* @param Token $token token representing the user
* @return array<string,mixed>
*/
public function getTokenUserInfo(Token $token): array
{
return json_decode($token->getClient()->get($this->getEndpointUrl('userinfo'))->getBody(), true);
}
// todo: introspect, etc
}