Browse Source

Finished refactor/reorg around authkit2 lib as entrypoint

master
Adam Pippin 3 years ago
parent
commit
b72495523f
  1. 71
      src/Authkit2.php
  2. 2
      src/Http/Controllers/AuthenticationController.php
  3. 35
      src/Observers/UserObserver.php
  4. 8
      src/Oidc/Authentication/Authentication.php
  5. 19
      src/Oidc/Authentication/TokenAuthentication.php
  6. 216
      src/Oidc/Client.php
  7. 10
      src/Oidc/Flows/ServiceAccountFlow.php
  8. 46
      src/Oidc/Flows/UserFlow.php
  9. 194
      src/Oidc/Token.php
  10. 4
      src/Providers/Authkit2ServiceProvider.php
  11. 9
      src/Providers/AuthnServiceProvider.php

71
src/Authkit2.php

@ -11,6 +11,10 @@ namespace authkit2;
* @method static void cache_set(string $key, mixed $value) * @method static void cache_set(string $key, mixed $value)
* @method static mixed session_get(string $key) * @method static mixed session_get(string $key)
* @method static void session_set(string $key, mixed $value) * @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 class Authkit2
{ {
@ -23,11 +27,16 @@ class Authkit2
/** /**
* Functions this class provides * Functions this class provides
*
* @array<string,callable> * @array<string,callable>
*/ */
protected $callbacks = []; 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 * Try and detect if we recognize the environment the library is running
* in and adjust our implementations accordingly. * in and adjust our implementations accordingly.
@ -38,14 +47,16 @@ class Authkit2
*/ */
protected function __construct() protected function __construct()
{ {
$callbacks = $this->getCommonCallbacks();
if (defined('LARAVEL_START')) if (defined('LARAVEL_START'))
{ {
$this->initializeLaravel(); $callbacks = array_merge($callbacks, $this->getLaravelCallbacks());
} }
else else
{ {
$this->initializeNative(); $callbacks = array_merge($callbacks, $this->getNativeCallbacks());
} }
$this->callbacks = $callbacks;
} }
/** /**
@ -126,20 +137,36 @@ class Authkit2
return $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 * Initialize the class by binding all the PHP native implementations of
* functions * functions
* *
* @return void * @return array<string,callable>
*/ */
protected function initializeNative(): void protected function getNativeCallbacks(): array
{ {
$this->callbacks = [ return [
'session_get' => [$this, 'native_session_get'], 'session_get' => [$this, 'native_session_get'],
'session_set' => [$this, 'native_session_set'], 'session_set' => [$this, 'native_session_set'],
'cache_get' => [$this, 'native_cache_get'], 'cache_get' => [$this, 'native_cache_get'],
'cache_set' => [$this, 'native_cache_set'], 'cache_set' => [$this, 'native_cache_set']
'cache' => [$this, 'cache_helper']
]; ];
} }
@ -147,16 +174,15 @@ class Authkit2
* Initialize the class by binding Laravel adapters as the implementation * Initialize the class by binding Laravel adapters as the implementation
* of all functions * of all functions
* *
* @return void * @return array<string,callable>
*/ */
protected function initializeLaravel(): void protected function getLaravelCallbacks(): array
{ {
$this->callbacks = [ return [
'session_get' => function(string $key) { return \Session::get($key); }, 'session_get' => function(string $key) { return \Session::get($key); },
'session_set' => function(string $key, $value) { \Session::put($key, $value); }, 'session_set' => function(string $key, $value) { \Session::put($key, $value); },
'cache_get' => function(string $key) { return \Cache::get($key); }, 'cache_get' => function(string $key) { return \Cache::get($key); },
'cache_set' => function(string $key, $value) { \Cache::set($key, $value); }, 'cache_set' => function(string $key, $value) { \Cache::set($key, $value); }
'cache' => [$this, 'cache_helper']
]; ];
} }
@ -224,5 +250,24 @@ class Authkit2
{ {
} }
protected function ak2_configure(string $client_id, string $client_secret, string $endpoint): void
{
$this->client = new \authkit2\Oidc\Client($endpoint, $client_id, $client_secret);
}
protected function ak2_get_client(): Oidc\Client
{
return $this->client;
}
protected function ak2_get_token(string $access_token, ?string $refresh_token = null): Oidc\Token
{
return Oidc\Token::fromString($this->client, $access_token, $refresh_token);
}
protected function ak2_refresh_token(Oidc\Token $token): Token
{
return $this->client->refreshToken($token);
}
} }

