You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
6.5 KiB

<?php
namespace authkit2\Oidc;
use authkit2\Oidc\Client;
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
/**
* A OpenId Connect token, optionally with a refresh token
*/
class Token
{
/**
* OIDC client
* @var Client
*/
protected $client = null;
/**
* OIDC JWT access token
* @var string
*/
protected $access_token;
/**
* OIDC JWT refresh token
* @var ?string
*/
protected $refresh_token;
/**
* Cache of userinfo endpoint response
* @var array<string,scalar>
*/
protected $user_info = null;
/**
* Decoded access token JWT data
* @var array<string,mixed>
*/
protected $access_token_data = null;
/**
* Decoded refresh token JWT data
* @var array<string,mixed>
*/
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<string,mixed> $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<string,mixed>
*/
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<string,mixed>
*/
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<string,scalar>
*/
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'];
}
}