Auth: Refactored OIDC RP-logout PR code, Extracted logout

Extracted logout to the login service so the logic can be shared instead
of re-implemented at each stage. For this, the SocialAuthService was
split so the driver management is in its own class, so it can be used
elsewhere without use (or circular dependencies) of the
SocialAuthService.

During review of #4467
This commit is contained in:
Dan Brown 2023-12-06 13:49:53 +00:00
parent cc10d1ddfc
commit bba7dcce49
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
17 changed files with 263 additions and 288 deletions

View File

@ -273,11 +273,8 @@ OIDC_USER_TO_GROUPS=false
OIDC_GROUPS_CLAIM=groups OIDC_GROUPS_CLAIM=groups
OIDC_REMOVE_FROM_GROUPS=false OIDC_REMOVE_FROM_GROUPS=false
OIDC_EXTERNAL_ID_CLAIM=sub OIDC_EXTERNAL_ID_CLAIM=sub
# OIDC Logout Feature: Its value should be value of end_session_endpoint from <issuer>/.well-known/openid-configuration
OIDC_END_SESSION_ENDPOINT=null OIDC_END_SESSION_ENDPOINT=null
# Disable default third-party services such as Gravatar and Draw.IO # Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option # Service-specific options will override this option
DISABLE_EXTERNAL_SERVICES=false DISABLE_EXTERNAL_SERVICES=false

View File

@ -3,7 +3,7 @@
namespace BookStack\Access\Controllers; namespace BookStack\Access\Controllers;
use BookStack\Access\LoginService; use BookStack\Access\LoginService;
use BookStack\Access\SocialAuthService; use BookStack\Access\SocialDriverManager;
use BookStack\Exceptions\LoginAttemptEmailNeededException; use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException; use BookStack\Exceptions\LoginAttemptException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
@ -17,19 +17,19 @@ class LoginController extends Controller
{ {
use ThrottlesLogins; use ThrottlesLogins;
protected SocialAuthService $socialAuthService; protected SocialDriverManager $socialDriverManager;
protected LoginService $loginService; protected LoginService $loginService;
/** /**
* Create a new controller instance. * Create a new controller instance.
*/ */
public function __construct(SocialAuthService $socialAuthService, LoginService $loginService) public function __construct(SocialDriverManager $driverManager, LoginService $loginService)
{ {
$this->middleware('guest', ['only' => ['getLogin', 'login']]); $this->middleware('guest', ['only' => ['getLogin', 'login']]);
$this->middleware('guard:standard,ldap', ['only' => ['login']]); $this->middleware('guard:standard,ldap', ['only' => ['login']]);
$this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]); $this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
$this->socialAuthService = $socialAuthService; $this->socialDriverManager = $driverManager;
$this->loginService = $loginService; $this->loginService = $loginService;
} }
@ -38,7 +38,7 @@ class LoginController extends Controller
*/ */
public function getLogin(Request $request) public function getLogin(Request $request)
{ {
$socialDrivers = $this->socialAuthService->getActiveDrivers(); $socialDrivers = $this->socialDriverManager->getActive();
$authMethod = config('auth.method'); $authMethod = config('auth.method');
$preventInitiation = $request->get('prevent_auto_init') === 'true'; $preventInitiation = $request->get('prevent_auto_init') === 'true';
@ -101,15 +101,9 @@ class LoginController extends Controller
/** /**
* Logout user and perform subsequent redirect. * Logout user and perform subsequent redirect.
*/ */
public function logout(Request $request) public function logout()
{ {
Auth::guard()->logout(); return redirect($this->loginService->logout());
$request->session()->invalidate();
$request->session()->regenerateToken();
$redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
return redirect($redirectUri);
} }
/** /**
@ -218,16 +212,4 @@ class LoginController extends Controller
redirect()->setIntendedUrl($previous); redirect()->setIntendedUrl($previous);
} }
/**
* Check if login auto-initiate should be valid based upon authentication config.
*/
protected function shouldAutoInitiate(): bool
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
$autoRedirect = config('auth.auto_initiate');
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
}
} }