2
src/Http/Controllers/AuthenticationController.php

@ -53,7 +53,7 @@ class AuthenticationController extends Controller
// TODO: Check for error response // TODO: Check for error response
// Exchange the code for a token // Exchange the code for a token
$token = $this->user_flow->exchangeCodeForToken(config('authkit.authn.openid.redirect_uri'), $request->code); $token = $this->user_flow->exchangeCodeForToken($request->code, config('authkit.authn.openid.redirect_uri'));
$user_info = $token->getUserInfo(); $user_info = $token->getUserInfo();
// Try and use the token to find the local user // Try and use the token to find the local user

35
src/Observers/UserObserver.php

@ -1,6 +1,7 @@
<?php <?php
namespace authkit2\Observers; namespace authkit2\Observers;
use authkit2\Authkit2;
class UserObserver class UserObserver
{ {
@ -8,8 +9,11 @@ class UserObserver
'saving', 'saved', 'restoring', 'restored', 'replicating', 'saving', 'saved', 'restoring', 'restored', 'replicating',
'deleting', 'deleted', 'forceDeleted',*/ 'deleting', 'deleted', 'forceDeleted',*/
protected static $token_cache = [];
public function retrieved($user) public function retrieved($user)
{ {
// Grab the token and refresh off of the user
if ($user instanceof \authkit2\Models\IAuthkitUser) if ($user instanceof \authkit2\Models\IAuthkitUser)
{ {
$token = $user->{$user->getAccessTokenName()}; $token = $user->{$user->getAccessTokenName()};
@ -21,14 +25,37 @@ class UserObserver
$refresh = $user->authkit_refresh_token; $refresh = $user->authkit_refresh_token;
} }
$user->authkit = \authkit2\Oidc\Token::fromString($token, $refresh); // Create a token object
// TODO: If access_token is expired, try refresh $user->authkit = Authkit2::get_token($token, $refresh);
// If refresh_token is expired, ?!! // Set a refresh callback on the token -- when it's been refreshed,
// \Illuminate\Auth\Access\UnauthorizedException // then set the new tokens on the user and save it
$user->authkit->setRefreshCallback(function($token) use ($user) {
if ($user instanceof \authkit2\Models\IAuthkitUser)
{
$user->{$user->getAccessTokenName()} = $token->getAccessToken();
$user->{$user->getRefreshTokenName()} = $token->getRefreshToken();
}
else
{
$user->authkit_access_token = $token->getAccessToken();
$user->authkit_refresh_token = $token->getRefreshToken();
}
$user->save();
});
} }
public function saving($user) public function saving($user)
{ {
if (isset($user->authkit))
static::$token_cache[$user->authkit->getAccessToken()] = $user->authkit;
unset($user->authkit); unset($user->authkit);
} }
public function saved($user)
{
$access_token = ($user instanceof \authkit2\Models\IAuthkitUser) ? $user->{$user->getAccessTokenName()} : $user->authkit_access_token;
if (isset(static::$token_cache[$access_token]))
$user->authkit = static::$token_cache[$access_token];
}
} }

8
src/Oidc/Authentication/Authentication.php

@ -33,4 +33,12 @@ abstract class Authentication
}; };
}; };
} }
public function getClient(array $options = []): \GuzzleHttp\Client
{
$stack = new \GuzzleHttp\HandlerStack();
$stack->setHandler(new \GuzzleHttp\Handler\CurlHandler());
$stack->push($this->getMiddleware());
return new \GuzzleHttp\Client(array_merge($options, ['handler' => $stack]));
}
} }

19
src/Oidc/Authentication/TokenAuthentication.php

