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.
307 lines
6.3 KiB
307 lines
6.3 KiB
<?php
|
|
|
|
namespace authkit2\Oidc;
|
|
use authkit2\Authkit2;
|
|
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 $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) use ($state) {
|
|
$client = $this->client;
|
|
$refresh_callback = $this->refresh_callback;
|
|
|
|
// Refresh the token
|
|
$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->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'];
|
|
}
|
|
|
|
}
|
|
|