View File

@ -11,9 +11,6 @@ class OidcController extends Controller
{ {
protected OidcService $oidcService; protected OidcService $oidcService;
/**
* OpenIdController constructor.
*/
public function __construct(OidcService $oidcService) public function __construct(OidcService $oidcService)
{ {
$this->oidcService = $oidcService; $this->oidcService = $oidcService;
@ -65,16 +62,10 @@ class OidcController extends Controller
} }
/** /**
* OIDC Logout Feature: Start the authorization logout flow via OIDC. * Log the user out then start the OIDC RP-initiated logout process.
*/ */
public function logout() public function logout()
{ {
try { return redirect($this->oidcService->logout());
return $this->oidcService->logout();
} catch (OidcException $exception) {
$this->showErrorNotification($exception->getMessage());
return redirect('/logout');
}
} }
} }

View File

@ -4,7 +4,7 @@ namespace BookStack\Access\Controllers;
use BookStack\Access\LoginService; use BookStack\Access\LoginService;
use BookStack\Access\RegistrationService; use BookStack\Access\RegistrationService;
use BookStack\Access\SocialAuthService; use BookStack\Access\SocialDriverManager;
use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
@ -15,7 +15,7 @@ use Illuminate\Validation\Rules\Password;
class RegisterController extends Controller class RegisterController extends Controller
{ {
protected SocialAuthService $socialAuthService; protected SocialDriverManager $socialDriverManager;
protected RegistrationService $registrationService; protected RegistrationService $registrationService;
protected LoginService $loginService; protected LoginService $loginService;
@ -23,14 +23,14 @@ class RegisterController extends Controller
* Create a new controller instance. * Create a new controller instance.
*/ */
public function __construct( public function __construct(
SocialAuthService $socialAuthService, SocialDriverManager $socialDriverManager,
RegistrationService $registrationService, RegistrationService $registrationService,
LoginService $loginService LoginService $loginService
) { ) {
$this->middleware('guest'); $this->middleware('guest');
$this->middleware('guard:standard'); $this->middleware('guard:standard');
$this->socialAuthService = $socialAuthService; $this->socialDriverManager = $socialDriverManager;
$this->registrationService = $registrationService; $this->registrationService = $registrationService;
$this->loginService = $loginService; $this->loginService = $loginService;
} }
@ -43,7 +43,7 @@ class RegisterController extends Controller
public function getRegister() public function getRegister()
{ {
$this->registrationService->ensureRegistrationAllowed(); $this->registrationService->ensureRegistrationAllowed();
$socialDrivers = $this->socialAuthService->getActiveDrivers(); $socialDrivers = $this->socialDriverManager->getActive();
return view('auth.register', [ return view('auth.register', [
'socialDrivers' => $socialDrivers, 'socialDrivers' => $socialDrivers,

View File

@ -79,7 +79,7 @@ class SocialController extends Controller
try { try {
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser); return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
} catch (SocialSignInAccountNotUsed $exception) { } catch (SocialSignInAccountNotUsed $exception) {
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) { if ($this->socialAuthService->drivers()->isAutoRegisterEnabled($socialDriver)) {
return $this->socialRegisterCallback($socialDriver, $socialUser); return $this->socialRegisterCallback($socialDriver, $socialUser);
} }
@ -114,7 +114,7 @@ class SocialController extends Controller
{ {
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser); $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser); $socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver); $emailVerified = $this->socialAuthService->drivers()->isAutoConfirmEmailEnabled($socialDriver);
// Create an array of the user data to create a new user instance // Create an array of the user data to create a new user instance
$userData = [ $userData = [

View File

@ -16,13 +16,11 @@ class LoginService
{ {
protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted'; protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
protected $mfaSession; public function __construct(
protected $emailConfirmationService; protected MfaSession $mfaSession,
protected EmailConfirmationService $emailConfirmationService,
public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService) protected SocialDriverManager $socialDriverManager,
{ ) {
$this->mfaSession = $mfaSession;
$this->emailConfirmationService = $emailConfirmationService;
} }
/** /**
@ -163,4 +161,29 @@ class LoginService
return $result; return $result;
} }
/**
* Logs the current user out of the application.
* Returns an app post-redirect path.
*/
public function logout(): string
{
auth()->logout();
session()->invalidate();
session()->regenerateToken();
return $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
}
/**
* Check if login auto-initiate should be valid based upon authentication config.
*/
protected function shouldAutoInitiate(): bool
{
$socialDrivers = $this->socialDriverManager->getActive();
$authMethod = config('auth.method');
$autoRedirect = config('auth.auto_initiate');
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
}
} }

View File

@ -217,11 +217,7 @@ class OidcService
$settings->keys, $settings->keys,
); );
// OIDC Logout Feature: Temporarily save token in session session()->put("oidc_id_token", $idTokenText);
$access_token_for_logout = $idTokenText;
session()->put("oidctoken", $access_token_for_logout);
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [ $returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
'access_token' => $accessToken->getToken(), 'access_token' => $accessToken->getToken(),
@ -291,36 +287,24 @@ class OidcService
return $this->config()['user_to_groups'] !== false; return $this->config()['user_to_groups'] !== false;
} }
/** /**
* OIDC Logout Feature: Initiate a logout flow. * Start the RP-initiated logout flow if active, otherwise start a standard logout flow.
* * Returns a post-app-logout redirect URL.
* @throws OidcException * Reference: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
*
* @return string
*/ */
public function logout() { public function logout(): string
{
$endSessionEndpoint = $this->config()["end_session_endpoint"];
$config = $this->config(); // TODO - Add autodiscovery and false/null config value support.
$app_url = env('APP_URL', '');
$end_session_endpoint = $config["end_session_endpoint"];
$oidctoken = session()->get("oidctoken");
session()->invalidate();
if (str_contains($app_url, 'https://')) {
$protocol = 'https://';
} else {
$protocol = 'http://';
}
return redirect($end_session_endpoint.'?id_token_hint='.$oidctoken."&post_logout_redirect_uri=".$protocol.$_SERVER['HTTP_HOST']."/");
$oidcToken = session()->pull("oidc_id_token");
$defaultLogoutUrl = url($this->loginService->logout());
$endpointParams = [
'id_token_hint' => $oidcToken,
'post_logout_redirect_uri' => $defaultLogoutUrl,
];
return $endSessionEndpoint . '?' . http_build_query($endpointParams);
} }
} }

View File

@ -71,8 +71,7 @@ class Saml2Service
throw $error; throw $error;
} }
$this->actionLogout(); $url = $this->loginService->logout();
$url = '/';
$id = null; $id = null;
} }
@ -140,20 +139,11 @@ class Saml2Service
); );
} }
$this->actionLogout(); $this->loginService->logout();
return $redirect; return $redirect;
} }
/**
* Do the required actions to log a user out.
*/
protected function actionLogout()
{
auth()->logout();
session()->invalidate();
}
/** /**
* Get the metadata for this service provider. * Get the metadata for this service provider.
* *

View File

@ -2,69 +2,24 @@
namespace BookStack\Access; namespace BookStack\Access;
use BookStack\Auth\Access\handler;
use BookStack\Exceptions\SocialDriverNotConfigured; use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed; use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite; use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\Provider; use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Contracts\User as SocialUser; use Laravel\Socialite\Contracts\User as SocialUser;
use Laravel\Socialite\Two\GoogleProvider; use Laravel\Socialite\Two\GoogleProvider;
use SocialiteProviders\Manager\SocialiteWasCalled;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
class SocialAuthService class SocialAuthService
{ {
/** public function __construct(
* The core socialite library used. protected Socialite $socialite,
* protected LoginService $loginService,
* @var Socialite protected SocialDriverManager $driverManager,
*/ ) {
protected $socialite;
/**
* @var LoginService
*/
protected $loginService;
/**
* The default built-in social drivers we support.
*
* @var string[]
*/
protected $validSocialDrivers = [
'google',
'github',
'facebook',
'slack',
'twitter',
'azure',
'okta',
'gitlab',
'twitch',
'discord',
];
/**
* Callbacks to run when configuring a social driver
* for an initial redirect action.
* Array is keyed by social driver name.
* Callbacks are passed an instance of the driver.
*
* @var array<string, callable>
*/
protected $configureForRedirectCallbacks = [];
/**
* SocialAuthService constructor.
*/
public function __construct(Socialite $socialite, LoginService $loginService)
{
$this->socialite = $socialite;
$this->loginService = $loginService;
} }
/** /**
@ -74,9 +29,10 @@ class SocialAuthService
*/ */
public function startLogIn(string $socialDriver): RedirectResponse public function startLogIn(string $socialDriver): RedirectResponse
{ {
$driver = $this->validateDriver($socialDriver); $socialDriver = trim(strtolower($socialDriver));
$this->driverManager->ensureDriverActive($socialDriver);
return $this->getDriverForRedirect($driver)->redirect(); return $this->getDriverForRedirect($socialDriver)->redirect();
} }
/** /**
@ -86,9 +42,10 @@ class SocialAuthService
*/ */
public function startRegister(string $socialDriver): RedirectResponse public function startRegister(string $socialDriver): RedirectResponse
{ {
$driver = $this->validateDriver($socialDriver); $socialDriver = trim(strtolower($socialDriver));
$this->driverManager->ensureDriverActive($socialDriver);
return $this->getDriverForRedirect($driver)->redirect(); return $this->getDriverForRedirect($socialDriver)->redirect();
} }
/** /**
@ -119,9 +76,10 @@ class SocialAuthService
*/ */
public function getSocialUser(string $socialDriver): SocialUser public function getSocialUser(string $socialDriver): SocialUser
{ {
$driver = $this->validateDriver($socialDriver); $socialDriver = trim(strtolower($socialDriver));
$this->driverManager->ensureDriverActive($socialDriver);
return $this->socialite->driver($driver)->user(); return $this->socialite->driver($socialDriver)->user();
} }
/** /**
@ -131,6 +89,7 @@ class SocialAuthService
*/ */
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser) public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
{ {
$socialDriver = trim(strtolower($socialDriver));
$socialId = $socialUser->getId(); $socialId = $socialUser->getId();
// Get any attached social accounts or users // Get any attached social accounts or users
@ -181,76 +140,11 @@ class SocialAuthService
} }
/** /**
* Ensure the social driver is correct and supported. * Get the social driver manager used by this service.
*
* @throws SocialDriverNotConfigured
*/ */
protected function validateDriver(string $socialDriver): string public function drivers(): SocialDriverManager
{ {
$driver = trim(strtolower($socialDriver)); return $this->driverManager;
if (!in_array($driver, $this->validSocialDrivers)) {
abort(404, trans('errors.social_driver_not_found'));
}
if (!$this->checkDriverConfigured($driver)) {
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($socialDriver)]));
}
return $driver;
}
/**
* Check a social driver has been configured correctly.
*/
protected function checkDriverConfigured(string $driver): bool
{
$lowerName = strtolower($driver);
$configPrefix = 'services.' . $lowerName . '.';
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
return !in_array(false, $config) && !in_array(null, $config);
}
/**
* Gets the names of the active social drivers.
* @returns array<string, string>
*/
public function getActiveDrivers(): array
{
$activeDrivers = [];
foreach ($this->validSocialDrivers as $driverKey) {
if ($this->checkDriverConfigured($driverKey)) {
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
}
}
return $activeDrivers;
}
/**
* Get the presentational name for a driver.
*/
public function getDriverName(string $driver): string
{
return config('services.' . strtolower($driver) . '.name');
}
/**
* Check if the current config for the given driver allows auto-registration.
*/
public function driverAutoRegisterEnabled(string $driver): bool
{
return config('services.' . strtolower($driver) . '.auto_register') === true;
}
/**
* Check if the current config for the given driver allow email address auto-confirmation.
*/
public function driverAutoConfirmEmailEnabled(string $driver): bool
{
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
} }
/** /**
@ -284,33 +178,8 @@ class SocialAuthService
$driver->with(['prompt' => 'select_account']); $driver->with(['prompt' => 'select_account']);
} }
if (isset($this->configureForRedirectCallbacks[$driverName])) { $this->driverManager->getConfigureForRedirectCallback($driverName)($driver);
$this->configureForRedirectCallbacks[$driverName]($driver);
}
return $driver; return $driver;
} }
/**
* Add a custom socialite driver to be used.
* Driver name should be lower_snake_case.
* Config array should mirror the structure of a service
* within the `Config/services.php` file.
* Handler should be a Class@method handler to the SocialiteWasCalled event.
*/
public function addSocialDriver(
string $driverName,
array $config,
string $socialiteHandler,
callable $configureForRedirect = null
) {
$this->validSocialDrivers[] = $driverName;
config()->set('services.' . $driverName, $config);
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
if (!is_null($configureForRedirect)) {
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
}
}
} }

