*/ protected $user_info = null; /** * Decoded access token JWT data. * @var array */ protected $access_token_data = null; /** * Decoded refresh token JWT data. * @var array */ protected $refresh_token_data = null; /** * Callback to be notified when this token is refreshed. * @var callable */ protected $refresh_callback; /** * Initialize token with the from*() static methods. */ protected function __construct() { } /** * Create a token given a access_token and optionally refresh_token, passed * as a string. * * @param Client $client * @param string $access_token * @param ?string $refresh_token * @return Token */ public static function fromString(Client $client, string $access_token, ?string $refresh_token = null): Token { $token = new Token(); $token->client = $client; $token->access_token = $access_token; $token->refresh_token = $refresh_token; return $token; } /** * Get a HTTP client that's authenticated with this token's credentials. * * @param array $options * @return \GuzzleHttp\Client */ public function getClient(array $options = []): \GuzzleHttp\Client { // Create the token auth implementation; this holds a callback to the token // to ask it to refresh itself $state = new \stdClass(); $state->refresher = function(Token $token) use ($state): Token { $client = $this->client; $refresh_callback = $this->refresh_callback; // Refresh the token if (!isset($this->refresh_token)) { throw new \Exception('Token expired'); } $new_token = $client->createTokenFromRefreshToken($this->refresh_token); // Rebind this callback to the new token $state->refresher->bindTo($new_token); // Call the refresh callback if (isset($refresh_callback)) { // Copy over the token-level refresh callback $new_token->setRefreshCallback($refresh_callback); $refresh_callback($new_token); } return $new_token; }; $auth = new Authentication\TokenAuthentication($this, $state->refresher); return $auth->getClient($options); } /** * Callback to notify when this token is refreshed. * * @param callable $callback * @return void */ public function setRefreshCallback(callable $callback): void { $this->refresh_callback = $callback; } /** * Create a token from a OIDC response from the token endpoint. * * @param Client $client * @param object $response * @return Token */ public static function fromResponse(Client $client, object $response): Token { $token = new Token(); $token->client = $client; $token->access_token = $response->access_token ?? null; $token->refresh_token = $response->refresh_token ?? null; return $token; } /** * Fetch the raw decoded data out of our JWT access token. * * @return array */ public function getAccessTokenData(): array { if (!isset($this->access_token_data)) { $this->access_token_data = json_decode(json_encode($this->decode($this->access_token)), true); } return $this->access_token_data; } /** * Fetch the raw decoded data out of our JWT refresh token. * * @return array */ public function getRefreshTokenData(): array { if (!isset($this->refresh_token_data)) { if (!isset($this->refresh_token)) { throw new \UnexpectedValueException('Refresh token not set!'); } $this->refresh_token_data = json_decode(json_encode($this->decode($this->refresh_token)), true); } return $this->refresh_token_data; } /** * Decode a token as a JWT token. * * @param string $token * @return object */ protected function decode(string $token): object { return JWT::decode($token, JWK::parseKeySet($this->client->getJsonWebKeySet()), $this->client->getTokenSigningAlgorithms()); } /** * Check whether the token is valid -- that is, whether you could actually * use it for things. * * To be considered valid, the access token must be parseable and signed by * the correct keys but _may_ be expired. The refresh token must be parseable, * signed by the correct key, and may not be expired. * * @return bool */ public function isValid(): bool { try { $this->getAccessTokenData(); return true; } catch (\UnexpectedValueException $ex) { if ($ex instanceof \Firebase\JWT\ExpiredException) { try { $this->getRefreshTokenData(); return true; } catch (\UnexpectedValueException $ex) { return false; } } return false; } } /** * Check whether the access token is expired. * * As long as the refresh token is valid, this is recoverly by calling * passing this token to refresh on the client. * * @return bool */ public function isExpired(): bool { try { $token_data = $this->getAccessTokenData(); if ($token_data['exp'] <= time()) { return true; } return false; } catch (\Firebase\JWT\ExpiredException $ex) { return true; } } /** * Check whether this token needs a refresh to be used. * * @return bool */ public function needsRefresh(): bool { return $this->isValid() && $this->isExpired() && isset($this->refresh_token); } /** * Fetch the underlying access token this token represents. * * @return string */ public function getAccessToken(): string { return $this->access_token; } /** * Fetch the user's refresh token. * * @return ?string */ public function getRefreshToken(): ?string { return $this->refresh_token; } /** * Fetch the user info associated with this token from the OIDC * provider. * * @return array */ public function getUserInfo(): array { return $this->client->getTokenUserInfo($this); } /** * Fetch the roles encoded in this token. * * @return string[] */ public function getRoles(): array { return $this->getAccessTokenData()['realm_access']['roles']; } /** * Fetch the uuid encoded in this token. * * @return string */ public function getUserId(): string { return 'crn:user:'.$this->getAccessTokenData()['sub']; } }