]> BookStack Code Mirror - bookstack/commitdiff
Auth: Refactored OIDC RP-logout PR code, Extracted logout
authorDan Brown <redacted>
Wed, 6 Dec 2023 13:49:53 +0000 (13:49 +0000)
committerDan Brown <redacted>
Wed, 6 Dec 2023 13:49:53 +0000 (13:49 +0000)
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

17 files changed:
.env.example.complete
app/Access/Controllers/LoginController.php
app/Access/Controllers/OidcController.php
app/Access/Controllers/RegisterController.php
app/Access/Controllers/SocialController.php
app/Access/LoginService.php
app/Access/Oidc/OidcService.php
app/Access/Saml2Service.php
app/Access/SocialAuthService.php
app/Access/SocialDriverManager.php [new file with mode: 0644]
app/App/Providers/AppServiceProvider.php
app/Config/oidc.php
app/Theming/ThemeService.php
app/Users/Controllers/UserAccountController.php
app/Users/Controllers/UserController.php
resources/views/layouts/parts/header-user-menu.blade.php
routes/web.php

index 0667cb75bd4ad1f85069c3caf700ece1bfd61ca1..57d24eb5da92b837898a6608dd51c5d29f6fc71e 100644 (file)
@@ -273,11 +273,8 @@ OIDC_USER_TO_GROUPS=false
 OIDC_GROUPS_CLAIM=groups
 OIDC_REMOVE_FROM_GROUPS=false
 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
 
-
 # Disable default third-party services such as Gravatar and Draw.IO
 # Service-specific options will override this option
 DISABLE_EXTERNAL_SERVICES=false
index 3b4f9b347aadaed26e196972ae7ed837753f30ba..9047366566d8082de71249e602c7cdaebbeecd91 100644 (file)
@@ -3,7 +3,7 @@
 namespace BookStack\Access\Controllers;
 
 use BookStack\Access\LoginService;
-use BookStack\Access\SocialAuthService;
+use BookStack\Access\SocialDriverManager;
 use BookStack\Exceptions\LoginAttemptEmailNeededException;
 use BookStack\Exceptions\LoginAttemptException;
 use BookStack\Facades\Activity;