@ -15,14 +15,26 @@ class TokenAuthentication extends Authentication
*/ */
protected $token; protected $token;
/**
* Who to call if the token is expired
* @var callable
*/
protected $refresh_callback;
/** /**
* Create a new token authentication provider * Create a new token authentication provider
* *
* @param Token $token token to authenticate requests with * @param Token $token token to authenticate requests with
*/ */
public function __construct(Token $token) public function __construct(Token $token, callable $refreshCallback = null)
{ {
$this->token = $token; $this->token = $token;
$this->refresh_callback = $refreshCallback;
}
public function setRefreshCallback(callable $refreshCallback): void
{
$this->refresh_callback = $refreshCallback;
} }
/** /**
@ -35,6 +47,11 @@ class TokenAuthentication extends Authentication
*/ */
public function authenticate(\GuzzleHttp\Psr7\Request $request): \GuzzleHttp\Psr7\Request public function authenticate(\GuzzleHttp\Psr7\Request $request): \GuzzleHttp\Psr7\Request
{ {
if ($this->token->isExpired() && isset($this->refresh_callback))
{
$callback = $this->refresh_callback;
$this->token = $callback($this);
}
return $request->withHeader('Authorization', 'Bearer '.$this->token->getAccessToken()); return $request->withHeader('Authorization', 'Bearer '.$this->token->getAccessToken());
} }
} }

216
src/Oidc/Client.php