View File

@ -0,0 +1,147 @@
<?php
namespace BookStack\Access;
use BookStack\Exceptions\SocialDriverNotConfigured;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use SocialiteProviders\Manager\SocialiteWasCalled;
class SocialDriverManager
{
/**
* The default built-in social drivers we support.
*
* @var string[]
*/
protected array $validDrivers = [
'google',
'github',
'facebook',
'slack',
'twitter',
'azure',
'okta',
'gitlab',
'twitch',
'discord',
];
/**
* Callbacks to run when configuring a social driver
* for an initial redirect action.
* Array is keyed by social driver name.
* Callbacks are passed an instance of the driver.
*
* @var array<string, callable>
*/
protected array $configureForRedirectCallbacks = [];
/**
* Check if the current config for the given driver allows auto-registration.
*/
public function isAutoRegisterEnabled(string $driver): bool
{
return $this->getDriverConfigProperty($driver, 'auto_register') === true;
}
/**
* Check if the current config for the given driver allow email address auto-confirmation.
*/
public function isAutoConfirmEmailEnabled(string $driver): bool
{
return $this->getDriverConfigProperty($driver, 'auto_confirm') === true;
}
/**
* Gets the names of the active social drivers, keyed by driver id.
* @returns array<string, string>
*/
public function getActive(): array
{
$activeDrivers = [];
foreach ($this->validDrivers as $driverKey) {
if ($this->checkDriverConfigured($driverKey)) {
$activeDrivers[$driverKey] = $this->getName($driverKey);
}
}
return $activeDrivers;
}
/**
* Get the configure-for-redirect callback for the given driver.
* This is a callable that allows modification of the driver at redirect time.
* Commonly used to perform custom dynamic configuration where required.
* The callback is passed a \Laravel\Socialite\Contracts\Provider instance.
*/
public function getConfigureForRedirectCallback(string $driver): callable
{
return $this->configureForRedirectCallbacks[$driver] ?? (fn() => true);
}
/**
* Add a custom socialite driver to be used.
* Driver name should be lower_snake_case.
* Config array should mirror the structure of a service
* within the `Config/services.php` file.
* Handler should be a Class@method handler to the SocialiteWasCalled event.
*/
public function addSocialDriver(
string $driverName,
array $config,
string $socialiteHandler,
callable $configureForRedirect = null
) {
$this->validDrivers[] = $driverName;
config()->set('services.' . $driverName, $config);
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
if (!is_null($configureForRedirect)) {
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
}
}
/**
* Get the presentational name for a driver.
*/
protected function getName(string $driver): string
{
return $this->getDriverConfigProperty($driver, 'name') ?? '';
}
protected function getDriverConfigProperty(string $driver, string $property): mixed
{
return config("services.{$driver}.{$property}");
}
/**
* Ensure the social driver is correct and supported.
*
* @throws SocialDriverNotConfigured
*/
public function ensureDriverActive(string $driverName): void
{
if (!in_array($driverName, $this->validDrivers)) {
abort(404, trans('errors.social_driver_not_found'));
}
if (!$this->checkDriverConfigured($driverName)) {
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($driverName)]));
}
}
/**
* Check a social driver has been configured correctly.
*/
protected function checkDriverConfigured(string $driver): bool
{
$lowerName = strtolower($driver);
$configPrefix = 'services.' . $lowerName . '.';
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
return !in_array(false, $config) && !in_array(null, $config);
}
}

