|
|
@ -21,44 +21,53 @@ class Client |
|
|
|
*/ |
|
|
|
protected $client; |
|
|
|
|
|
|
|
/** |
|
|
|
* OAuth client id |
|
|
|
* @var string |
|
|
|
*/ |
|
|
|
protected $client_id; |
|
|
|
|
|
|
|
/** |
|
|
|
* Base url of the OIDC realm |
|
|
|
* @var string |
|
|
|
*/ |
|
|
|
static $oidc_url; |
|
|
|
protected $oidc_url; |
|
|
|
|
|
|
|
/** |
|
|
|
* OIDC config fetched from the server or restored from cache |
|
|
|
* @var array<string,mixed> |
|
|
|
*/ |
|
|
|
static $oidc_config; |
|
|
|
protected $oidc_config; |
|
|
|
|
|
|
|
/** |
|
|
|
* Create a new OIDC client using the passed authentication provider |
|
|
|
* 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 Authentication\Authentication $auth |
|
|
|
* @param string $url |
|
|
|
* @param string $client_id |
|
|
|
* @param string $client_secret |
|
|
|
*/ |
|
|
|
public function __construct(Authentication\Authentication $auth) |
|
|
|
public function __construct(string $url, string $client_id, string $client_secret) |
|
|
|
{ |
|
|
|
$this->auth = $auth; |
|
|
|
$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 |
|
|
|
* |
|
|
|
* Constructed on first use |
|
|
|
* |
|
|
|
* @return \GuzzleHttp\Client |
|
|
|
*/ |
|
|
|
public function getClient(): \GuzzleHttp\Client |
|
|
|
{ |
|
|
|
if (!isset($this->client)) |
|
|
|
{ |
|
|
|
$stack = new \GuzzleHttp\HandlerStack(); |
|
|
|
$stack->setHandler(new \GuzzleHttp\Handler\CurlHandler()); |
|
|
|
$stack->push($this->auth->getMiddleware()); |
|
|
|
$this->client = new \GuzzleHttp\Client(['handler' => $stack]); |
|
|
|
} |
|
|
|
return $this->client; |
|
|
|
} |
|
|
|
|
|
|
@ -67,101 +76,79 @@ class Client |
|
|
|
* |
|
|
|
* @return ?string |
|
|
|
*/ |
|
|
|
public static function getUrl(): ?string |
|
|
|
public function getUrl(): string |
|
|
|
{ |
|
|
|
return static::$oidc_url; |
|
|
|
return $this->oidc_url; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Configure the library with a OpenId Connect realm url |
|
|
|
* Get the OpenId Connect configuration |
|
|
|
* |
|
|
|
* @param string $url |
|
|
|
* @return void |
|
|
|
* @return array |
|
|
|
*/ |
|
|
|
public static function setUrl(string $url): void |
|
|
|
public function getConfiguration(): array |
|
|
|
{ |
|
|
|
static::$oidc_url = $url; |
|
|
|
} |
|
|
|
if (!isset($this->oidc_config)) |
|
|
|
{ |
|
|
|
$url = $this->oidc_url; |
|
|
|
$this->oidc_config = Authkit2::cache('oidc.config.'.md5($this->oidc_url), function() use ($url) { |
|
|
|
$response = (new \GuzzleHttp\Client())->get($url.'/.well-known/openid-configuration'); |
|
|
|
return json_decode($response->getBody(), true); |
|
|
|
}); |
|
|
|
|
|
|
|
/** |
|
|
|
* Set the OpenId Connect configuration |
|
|
|
* |
|
|
|
* This is provided to allow external caching rather than having us refetch |
|
|
|
* the configuration on every invocation of the project. |
|
|
|
* |
|
|
|
* @param array $config config as retrieved from getOidcConfig() |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
public static function setOidcConfig(array $config): void |
|
|
|
{ |
|
|
|
static::$oidc_config = $config; |
|
|
|
} |
|
|
|
return $this->oidc_config; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Get the OpenId Connect configuration |
|
|
|
* Get the web key set for verifying JWTs |
|
|
|
* |
|
|
|
* If not restored/set via setOidcConfig, this will be fetched from the OIDC |
|
|
|
* realm on first use |
|
|
|
* |
|
|
|
* @return array |
|
|
|
* @return array<string,array> |
|
|
|
*/ |
|
|
|
public static function getOidcConfig(): array |
|
|
|
public function getJsonWebKeySet(): array |
|
|
|
{ |
|
|
|
if (!isset(static::$oidc_config)) |
|
|
|
if (!isset($this->oidc_jwks)) |
|
|
|
{ |
|
|
|
static::$oidc_config = Authkit2::cache('oidc.config', function() { |
|
|
|
$response = (new \GuzzleHttp\Client())->get(static::$oidc_url.'/.well-known/openid-configuration'); |
|
|
|
return json_decode($response->getBody(), true); |
|
|
|
$client = $this; |
|
|
|
$this->oidc_jwks = Authkit2::cache('oidc.config.'.md5($this->oidc_url).'.jwks', function() use ($client) { |
|
|
|
$response = $client->get($client->getConfiguration()['jwks_uri']); |
|
|
|
return json_decode(json_encode($response), true); |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
return static::$oidc_config; |
|
|
|
return $this->oidc_jwks; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Fetch a specific OpenId Connect endpoint from the configuration |
|
|
|
* Get the signing algorithms for signing JWTs |
|
|
|
* |
|
|
|
* @param string $endpoint_name |
|
|
|
* @return string |
|
|
|
* @return string[] |
|
|
|
*/ |
|
|
|
public function getEndpointUrl(string $endpoint_name): string |
|
|
|
public function getTokenSigningAlgorithms(): array |
|
|
|
{ |
|
|
|
return static::getOidcConfig()[$endpoint_name.'_endpoint']; |
|
|
|
return $this->getConfiguration()['id_token_signing_alg_values_supported']; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Determine the final URL to make a request to given an arbitrarily-defined |
|
|
|
* 'endpoint'. |
|
|
|
* |
|
|
|
* If the passed in value is a valid URI, it will be used directly. Otherwise, |
|
|
|
* we will attempt to find a configured endpoint with a name matching the |
|
|
|
* passed in value in the OIDC config. |
|
|
|
* Fetch a specific OpenId Connect endpoint from the configuration |
|
|
|
* |
|
|
|
* @param string $endpoint url or endpoint name |
|
|
|
* @return string url to call |
|
|
|
* @param string $endpoint_name |
|
|
|
* @return string |
|
|
|
*/ |
|
|
|
protected function parseEndpoint(string $endpoint): string |
|
|
|
public function getEndpointUrl(string $endpoint_name): string |
|
|
|
{ |
|
|
|
if (filter_var($endpoint, \FILTER_VALIDATE_URL)) |
|
|
|
{ |
|
|
|
return $endpoint; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
return $this->getEndpointUrl($endpoint); |
|
|
|
} |
|
|
|
return $this->getConfiguration()[$endpoint_name.'_endpoint']; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Make a HTTP get request to a OIDC endpoint or other URL |
|
|
|
* |
|
|
|
* @param string $endpoint url or endpoint name |
|
|
|
* @param string $url |
|
|
|
* @param array<string,scalar> $params query string parameters |
|
|
|
* @return object json decoded response |
|
|
|
*/ |
|
|
|
public function get(string $endpoint, array $params = []): object |
|
|
|
protected function get(string $url, array $params = []): object |
|
|
|
{ |
|
|
|
$response = $this->getClient()->get($this->parseEndpoint($endpoint), [ |
|
|
|
$response = $this->getClient()->get($url, [ |
|
|
|
'query' => $params |
|
|
|
]); |
|
|
|
return json_decode($response->getBody()); |
|
|
@ -173,15 +160,94 @@ class Client |
|
|
|
* If form parameters are provided the request is sent as |
|
|
|
* application/x-www-form-urlencoded |
|
|
|
* |
|
|
|
* @param string $endpoint url or endpoint name |
|
|
|
* @param string $url |
|
|
|
* @param array<string,scalar> $params form fields |
|
|
|
* @return object json decoded response |
|
|
|
*/ |
|
|
|
public function post(string $endpoint, array $params = []): object |
|
|
|
protected function post(string $url, array $params = []): object |
|
|
|
{ |
|
|
|
$response = $this->getClient()->post($this->parseEndpoint($endpoint), [ |
|
|
|
$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); |
|
|
|
} |
|
|
|
|
|
|
|
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 |
|
|
|
]); |
|
|
|
} |
|
|
|
|
|
|
|
public function createLogoutUrl(string $redirect_uri): string |
|
|
|
{ |
|
|
|
return $this->getEndpointUrl('end_session').'?'.http_build_query([ |
|
|
|
'redirect_uri' => $redirect_uri |
|
|
|
]); |
|
|
|
} |
|
|
|
|
|
|
|
public function refreshToken(Token $token): Token |
|
|
|
{ |
|
|
|
return $this->createTokenFromRefreshToken($token->getRefreshToken()); |
|
|
|
} |
|
|
|
|
|
|
|
public function getTokenUserInfo(Token $token): array |
|
|
|
{ |
|
|
|
return json_decode($token->getClient()->get($this->getEndpointUrl('userinfo'))->getBody(), true); |
|
|
|
} |
|
|
|
|
|
|
|
// todo: introspect, etc |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|