@ -21,44 +21,53 @@ class Client
*/ */
protected $client; protected $client;
/**
* OAuth client id
* @var string
*/
protected $client_id;
/** /**
* Base url of the OIDC realm * Base url of the OIDC realm
* @var string * @var string
*/ */
static $oidc_url; protected $oidc_url;
/** /**
* OIDC config fetched from the server or restored from cache * OIDC config fetched from the server or restored from cache
* @var array<string,mixed> * @var array<string,mixed>
*/ */
static $oidc_config; protected $oidc_config;
/** /**
* Create a new OIDC client using the passed authentication provider * Keys for validating signed JWT tokens
* @var array<string,mixed>
*/
protected $oidc_jwks;
/**
* Create a new OIDC client using the passed in client credentials
* *
* @param Authentication\Authentication $auth * @param string $url
* @param string $client_id
* @param string $client_secret
*/ */
public function __construct(Authentication\Authentication $auth) public function __construct(string $url, string $client_id, string $client_secret)
{ {
$this->auth = $auth; $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 * Retrieve a HTTP client containing our authentication middleware
* *
* Constructed on first use
*
* @return \GuzzleHttp\Client * @return \GuzzleHttp\Client
*/ */
public function getClient(): \GuzzleHttp\Client public function getClient(): \GuzzleHttp\Client
{ {
if (!isset($this->client))
{
$stack = new \GuzzleHttp\HandlerStack();
$stack->setHandler(new \GuzzleHttp\Handler\CurlHandler());
$stack->push($this->auth->getMiddleware());
$this->client = new \GuzzleHttp\Client(['handler' => $stack]);
}
return $this->client; return $this->client;
} }
@ -67,101 +76,79 @@ class Client
* *
* @return ?string * @return ?string
*/ */
public static function getUrl(): ?string public function getUrl(): string
{ {
return static::$oidc_url; return $this->oidc_url;
} }
/** /**
* Configure the library with a OpenId Connect realm url * Get the OpenId Connect configuration
* *
* @param string $url * @return array
* @return void
*/ */
public static function setUrl(string $url): void public function getConfiguration(): array
{ {
static::$oidc_url = $url; if (!isset($this->oidc_config))
} {
$url = $this->oidc_url;
$this->oidc_config = Authkit2::cache('oidc.config.'.md5($this->oidc_url), function() use ($url) {
$response = (new \GuzzleHttp\Client())->get($url.'/.well-known/openid-configuration');
return json_decode($response->getBody(), true);
});
/** }
* Set the OpenId Connect configuration return $this->oidc_config;
*
* This is provided to allow external caching rather than having us refetch
* the configuration on every invocation of the project.
*
* @param array $config config as retrieved from getOidcConfig()
* @return void
*/
public static function setOidcConfig(array $config): void
{
static::$oidc_config = $config;
} }
/** /**
* Get the OpenId Connect configuration * Get the web key set for verifying JWTs
* *
* If not restored/set via setOidcConfig, this will be fetched from the OIDC * @return array<string,array>
* realm on first use
*
* @return array
*/ */
public static function getOidcConfig(): array public function getJsonWebKeySet(): array
{ {
if (!isset(static::$oidc_config)) if (!isset($this->oidc_jwks))
{ {
static::$oidc_config = Authkit2::cache('oidc.config', function() { $client = $this;
$response = (new \GuzzleHttp\Client())->get(static::$oidc_url.'/.well-known/openid-configuration'); $this->oidc_jwks = Authkit2::cache('oidc.config.'.md5($this->oidc_url).'.jwks', function() use ($client) {
return json_decode($response->getBody(), true); $response = $client->get($client->getConfiguration()['jwks_uri']);
return json_decode(json_encode($response), true);
}); });
} }
return static::$oidc_config; return $this->oidc_jwks;
} }
/** /**
* Fetch a specific OpenId Connect endpoint from the configuration * Get the signing algorithms for signing JWTs
* *
* @param string $endpoint_name * @return string[]
* @return string
*/ */
public function getEndpointUrl(string $endpoint_name): string public function getTokenSigningAlgorithms(): array
{ {
return static::getOidcConfig()[$endpoint_name.'_endpoint']; return $this->getConfiguration()['id_token_signing_alg_values_supported'];
} }
/** /**
* Determine the final URL to make a request to given an arbitrarily-defined * Fetch a specific OpenId Connect endpoint from the configuration
* 'endpoint'.
*
* If the passed in value is a valid URI, it will be used directly. Otherwise,
* we will attempt to find a configured endpoint with a name matching the
* passed in value in the OIDC config.
* *
* @param string $endpoint url or endpoint name * @param string $endpoint_name
* @return string url to call * @return string
*/ */
protected function parseEndpoint(string $endpoint): string public function getEndpointUrl(string $endpoint_name): string
{ {
if (filter_var($endpoint, \FILTER_VALIDATE_URL)) return $this->getConfiguration()[$endpoint_name.'_endpoint'];
{
return $endpoint;
}
else
{
return $this->getEndpointUrl($endpoint);
}
} }
/** /**
* Make a HTTP get request to a OIDC endpoint or other URL * Make a HTTP get request to a OIDC endpoint or other URL
* *
* @param string $endpoint url or endpoint name * @param string $url
* @param array<string,scalar> $params query string parameters * @param array<string,scalar> $params query string parameters
* @return object json decoded response * @return object json decoded response
*/ */
public function get(string $endpoint, array $params = []): object protected function get(string $url, array $params = []): object
{ {
$response = $this->getClient()->get($this->parseEndpoint($endpoint), [ $response = $this->getClient()->get($url, [
'query' => $params 'query' => $params
]); ]);
return json_decode($response->getBody()); return json_decode($response->getBody());
@ -173,15 +160,94 @@ class Client
* If form parameters are provided the request is sent as * If form parameters are provided the request is sent as
* application/x-www-form-urlencoded * application/x-www-form-urlencoded
* *
* @param string $endpoint url or endpoint name * @param string $url
* @param array<string,scalar> $params form fields * @param array<string,scalar> $params form fields
* @return object json decoded response * @return object json decoded response
*/ */
public function post(string $endpoint, array $params = []): object protected function post(string $url, array $params = []): object
{ {
$response = $this->getClient()->post($this->parseEndpoint($endpoint), [ $response = $this->getClient()->post($url, [
'form_params' => $params 'form_params' => $params
]); ]);
return json_decode($response->getBody()); 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);
}
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
]);
}
public function createLogoutUrl(string $redirect_uri): string
{
return $this->getEndpointUrl('end_session').'?'.http_build_query([
'redirect_uri' => $redirect_uri
]);
}
public function refreshToken(Token $token): Token
{
return $this->createTokenFromRefreshToken($token->getRefreshToken());
}
public function getTokenUserInfo(Token $token): array
{
return json_decode($token->getClient()->get($this->getEndpointUrl('userinfo'))->getBody(), true);
}
// todo: introspect, etc
} }

10
src/Oidc/Flows/ServiceAccountFlow.php

