use BookStack\Exceptions\OpenIdException;
use BookStack\Exceptions\UserRegistrationException;
use Exception;
-use Illuminate\Support\Str;
use Lcobucci\JWT\Token;
+use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use OpenIDConnectClient\AccessToken;
use OpenIDConnectClient\OpenIDConnectProvider;
class OpenIdService extends ExternalAuthService
{
protected $config;
- protected $registrationService;
- protected $user;
/**
* OpenIdService constructor.
*/
public function __construct(RegistrationService $registrationService, User $user)
{
+ parent::__construct($registrationService, $user);
+
$this->config = config('openid');
- $this->registrationService = $registrationService;
- $this->user = $user;
}
/**
return ['url' => $url, 'id' => $id];
}
+ /**
+ * Refresh the currently logged in user.
+ * @throws Error
+ */
+ public function refresh(): bool
+ {
+ // Retrieve access token for current session
+ $json = session()->get('openid_token');
+
+ // If no access token was found, reject the refresh
+ if (!$json) {
+ $this->actionLogout();
+ return false;
+ }
+
+ $accessToken = new AccessToken(json_decode($json, true) ?? []);
+
+ // If the token is not expired, refreshing isn't necessary
+ if ($this->isUnexpired($accessToken)) {
+ return true;
+ }
+
+ // Try to obtain refreshed access token
+ try {
+ $newAccessToken = $this->refreshAccessToken($accessToken);
+ } catch (\Exception $e) {
+ // Log out if an unknown problem arises
+ $this->actionLogout();
+ throw $e;
+ }
+
+ // If a token was obtained, update the access token, otherwise log out
+ if ($newAccessToken !== null) {
+ session()->put('openid_token', json_encode($newAccessToken));
+ return true;
+ } else {
+ $this->actionLogout();
+ return false;
+ }
+ }
+
+ /**
+ * Check whether an access token or OpenID token isn't expired.
+ */
+ protected function isUnexpired(AccessToken $accessToken): bool
+ {
+ $idToken = $accessToken->getIdToken();
+
+ $accessTokenUnexpired = $accessToken->getExpires() && !$accessToken->hasExpired();
+ $idTokenUnexpired = !$idToken || !$idToken->isExpired();
+
+ return $accessTokenUnexpired && $idTokenUnexpired;
+ }
+
+ /**
+ * Generate an updated access token, through the associated refresh token.
+ * @throws Error
+ */
+ protected function refreshAccessToken(AccessToken $accessToken): ?AccessToken
+ {
+ // If no refresh token available, abort
+ if ($accessToken->getRefreshToken() === null) {
+ return null;
+ }
+
+ // ID token or access token is expired, we refresh it using the refresh token
+ try {
+ return $this->getProvider()->getAccessToken('refresh_token', [
+ 'refresh_token' => $accessToken->getRefreshToken(),
+ ]);
+ } catch (IdentityProviderException $e) {
+ // Refreshing failed
+ return null;
+ }
+ }
+
/**
* Process the Authorization response from the authorization server and
* return the matching, or new if registration active, user matched to
}
/**
- * Load the underlying Onelogin SAML2 toolkit.
+ * Load the underlying OpenID Connect Provider.
* @throws Error
* @throws Exception
*/
protected function getProvider(): OpenIDConnectProvider
{
+ // Setup settings
$settings = $this->config['openid'];
$overrides = $this->config['openid_overrides'] ?? [];
$openIdSettings = $this->loadOpenIdDetails();
$settings = array_replace_recursive($settings, $openIdSettings, $overrides);
- $signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
- return new OpenIDConnectProvider($settings, ['signer' => $signer]);
+ // Setup services
+ $services = $this->loadOpenIdServices();
+ $overrides = $this->config['openid_services'] ?? [];
+
+ $services = array_replace_recursive($services, $overrides);
+
+ return new OpenIDConnectProvider($settings, $services);
}
/**
- * Load dynamic service provider options required by the onelogin toolkit.
+ * Load services utilized by the OpenID Connect provider.
+ */
+ protected function loadOpenIdServices(): array
+ {
+ return [
+ 'signer' => new \Lcobucci\JWT\Signer\Rsa\Sha256(),
+ ];
+ }
+
+ /**
+ * Load dynamic service provider options required by the OpenID Connect provider.
*/
protected function loadOpenIdDetails(): array
{
}
/**
- * Extract the details of a user from a SAML response.
+ * Extract the details of a user from an ID token.
*/
protected function getUserDetails(Token $token): array
{
];
}
- /**
- * Get the user from the database for the specified details.
- * @throws OpenIdException
- * @throws UserRegistrationException
- */
- protected function getOrRegisterUser(array $userDetails): ?User
- {
- $user = $this->user->newQuery()
- ->where('external_auth_id', '=', $userDetails['external_id'])
- ->first();
-
- if (is_null($user)) {
- $userData = [
- 'name' => $userDetails['name'],
- 'email' => $userDetails['email'],
- 'password' => Str::random(32),
- 'external_auth_id' => $userDetails['external_id'],
- ];
-
- $user = $this->registrationService->registerUser($userData, null, false);
- }
-
- return $user;
- }
-
/**
* Processes a received access token for a user. Login the user when
* they exist, optionally registering them automatically.
}
auth()->login($user);
+ session()->put('openid_token', json_encode($accessToken));
return $user;
}
}