View File

@ -2,7 +2,7 @@
namespace BookStack\App\Providers; namespace BookStack\App\Providers;
use BookStack\Access\SocialAuthService; use BookStack\Access\SocialDriverManager;
use BookStack\Activity\Tools\ActivityLogger; use BookStack\Activity\Tools\ActivityLogger;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
@ -36,7 +36,7 @@ class AppServiceProvider extends ServiceProvider
public $singletons = [ public $singletons = [
'activity' => ActivityLogger::class, 'activity' => ActivityLogger::class,
SettingService::class => SettingService::class, SettingService::class => SettingService::class,
SocialAuthService::class => SocialAuthService::class, SocialDriverManager::class => SocialDriverManager::class,
CspService::class => CspService::class, CspService::class => CspService::class,
HttpRequestService::class => HttpRequestService::class, HttpRequestService::class => HttpRequestService::class,
]; ];

View File

@ -36,6 +36,12 @@ return [
'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null), 'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null), 'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null),
// OIDC RP-Initiated Logout endpoint
// A null value gets the URL from discovery, if active.
// A false value force-disables RP-Initiated Logout.
// A string value forces the given URL to be used.
'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', null),
// Add extra scopes, upon those required, to the OIDC authentication request // Add extra scopes, upon those required, to the OIDC authentication request
// Multiple values can be provided comma seperated. // Multiple values can be provided comma seperated.
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null), 'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
@ -45,11 +51,6 @@ return [
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false), 'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
// Attribute, within a OIDC ID token, to find group names within // Attribute, within a OIDC ID token, to find group names within
'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'), 'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'),
// When syncing groups, remove any groups that no longer match. Otherwise sync only adds new groups. // When syncing groups, remove any groups that no longer match. Otherwise, sync only adds new groups.
'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false), 'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false),
// OIDC Logout Feature: OAuth2 end_session_endpoint
'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', null),
]; ];

