*/ 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 */ 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 */ 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 */ 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($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($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($token->getClient()->get($this->getEndpointUrl('userinfo'))->getBody(), true); } // todo: introspect, etc }