@ -19,12 +19,11 @@ class ServiceAccountFlow
/** /**
* Initialize a new service account flow * Initialize a new service account flow
* *
* @param string $client_id oauth client id * @param Client $client
* @param string $client_secret oauth client secret
*/ */
public function __construct(string $client_id, string $client_secret) public function __construct(Client $client)
{ {
$this->client = new Client(new ClientAuthentication($client_id, $client_secret)); $this->client = $client;
} }
/** /**
@ -34,8 +33,7 @@ class ServiceAccountFlow
*/ */
public function getServiceAccountToken(): Token public function getServiceAccountToken(): Token
{ {
$response = $this->client->post('token', ['grant_type'=>'client_credentials']); return $this->client->createTokenFromClient();
return Token::fromResponse($response);
} }
} }

46
src/Oidc/Flows/UserFlow.php

@ -12,32 +12,19 @@ use authkit2\Oidc\Token;
class UserFlow class UserFlow
{ {
/** /**
* OIDC client authenticating as the client * OIDC client
* @var Client * @var Client
*/ */
protected $client; protected $client;
/**
* oauth client id
* @var string
*/
protected $client_id;
/**
* oauth client secret
* @var string
*/
protected $client_secret;
/** /**
* Initialize a new user login flow * Initialize a new user login flow
* *
* @param string $client_id oauth client id * @param Client $client
* @param string $client_secret oauth client secret
*/ */
public function __construct(string $client_id, string $client_secret) public function __construct(Client $client)
{ {
$this->client_id = $client_id; $this->client = $client;
$this->client_secret = $client_secret;
$this->client = new Client(new ClientAuthentication($client_id, $client_secret));
} }
/** /**
@ -57,14 +44,7 @@ class UserFlow
$states = Authkit2::session_get('userflow.state') ?? []; $states = Authkit2::session_get('userflow.state') ?? [];
array_push($states, $state); array_push($states, $state);
Authkit2::session_set('userflow.state', $states); Authkit2::session_set('userflow.state', $states);
return $this->client->createAuthorizationRedirectUrl($redirect_uri, $scopes, $state);
return $this->client->getEndpointUrl('authorization').'?'.http_build_query([
'client_id' => $this->client_id,
'redirect_uri' => $redirect_uri,
'scope' => implode(',', $scopes),
'response_type' => 'code',
'state' => $state
]);
} }
/** /**
@ -92,19 +72,15 @@ class UserFlow
* After the user is redirected back with a authorization code, exchange it * After the user is redirected back with a authorization code, exchange it
* for an access token * for an access token
* *
* THIS DOES NOT VALIDATE THE STATE. Call validateState first.
*
* @param string $redirect_uri url the oidc endpoint redirects back to; must match one given in call to getRedirectUrl * @param string $redirect_uri url the oidc endpoint redirects back to; must match one given in call to getRedirectUrl
* @param string $code code returned by the authorization flow * @param string $code code returned by the authorization flow
* @return Token * @return Token
*/ */
public function exchangeCodeForToken(string $redirect_uri, string $code): Token public function exchangeCodeForToken(string $code, string $redirect_uri): Token
{ {
$response = $this->client->post('token', [ return $this->client->createTokenFromAuthorizationCode($code, $redirect_uri);
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $redirect_uri
]);
return Token::fromResponse($response);
} }
/** /**
@ -116,9 +92,7 @@ class UserFlow
*/ */
public function getLogoutUrl(string $redirect_uri): string public function getLogoutUrl(string $redirect_uri): string
{ {
return $this->client->getEndpointUrl('end_session').'?'.http_build_query([ return $this->client->createLogoutUrl($redirect_uri);
'redirect_uri' => $redirect_uri
]);
} }
} }

194
src/Oidc/Token.php

