|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
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>
|
|
|
|
*/
|
|
|
|
static function() use ($url) {
|
|
|
|
$response = (new \GuzzleHttp\Client())->get($url.'/.well-known/openid-configuration');
|
|
|
|
return json_decode((string)$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>
|
|
|
|
*/
|
|
|
|
static 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((string)$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((string)$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((string)$token->getClient()->get($this->getEndpointUrl('userinfo'))->getBody(), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// todo: introspect, etc
|
|
|
|
}
|