*/ protected $oidc_config; /** * Keys for validating signed JWT tokens. * @var array */ 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 */ 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 */ 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 */ 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 */ 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 $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 $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 */ public function getTokenUserInfo(Token $token): array { return json_decode((string)$token->getClient()->get($this->getEndpointUrl('userinfo'))->getBody(), true); } // todo: introspect, etc }