View File

@ -2,7 +2,7 @@
namespace BookStack\Theming; namespace BookStack\Theming;
use BookStack\Access\SocialAuthService; use BookStack\Access\SocialDriverManager;
use BookStack\Exceptions\ThemeException; use BookStack\Exceptions\ThemeException;
use Illuminate\Console\Application; use Illuminate\Console\Application;
use Illuminate\Console\Application as Artisan; use Illuminate\Console\Application as Artisan;
@ -82,11 +82,11 @@ class ThemeService
} }
/** /**
* @see SocialAuthService::addSocialDriver * @see SocialDriverManager::addSocialDriver
*/ */
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null): void public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null): void
{ {
$socialAuthService = app()->make(SocialAuthService::class); $driverManager = app()->make(SocialDriverManager::class);
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect); $driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
} }
} }

View File

@ -2,7 +2,7 @@
namespace BookStack\Users\Controllers; namespace BookStack\Users\Controllers;
use BookStack\Access\SocialAuthService; use BookStack\Access\SocialDriverManager;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\UserNotificationPreferences; use BookStack\Settings\UserNotificationPreferences;
@ -161,7 +161,7 @@ class UserAccountController extends Controller
/** /**
* Show the view for the "Access & Security" account options. * Show the view for the "Access & Security" account options.
*/ */
public function showAuth(SocialAuthService $socialAuthService) public function showAuth(SocialDriverManager $socialDriverManager)
{ {
$mfaMethods = user()->mfaValues()->get()->groupBy('method'); $mfaMethods = user()->mfaValues()->get()->groupBy('method');
@ -171,7 +171,7 @@ class UserAccountController extends Controller
'category' => 'auth', 'category' => 'auth',
'mfaMethods' => $mfaMethods, 'mfaMethods' => $mfaMethods,
'authMethod' => config('auth.method'), 'authMethod' => config('auth.method'),
'activeSocialDrivers' => $socialAuthService->getActiveDrivers(), 'activeSocialDrivers' => $socialDriverManager->getActive(),
]); ]);
} }

