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.

332 lines
8.2 KiB

<?php
namespace authkit2;
/**
* Helper class to abstract away the framework authkit2 is running on as well
* as provide simple helper methods for using it outside of a framework.
*
* @method static mixed cache(string $key, callable $generator)
* @method static mixed cache_get(string $key, mixed $default = null)
* @method static void cache_set(string $key, mixed $value)
* @method static mixed session_get(string $key)
* @method static void session_set(string $key, mixed $value)
* @method static void configure(string $client_id, string $client_secret, string $endpoint)
* @method static Oidc\Client get_client()
* @method static Oidc\Token get_token(string $access_token, ?string $refresh_token = null)
* @method static Oidc\Token refresh_token(Oidc\Token $token)
*/
class Authkit2
{
/**
* All data we shove into the session/cache will have its key prefixed
* with this value.
* @var string
*/
private const LIB_PREFIX = 'authkit2.';
/**
* Functions this class provides
* @array<string,callable>
*/
protected $callbacks = [];
/**
* Oidc client with the application credentials
* @var \authkit2\Oidc\Client
*/
protected $client;
/**
* Try and detect if we recognize the environment the library is running
* in and adjust our implementations accordingly.
*
* Basically, if we see the LARAVEL_START constant we assume Laravel and
* use Laravel facades, otherwise we use native php implementations.
*
*/
protected function __construct()
{
$callbacks = $this->getCommonCallbacks();
if (defined('LARAVEL_START'))
{
$callbacks = array_merge($callbacks, $this->getLaravelCallbacks());
}
else
{
$callbacks = array_merge($callbacks, $this->getNativeCallbacks());
}
$this->callbacks = $callbacks;
}
/**
* Retrieve the instance of Authkit2 class
*
* @return Authkit2
*/
public static function get(): Authkit2
{
static $authkit2;
if (!isset($authkit2))
{
$authkit2 = new Authkit2();
}
return $authkit2;
}
/**
* Override any of the function implementations
*
* Name is the same as the callable function name, e.g.,
* Authkit2::cache_set() can be overriden with Authkit2->cache_set = function(...) {}
*
* @param string $name
* @param callable $value
* @return void
*/
public function __set(string $name, $value): void
{
if (!array_key_exists($name, $this->callbacks))
{
trigger_error('Undefined property: '.__CLASS__.'::$'.$name, E_USER_WARNING);
return;
}
if (!is_callable($value))
{
throw new \Exception('Authkit2::'.$name.' value must be callable');
}
$this->callbacks[$name] = $value;
}
/**
* Call any of the provided methods
*
* @param string $name
* @param mixed[] $arguments
* @return mixed
*/
public static function __callStatic(string $name, array $arguments)
{
$authkit2 = static::get();
if (!isset($authkit2->callbacks[$name]))
{
trigger_error('Call to undefined method '.__CLASS__.'::'.$name.'()', E_USER_ERROR);
}
return call_user_func_array($authkit2->callbacks[$name], $arguments);
}
/**
* Helper method for getting cache values, and generating and setting if
* they do not exist.
*
* @param string $key cache key
* @param callable $generator method that returns the value if we do not have it cached
* @return mixed
*/
protected function cache_helper(string $key, callable $generator)
{
$value = static::cache_get($key, null);
if (!isset($value))
{
$value = $generator();
static::cache_set($key, $value);
}
return $value;
}
/**
* Initialize common library functions that don't require an environment-specific
* implementation
*
* @return array<string,callable>
*/
protected function getCommonCallbacks(): array
{
return [
'cache' => [$this, 'cache_helper'],
'configure' => [$this, 'ak2_configure'],
'get_client' => [$this, 'ak2_get_client'],
'get_token' => [$this, 'ak2_get_token'],
'refresh_token' => [$this, 'ak2_refresh_token']
];
}
/**
* Initialize the class by binding all the PHP native implementations of
* functions
*
* @return array<string,callable>
*/
protected function getNativeCallbacks(): array
{
return [
'session_get' => [$this, 'native_session_get'],
'session_set' => [$this, 'native_session_set'],
'cache_get' => [$this, 'native_cache_get'],
'cache_set' => [$this, 'native_cache_set']
];
}
/**
* Initialize the class by binding Laravel adapters as the implementation
* of all functions
*
* @return array<string,callable>
*/
protected function getLaravelCallbacks(): array
{
return [
'session_get' =>
/**
* Fetch a variable from the session
* @param string $key
* @return mixed
*/
function(string $key) { return \Session::get($key); },
'session_set' =>
/**
* Set a variable in the session
* @param string $key
* @param mixed $value
* @return void
*/
function(string $key, $value): void { \Session::put($key, $value); },
'cache_get' =>
/**
* Fetch a value from cache
* @param string $key
* @return mixed
*/
function(string $key) { return \Cache::get($key); },
'cache_set' =>
/**
* Set a value in cache
* @param string $key
* @param mixed $value
* @return void
*/
function(string $key, $value): void { \Cache::set($key, $value); }
];
}
/**
* Retrieve a property out of the $_SESSION variable; null if the
* property doesn't exist.
*
* @param string $key
* @return mixed
*/
protected function native_session_get(string $key)
{
$this->native_session_check();
return $_SESSION[static::LIB_PREFIX.$key] ?? null;
}
/**
* Set a value in the $_SESSION variable
*
* @param string $key
* @param mixed $value
* @return void
*/
protected function native_session_set(string $key, $value): void
{
$this->native_session_check();
$_SESSION[static::LIB_PREFIX.$key] = $value;
}
/**
* Check whether a PHP session exists, and if not try and start one
*
* @internal
* @return void
*/
protected function native_session_check(): void
{
if (session_status() == \PHP_SESSION_NONE)
session_start();
else if (session_status() == \PHP_SESSION_DISABLED)
throw new \Exception("Authkit2 requires PHP sessions are enabled");
}
/**
* Dummy cache implementation to avoid errors; always returns default
*
* @todo Check if apcu is available and use if so? Fall back to temp files?
* @param string $key cache key to retrieve
* @param mixed $default value to return if the specified key is not found
* @return mixed
*/
protected function native_cache_get(string $key, $default = null)
{
return $default;
}
/**
* Dummy cache implementation
*
* @param string $key cache key to set
* @param mixed $value value to cache
* @return void
*/
protected function native_cache_set(string $key, $value): void
{
}
/**
* Configure the authkit2 library
*
* @param string $client_id
* @param string $client_secret
* @param string $endpoint
* @return void
*/
protected function ak2_configure(string $client_id, string $client_secret, string $endpoint): void
{
$this->client = new \authkit2\Oidc\Client($endpoint, $client_id, $client_secret);
}
/**
* Fetch a OIDC client authenticated as this application
*
* @return Oidc\Client
*/
protected function ak2_get_client(): Oidc\Client
{
return $this->client;
}
/**
* Given the essential values from a token (access token, refresh token),
* convert that into a Token object that can be used to make and authenticate
* requests.
*
* The refresh token is not strictly required, however if the token is
* expired then requests will simply fail. This use case is intended for
* authenticating requests using tokens other applications have sent to us.
*
* @param string $access_token
* @param ?string $refresh_token
* @return Oidc\Token
*/
protected function ak2_get_token(string $access_token, ?string $refresh_token = null): Oidc\Token
{
return Oidc\Token::fromString($this->client, $access_token, $refresh_token);
}
/**
* Refresh a token object -- generate a new access token from its
* refresh_token.
*
* @param Oidc\Token $token
* @return Oidc\Token a newly generated token
*/
protected function ak2_refresh_token(Oidc\Token $token): Oidc\Token
{
return $this->client->refreshToken($token);
}
}