@@ -17,19 +17,19 @@ class LoginController extends Controller
 {
     use ThrottlesLogins;
 
-    protected SocialAuthService $socialAuthService;
+    protected SocialDriverManager $socialDriverManager;
     protected LoginService $loginService;
 
     /**
      * 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('guard:standard,ldap', ['only' => ['login']]);
         $this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
 
-        $this->socialAuthService = $socialAuthService;
+        $this->socialDriverManager = $driverManager;
         $this->loginService = $loginService;
     }
 
@@ -38,7 +38,7 @@ class LoginController extends Controller
      */
     public function getLogin(Request $request)
     {
-        $socialDrivers = $this->socialAuthService->getActiveDrivers();
+        $socialDrivers = $this->socialDriverManager->getActive();
         $authMethod = config('auth.method');
         $preventInitiation = $request->get('prevent_auto_init') === 'true';
 
@@ -101,15 +101,9 @@ class LoginController extends Controller
     /**
      * Logout user and perform subsequent redirect.
      */
-    public function logout(Request $request)
+    public function logout()
     {
-        Auth::guard()->logout();
-        $request->session()->invalidate();
-        $request->session()->regenerateToken();
-
-        $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
-
-        return redirect($redirectUri);
+        return redirect($this->loginService->logout());
     }
 
     /**
@@ -218,16 +212,4 @@ class LoginController extends Controller
 
         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']);
-    }
 }
index 083e83e35773131a199b0d34fb815247d10065bf..055d4c1403ff2cae480bb8424f065b3a6d159970 100644 (file)
@@ -11,9 +11,6 @@ class OidcController extends Controller
 {
     protected OidcService $oidcService;
 
-    /**
-     * OpenIdController constructor.
-     */
     public function __construct(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()
     {
-        try {
-            return $this->oidcService->logout();
-        } catch (OidcException $exception) {
-            $this->showErrorNotification($exception->getMessage());
-            return redirect('/logout');
-        }
+        return redirect($this->oidcService->logout());
     }
-
 }
index 3c653a073a3b7b753c17b6e656565af04ebb2d15..13b97f03c9632c6f280cf305e5064ef142d67ada 100644 (file)
@@ -4,7 +4,7 @@ namespace BookStack\Access\Controllers;
 
 use BookStack\Access\LoginService;
 use BookStack\Access\RegistrationService;
-use BookStack\Access\SocialAuthService;
+use BookStack\Access\SocialDriverManager;
 use BookStack\Exceptions\StoppedAuthenticationException;
 use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Http\Controller;
@@ -15,7 +15,7 @@ use Illuminate\Validation\Rules\Password;
 
 class RegisterController extends Controller
 {
-    protected SocialAuthService $socialAuthService;
+    protected SocialDriverManager $socialDriverManager;
     protected RegistrationService $registrationService;
     protected LoginService $loginService;
 
@@ -23,14 +23,14 @@ class RegisterController extends Controller
      * Create a new controller instance.
      */
     public function __construct(
-        SocialAuthService $socialAuthService,
+        SocialDriverManager $socialDriverManager,
         RegistrationService $registrationService,
         LoginService $loginService
     ) {
         $this->middleware('guest');
         $this->middleware('guard:standard');
 
-        $this->socialAuthService = $socialAuthService;
+        $this->socialDriverManager = $socialDriverManager;
         $this->registrationService = $registrationService;
         $this->loginService = $loginService;
     }
@@ -43,7 +43,7 @@ class RegisterController extends Controller
     public function getRegister()
     {
         $this->registrationService->ensureRegistrationAllowed();
-        $socialDrivers = $this->socialAuthService->getActiveDrivers();
+        $socialDrivers = $this->socialDriverManager->getActive();
 
         return view('auth.register', [
             'socialDrivers' => $socialDrivers,
index ff6d5c2ddd2d5ef014edfbf2eeaf82ecafbab4dd..bbdabb4ab02824ea11ccc87010eb98c28f2b3ea8 100644 (file)
@@ -79,7 +79,7 @@ class SocialController extends Controller
             try {
                 return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
             } catch (SocialSignInAccountNotUsed $exception) {
-                if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
+                if ($this->socialAuthService->drivers()->isAutoRegisterEnabled($socialDriver)) {
                     return $this->socialRegisterCallback($socialDriver, $socialUser);
                 }
 
@@ -114,7 +114,7 @@ class SocialController extends Controller
     {
         $socialUser = $this->socialAuthService->handleRegistrationCallback($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
         $userData = [
index 27480ba214ce6d9846f9d32434109c76f2de9c95..f0f6ad4d3a0500b3c4f68266006529a1e6179fc5 100644 (file)
@@ -16,13 +16,11 @@ class LoginService
 {
     protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
 
-    protected $mfaSession;
-    protected $emailConfirmationService;
-
-    public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
-    {
-        $this->mfaSession = $mfaSession;
-        $this->emailConfirmationService = $emailConfirmationService;
+    public function __construct(
+        protected MfaSession $mfaSession,
+        protected EmailConfirmationService $emailConfirmationService,
+        protected SocialDriverManager $socialDriverManager,
+    ) {
     }
 
     /**
@@ -163,4 +161,29 @@ class LoginService
 
         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']);
+    }
 }
index 1067b0832d44086e41b58cafe25521ec939385e1..be869b1797faa2efe183a1de10402c76228fea5b 100644 (file)
@@ -217,11 +217,7 @@ class OidcService
             $settings->keys,
         );
 
-        // OIDC Logout Feature: Temporarily save token in session 
-        $access_token_for_logout = $idTokenText;
-        session()->put("oidctoken", $access_token_for_logout);
-
-
+        session()->put("oidc_id_token", $idTokenText);
 
         $returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
             'access_token' => $accessToken->getToken(),
@@ -291,36 +287,24 @@ class OidcService
         return $this->config()['user_to_groups'] !== false;
     }
 
-
     /**
-     * OIDC Logout Feature: Initiate a logout flow.
-     *
-     * @throws OidcException
-     *
-     * @return string
+     * Start the RP-initiated logout flow if active, otherwise start a standard logout flow.
+     * Returns a post-app-logout redirect URL.
+     * Reference: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
      */
-    public function logout() {
-
-        $config = $this->config();
-        $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://';
-        }
-
-
+    public function logout(): string
+    {
+        $endSessionEndpoint = $this->config()["end_session_endpoint"];
 
-        return redirect($end_session_endpoint.'?id_token_hint='.$oidctoken."&post_logout_redirect_uri=".$protocol.$_SERVER['HTTP_HOST']."/");
+        // TODO - Add autodiscovery and false/null config value support.
 
+        $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);
     }
-
-
-
 }
index 7f599762e13d08cc3805c007b9be375465886db6..e7627f7e4c2d8a9ce4cedd5cbac357ba9b67420b 100644 (file)
@@ -71,8 +71,7 @@ class Saml2Service
                 throw $error;
             }
 
-            $this->actionLogout();
-            $url = '/';
+            $url = $this->loginService->logout();
             $id = null;
         }
 
@@ -140,20 +139,11 @@ class Saml2Service
             );
         }
 
-        $this->actionLogout();
+        $this->loginService->logout();
 
         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.
      *
index f0e0413f092dbff1ff34ce0131f72214f739dfff..c3c20587db38b0170b4366ebeda7e5ea3abb6741 100644 (file)
@@ -2,69 +2,24 @@
 
 namespace BookStack\Access;
 
-use BookStack\Auth\Access\handler;
 use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Users\Models\User;
-use Illuminate\Support\Facades\Event;
 use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\Factory as Socialite;
 use Laravel\Socialite\Contracts\Provider;
 use Laravel\Socialite\Contracts\User as SocialUser;
 use Laravel\Socialite\Two\GoogleProvider;
-use SocialiteProviders\Manager\SocialiteWasCalled;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
 class SocialAuthService
 {
-    /**
-     * The core socialite library used.
-     *
-     * @var Socialite
-     */
-    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;
+    public function __construct(
+        protected Socialite $socialite,
+        protected LoginService $loginService,
+        protected SocialDriverManager $driverManager,
+    ) {
     }
 
     /**
@@ -74,9 +29,10 @@ class SocialAuthService
      */
     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
     {
-        $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
     {
-        $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)
     {
+        $socialDriver = trim(strtolower($socialDriver));
         $socialId = $socialUser->getId();
 
         // Get any attached social accounts or users
@@ -181,76 +140,11 @@ class SocialAuthService
     }
 
     /**
-     * Ensure the social driver is correct and supported.
-     *
-     * @throws SocialDriverNotConfigured
+     * Get the social driver manager used by this service.
      */
-    protected function validateDriver(string $socialDriver): string
+    public function drivers(): SocialDriverManager
     {
-        $driver = trim(strtolower($socialDriver));
-
-        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;
+        return $this->driverManager;
     }
 
     /**
@@ -284,33 +178,8 @@ class SocialAuthService
             $driver->with(['prompt' => 'select_account']);
         }
 
-        if (isset($this->configureForRedirectCallbacks[$driverName])) {
-            $this->configureForRedirectCallbacks[$driverName]($driver);
-        }
+        $this->driverManager->getConfigureForRedirectCallback($driverName)($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;
-        }
-    }
 }
diff --git a/app/Access/SocialDriverManager.php b/app/Access/SocialDriverManager.php
new file mode 100644 (file)
index 0000000..536b2e6
--- /dev/null
@@ -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);
+    }
+}
index 0275a54891a18c70f5fa5cb852b754d94aad0f38..0f4dc55dd2fe061ba8a39d749797aa127e09b366 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\App\Providers;
 
-use BookStack\Access\SocialAuthService;
+use BookStack\Access\SocialDriverManager;
 use BookStack\Activity\Tools\ActivityLogger;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
@@ -36,7 +36,7 @@ class AppServiceProvider extends ServiceProvider
     public $singletons = [
         'activity' => ActivityLogger::class,
         SettingService::class => SettingService::class,
-        SocialAuthService::class => SocialAuthService::class,
+        SocialDriverManager::class => SocialDriverManager::class,
         CspService::class => CspService::class,
         HttpRequestService::class => HttpRequestService::class,
     ];
index 0410588b829efa449622a0e82fb3501f74d5e5c3..5f61063f63377be49bf4b16eb259b89894f82e44 100644 (file)
@@ -36,6 +36,12 @@ return [
     'authorization_endpoint' => env('OIDC_AUTH_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
     // Multiple values can be provided comma seperated.
     'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
@@ -45,11 +51,6 @@ return [
     'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
     // Attribute, within a OIDC ID token, to find group names within
     '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),
-
-    // OIDC Logout Feature: OAuth2 end_session_endpoint
-    'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', null),
-
 ];
-
index 0c252653699dfa48e8d34deab8423f4c67775a8d..94e4712176b2463acd942ff6336352b948744f55 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Theming;
 
-use BookStack\Access\SocialAuthService;
+use BookStack\Access\SocialDriverManager;
 use BookStack\Exceptions\ThemeException;
 use Illuminate\Console\Application;
 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
     {
-        $socialAuthService = app()->make(SocialAuthService::class);
-        $socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
+        $driverManager = app()->make(SocialDriverManager::class);
+        $driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
     }
 }
index 55776a7f685af7bbc08d322946238b6b50a6970a..708a91e9d4a4a6335d919bdf19d1d305f55f5a78 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Users\Controllers;
 
-use BookStack\Access\SocialAuthService;
+use BookStack\Access\SocialDriverManager;
 use BookStack\Http\Controller;
 use BookStack\Permissions\PermissionApplicator;
 use BookStack\Settings\UserNotificationPreferences;
@@ -161,7 +161,7 @@ class UserAccountController extends Controller
     /**
      * 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');
 
@@ -171,7 +171,7 @@ class UserAccountController extends Controller
             'category' => 'auth',
             'mfaMethods' => $mfaMethods,
             'authMethod' => config('auth.method'),
-            'activeSocialDrivers' => $socialAuthService->getActiveDrivers(),
+            'activeSocialDrivers' => $socialDriverManager->getActive(),
         ]);
     }
 
index 507c7cf064ebeaf793f805424fd78ce3f108e212..185d6101c61413213e757057d086da16e89b36db 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Users\Controllers;
 
-use BookStack\Access\SocialAuthService;
+use BookStack\Access\SocialDriverManager;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Exceptions\UserUpdateException;
 use BookStack\Http\Controller;
@@ -101,7 +101,7 @@ class UserController extends Controller
     /**
      * 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');
 
@@ -109,7 +109,7 @@ class UserController extends Controller
         $user->load(['apiTokens', 'mfaValues']);
         $authMethod = ($user->system_name) ? 'system' : config('auth.method');
 
-        $activeSocialDrivers = $socialAuthService->getActiveDrivers();
+        $activeSocialDrivers = $socialDriverManager->getActive();
         $mfaMethods = $user->mfaValues->groupBy('method');
         $this->setPageTitle(trans('settings.user_profile'));
         $roles = Role::query()->orderBy('display_name', 'asc')->get();
index ff28f1cfb911bdd216728d08430319a26a428f23..db4820a4d8a864827f4d7250f58721a043a8df4f 100644 (file)
         </li>
         <li><hr></li>
         <li>
-            <?php
-// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
-            if (config('auth.method') === 'oidc')  {
-                ?>
-                <form action="/oidc/logout"
-                    method="get">
-                    <?php
-// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
-                } else {
-                    ?>
-                <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
-                      method="post">
-                        <?php
-// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
-                    }
-                    ?>
-                    {{ csrf_field() }}
-                    <button class="icon-item" data-shortcut="logout">
-                        @icon('logout')
-                        <div>{{ trans('auth.logout') }}</div>
-                    </button>
-                </form>
+            @php
+                $logoutPath = match (config('auth.method')) {
+                    'saml2' => '/saml2/logout',
+                    'oidc' => '/oidc/logout',
+                    default => '/logout',
+                }
+            @endphp
+            <form action="{{ url($logoutPath) }}" method="post">
+                {{ csrf_field() }}
+                <button class="icon-item" data-shortcut="logout">
+                    @icon('logout')
+                    <div>{{ trans('auth.logout') }}</div>
+                </button>
+            </form>
         </li>
     </ul>
 </div>
\ No newline at end of file
index a02b19ca331401673597476380a61177ad2484cd..8fc90ee54c4f77f3ea93040be0407cce302fc597 100644 (file)
@@ -332,8 +332,7 @@ Route::get('/saml2/acs', [AccessControllers\Saml2Controller::class, 'processAcs'
 // OIDC routes
 Route::post('/oidc/login', [AccessControllers\OidcController::class, 'login']);
 Route::get('/oidc/callback', [AccessControllers\OidcController::class, 'callback']);
-// OIDC Logout Feature: Added to cater OIDC logout
-Route::get('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
+Route::post('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
 
 // User invitation routes
 Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);