View File

@ -2,7 +2,7 @@
namespace BookStack\Users\Controllers; namespace BookStack\Users\Controllers;
use BookStack\Access\SocialAuthService; use BookStack\Access\SocialDriverManager;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\UserUpdateException; use BookStack\Exceptions\UserUpdateException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
@ -101,7 +101,7 @@ class UserController extends Controller
/** /**
* Show the form for editing the specified user. * Show the form for editing the specified user.
*/ */
public function edit(int $id, SocialAuthService $socialAuthService) public function edit(int $id, SocialDriverManager $socialDriverManager)
{ {
$this->checkPermission('users-manage'); $this->checkPermission('users-manage');
@ -109,7 +109,7 @@ class UserController extends Controller
$user->load(['apiTokens', 'mfaValues']); $user->load(['apiTokens', 'mfaValues']);
$authMethod = ($user->system_name) ? 'system' : config('auth.method'); $authMethod = ($user->system_name) ? 'system' : config('auth.method');
$activeSocialDrivers = $socialAuthService->getActiveDrivers(); $activeSocialDrivers = $socialDriverManager->getActive();
$mfaMethods = $user->mfaValues->groupBy('method'); $mfaMethods = $user->mfaValues->groupBy('method');
$this->setPageTitle(trans('settings.user_profile')); $this->setPageTitle(trans('settings.user_profile'));
$roles = Role::query()->orderBy('display_name', 'asc')->get(); $roles = Role::query()->orderBy('display_name', 'asc')->get();

View File

@ -29,28 +29,20 @@
</li> </li>
<li><hr></li> <li><hr></li>
<li> <li>
<?php @php
// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc. $logoutPath = match (config('auth.method')) {
if (config('auth.method') === 'oidc') { 'saml2' => '/saml2/logout',
?> 'oidc' => '/oidc/logout',
<form action="/oidc/logout" default => '/logout',
method="get"> }
<?php @endphp
// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc. <form action="{{ url($logoutPath) }}" method="post">
} else { {{ csrf_field() }}
?> <button class="icon-item" data-shortcut="logout">
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}" @icon('logout')
method="post"> <div>{{ trans('auth.logout') }}</div>
<?php </button>
// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc. </form>
}
?>
{{ csrf_field() }}
<button class="icon-item" data-shortcut="logout">
@icon('logout')
<div>{{ trans('auth.logout') }}</div>
</button>
</form>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -332,8 +332,7 @@ Route::get('/saml2/acs', [AccessControllers\Saml2Controller::class, 'processAcs'
// OIDC routes // OIDC routes
Route::post('/oidc/login', [AccessControllers\OidcController::class, 'login']); Route::post('/oidc/login', [AccessControllers\OidcController::class, 'login']);
Route::get('/oidc/callback', [AccessControllers\OidcController::class, 'callback']); Route::get('/oidc/callback', [AccessControllers\OidcController::class, 'callback']);
// OIDC Logout Feature: Added to cater OIDC logout Route::post('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
Route::get('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
// User invitation routes // User invitation routes
Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']); Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);