@ -12,16 +12,8 @@ use Firebase\JWT\JWK;
class Token class Token
{ {
/** /**
* If we check for an expired token and it expires within this many seconds * OIDC client
* from now, just go ahead and refresh it early * @var Client
* @var int
*/
const EXPIRATION_GRACE_PERIOD = 60;
/**
* Http client we've initialized with our authentication middleware for this
* token in place
* @var \GuzzleHttp\Client
*/ */
protected $client = null; protected $client = null;
@ -41,13 +33,25 @@ class Token
* Cache of userinfo endpoint response * Cache of userinfo endpoint response
* @var array<string,scalar> * @var array<string,scalar>
*/ */
protected $userinfo = null; protected $user_info = null;
/**
* Decoded access token JWT data
* @var array<string,mixed>
*/
protected $access_token_data = null;
/** /**
* Cache of decoded JWT token * Decoded refresh token JWT data
* @var array<string,mixed> * @var array<string,mixed>
*/ */
protected $token_data = null; 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 * Initialize token with the from*() static methods
@ -60,76 +64,167 @@ class Token
* Create a token given a access_token and optionally refresh_token, passed * Create a token given a access_token and optionally refresh_token, passed
* as a string * as a string
* *
* @param Client $client
* @param string $access_token * @param string $access_token
* @param ?string $refresh_token * @param ?string $refresh_token
* @return Token * @return Token
*/ */
public static function fromString(string $access_token, ?string $refresh_token = null): Token public static function fromString(Client $client, string $access_token, ?string $refresh_token = null): Token
{ {
$token = new Token(); $token = new Token();
$token->client = $client;
$token->access_token = $access_token; $token->access_token = $access_token;
$token->refresh_token = $refresh_token; $token->refresh_token = $refresh_token;
return $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 * Create a token from a OIDC response from the token endpoint
* *
* @param Client $client
* @param object $response * @param object $response
* @return Token * @return Token
*/ */
public static function fromResponse(object $response): Token public static function fromResponse(Client $client, object $response): Token
{ {
$token = new Token(); $token = new Token();
$token->client = $client;
$token->access_token = $response->access_token ?? null; $token->access_token = $response->access_token ?? null;
$token->access_token_expires_at = isset($response->expires_in) ? time() + $response->expires_in : null;
$token->refresh_token = $response->refresh_token ?? null; $token->refresh_token = $response->refresh_token ?? null;
$token->refresh_token_expires_at = isset($response->refresh_expires_in) ? time() + $response->refresh_expires_in : null;
return $token; return $token;
} }
/** /**
* Fetch an oidc client authenticated with this token * Fetch the raw decoded data out of our JWT access token
* *
* @return Client * @return array<string,mixed>
*/ */
public function getClient(): Client public function getAccessTokenData(): array
{ {
if (!isset($this->client)) if (!isset($this->access_token_data))
{ {
$this->client = new Client(new Authentication\TokenAuthentication($this)); $this->access_token_data = json_decode(json_encode($this->decode($this->access_token)), true);
} }
return $this->client; return $this->access_token_data;
} }
/** /**
* Refresh the access token * Fetch the raw decoded data out of our JWT refresh token
* *
* @return void * @return array<string,mixed>
*/ */
public function refresh() 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;
} }
/** /**
* Fetch the raw decoded data out of our JWT access token * Decode a token as a JWT token
* *
* @return array<string,mixed> * @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 getTokenData(): array public function isValid(): bool
{ {
if (!isset($this->token_data)) try
{ {
$this->token_data = json_decode(json_encode($this->decode()), true); $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;
} }
return $this->token_data;
} }
/** /**
* Check whether the access token is expired * Check whether the access token is expired
* *
* If we fail to parse it because it's expired, or the expiration time is * As long as the refresh token is valid, this is recoverly by calling
* within EXPIRATION_GRACE_PERIOD seconds of now, we consider it expired. * passing this token to refresh on the client.
* *
* @return bool * @return bool
*/ */
@ -137,31 +232,25 @@ class Token
{ {
try try
{ {
$data = $this->getTokenData(); $token_data = $this->getAccessTokenData();
if ($token_data['exp'] <= time())
return true;
return false;
} }
catch (\Firebase\JWT\ExpiredException $ex) catch (\Firebase\JWT\ExpiredException $ex)
{ {
return true; return true;
} }
if ($data['exp'] <= time() - static::EXPIRATION_GRACE_PERIOD)
return true;
else
return false;
} }
/** /**
* Decode the access token as a JWT token * Check whether this token needs a refresh to be used
* *
* @return object * @return bool
*/ */
protected function decode(): object public function needsRefresh(): bool
{ {
$client = $this->getClient(); return $this->isValid() && $this->isExpired() && isset($this->refresh_token);
$jwks = Authkit2::cache('oidc.jwks', function() use ($client) {
$response = $client->get(Client::getOidcConfig()['jwks_uri']);
return json_decode(json_encode($response), true);
});
return JWT::decode($this->access_token, JWK::parseKeySet($jwks), Client::getOidcConfig()['id_token_signing_alg_values_supported']);
} }
/** /**
@ -192,12 +281,7 @@ class Token
*/ */
public function getUserInfo(): array public function getUserInfo(): array
{ {
if (!isset($this->userinfo)) return $this->client->getTokenUserInfo($this);
{
$this->userinfo = json_decode(json_encode($this->getClient()->get('userinfo')), true);
}
return $this->userinfo;
} }
/** /**
@ -217,7 +301,7 @@ class Token
*/ */
public function getUserId(): string public function getUserId(): string
{ {
return 'crn:user:'.$this->getTokenData()['sub']; return 'crn:user:'.$this->getAccessTokenData()['sub'];
} }
} }

4
src/Providers/Authkit2ServiceProvider.php

@ -48,10 +48,6 @@ class Authkit2ServiceProvider extends ServiceProvider
__DIR__.'/../../database/migrations/existing/authkit2_users_update_minimal.php' => database_path('migrations/'.date('Y_m_d_His').'_authkit2_users_update_minimal.php') __DIR__.'/../../database/migrations/existing/authkit2_users_update_minimal.php' => database_path('migrations/'.date('Y_m_d_His').'_authkit2_users_update_minimal.php')
], 'migrations_existing'); ], 'migrations_existing');
} }
$this->app->booted(function($app) {
\authkit2\Oidc\Client::setUrl(config('authkit.authn.openid.endpoint'));
});
} }
} }

9
src/Providers/AuthnServiceProvider.php

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace authkit2\Providers; namespace authkit2\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use \authkit2\Authkit2;
/** /**
* Authentication provider to perform setup for authentication processes * Authentication provider to perform setup for authentication processes
@ -19,10 +20,10 @@ class AuthnServiceProvider extends ServiceProvider
public function register(): void public function register(): void
{ {
$this->app->singleton(\authkit2\Oidc\Flows\ServiceAccountFlow::class, function($app) { $this->app->singleton(\authkit2\Oidc\Flows\ServiceAccountFlow::class, function($app) {
return new \authkit2\Oidc\Flows\ServiceAccountFlow(config('authkit.authn.openid.client_id'), config('authkit.authn.openid.client_secret')); return new \authkit2\Oidc\Flows\ServiceAccountFlow(Authkit2::get_client());
}); });
$this->app->singleton(\authkit2\Oidc\Flows\UserFlow::class, function($app) { $this->app->singleton(\authkit2\Oidc\Flows\UserFlow::class, function($app) {
return new \authkit2\Oidc\Flows\UserFlow(config('authkit.authn.openid.client_id'), config('authkit.authn.openid.client_secret')); return new \authkit2\Oidc\Flows\UserFlow(Authkit2::get_client());
}); });
} }
@ -118,5 +119,9 @@ class AuthnServiceProvider extends ServiceProvider
'authkit.authn.openid.endpoint' => env('AUTHKIT_ENDPOINT', isset($config_json['auth-server-url']) && isset($config_json['realm']) ? $config_json['auth-server-url'].'realms/'.$config_json['realm'] : null) 'authkit.authn.openid.endpoint' => env('AUTHKIT_ENDPOINT', isset($config_json['auth-server-url']) && isset($config_json['realm']) ? $config_json['auth-server-url'].'realms/'.$config_json['realm'] : null)
]); ]);
} }
$this->app->booted(function($app) {
Authkit2::configure(config('authkit.authn.openid.client_id'), config('authkit.authn.openid.client_secret'), config('authkit.authn.openid.endpoint'));
});
} }
} }

Loading…
Cancel
Save