Merge pull request #4714 from BookStackApp/oidc_logout
OIDC RP-Initiated logout
This commit is contained in:
		
						commit
						4c0b7f3123
					
				| 
						 | 
				
			
			@ -273,6 +273,7 @@ OIDC_USER_TO_GROUPS=false
 | 
			
		|||
OIDC_GROUPS_CLAIM=groups
 | 
			
		||||
OIDC_REMOVE_FROM_GROUPS=false
 | 
			
		||||
OIDC_EXTERNAL_ID_CLAIM=sub
 | 
			
		||||
OIDC_END_SESSION_ENDPOINT=false
 | 
			
		||||
 | 
			
		||||
# Disable default third-party services such as Gravatar and Draw.IO
 | 
			
		||||
# Service-specific options will override this option
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,34 +3,26 @@
 | 
			
		|||
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;
 | 
			
		||||
use BookStack\Http\Controller;
 | 
			
		||||
use Illuminate\Http\RedirectResponse;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Auth;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
 | 
			
		||||
class LoginController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    use ThrottlesLogins;
 | 
			
		||||
 | 
			
		||||
    protected SocialAuthService $socialAuthService;
 | 
			
		||||
    protected LoginService $loginService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new controller instance.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
 | 
			
		||||
    {
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        protected SocialDriverManager $socialDriverManager,
 | 
			
		||||
        protected 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->loginService = $loginService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +30,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';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -52,7 +44,7 @@ class LoginController extends Controller
 | 
			
		|||
        // Store the previous location for redirect after login
 | 
			
		||||
        $this->updateIntendedFromPrevious();
 | 
			
		||||
 | 
			
		||||
        if (!$preventInitiation && $this->shouldAutoInitiate()) {
 | 
			
		||||
        if (!$preventInitiation && $this->loginService->shouldAutoInitiate()) {
 | 
			
		||||
            return view('auth.login-initiate', [
 | 
			
		||||
                'authMethod'    => $authMethod,
 | 
			
		||||
            ]);
 | 
			
		||||
| 
						 | 
				
			
			@ -101,15 +93,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());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +186,7 @@ class LoginController extends Controller
 | 
			
		|||
    {
 | 
			
		||||
        // Store the previous location for redirect after login
 | 
			
		||||
        $previous = url()->previous('');
 | 
			
		||||
        $isPreviousFromInstance = (strpos($previous, url('/')) === 0);
 | 
			
		||||
        $isPreviousFromInstance = str_starts_with($previous, url('/'));
 | 
			
		||||
        if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -211,23 +197,11 @@ class LoginController extends Controller
 | 
			
		|||
        ];
 | 
			
		||||
 | 
			
		||||
        foreach ($ignorePrefixList as $ignorePrefix) {
 | 
			
		||||
            if (strpos($previous, url($ignorePrefix)) === 0) {
 | 
			
		||||
            if (str_starts_with($previous, url($ignorePrefix))) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,6 @@ class OidcController extends Controller
 | 
			
		|||
{
 | 
			
		||||
    protected OidcService $oidcService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * OpenIdController constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(OidcService $oidcService)
 | 
			
		||||
    {
 | 
			
		||||
        $this->oidcService = $oidcService;
 | 
			
		||||
| 
						 | 
				
			
			@ -63,4 +60,12 @@ class OidcController extends Controller
 | 
			
		|||
 | 
			
		||||
        return redirect()->intended();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log the user out then start the OIDC RP-initiated logout process.
 | 
			
		||||
     */
 | 
			
		||||
    public function logout()
 | 
			
		||||
    {
 | 
			
		||||
        return redirect($this->oidcService->logout());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,33 @@ 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 active based upon authentication config.
 | 
			
		||||
     */
 | 
			
		||||
    public function shouldAutoInitiate(): bool
 | 
			
		||||
    {
 | 
			
		||||
        $autoRedirect = config('auth.auto_initiate');
 | 
			
		||||
        if (!$autoRedirect) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $socialDrivers = $this->socialDriverManager->getActive();
 | 
			
		||||
        $authMethod = config('auth.method');
 | 
			
		||||
 | 
			
		||||
        return count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ class OidcProviderSettings
 | 
			
		|||
    public ?string $redirectUri;
 | 
			
		||||
    public ?string $authorizationEndpoint;
 | 
			
		||||
    public ?string $tokenEndpoint;
 | 
			
		||||
    public ?string $endSessionEndpoint;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var string[]|array[]
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +133,10 @@ class OidcProviderSettings
 | 
			
		|||
            $discoveredSettings['keys'] = $this->filterKeys($keys);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!empty($result['end_session_endpoint'])) {
 | 
			
		||||
            $discoveredSettings['endSessionEndpoint'] = $result['end_session_endpoint'];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $discoveredSettings;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,6 +84,7 @@ class OidcService
 | 
			
		|||
            'redirectUri'           => url('/oidc/callback'),
 | 
			
		||||
            'authorizationEndpoint' => $config['authorization_endpoint'],
 | 
			
		||||
            'tokenEndpoint'         => $config['token_endpoint'],
 | 
			
		||||
            'endSessionEndpoint'    => is_string($config['end_session_endpoint']) ? $config['end_session_endpoint'] : null,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Use keys if configured
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +101,14 @@ class OidcService
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Prevent use of RP-initiated logout if specifically disabled
 | 
			
		||||
        // Or force use of a URL if specifically set.
 | 
			
		||||
        if ($config['end_session_endpoint'] === false) {
 | 
			
		||||
            $settings->endSessionEndpoint = null;
 | 
			
		||||
        } else if (is_string($config['end_session_endpoint'])) {
 | 
			
		||||
            $settings->endSessionEndpoint = $config['end_session_endpoint'];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $settings->validate();
 | 
			
		||||
 | 
			
		||||
        return $settings;
 | 
			
		||||
| 
						 | 
				
			
			@ -217,6 +226,8 @@ class OidcService
 | 
			
		|||
            $settings->keys,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        session()->put("oidc_id_token", $idTokenText);
 | 
			
		||||
 | 
			
		||||
        $returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
 | 
			
		||||
            'access_token' => $accessToken->getToken(),
 | 
			
		||||
            'expires_in' => $accessToken->getExpires(),
 | 
			
		||||
| 
						 | 
				
			
			@ -284,4 +295,30 @@ class OidcService
 | 
			
		|||
    {
 | 
			
		||||
        return $this->config()['user_to_groups'] !== false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     * @throws OidcException
 | 
			
		||||
     */
 | 
			
		||||
    public function logout(): string
 | 
			
		||||
    {
 | 
			
		||||
        $oidcToken = session()->pull("oidc_id_token");
 | 
			
		||||
        $defaultLogoutUrl = url($this->loginService->logout());
 | 
			
		||||
        $oidcSettings = $this->getProviderSettings();
 | 
			
		||||
 | 
			
		||||
        if (!$oidcSettings->endSessionEndpoint) {
 | 
			
		||||
            return $defaultLogoutUrl;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $endpointParams = [
 | 
			
		||||
            'id_token_hint' => $oidcToken,
 | 
			
		||||
            'post_logout_redirect_uri' => $defaultLogoutUrl,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $joiner = str_contains($oidcSettings->endSessionEndpoint, '?') ? '&' : '?';
 | 
			
		||||
 | 
			
		||||
        return $oidcSettings->endSessionEndpoint . $joiner . http_build_query($endpointParams);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
    ];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,12 @@ return [
 | 
			
		|||
    'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
 | 
			
		||||
    'token_endpoint'         => env('OIDC_TOKEN_ENDPOINT', null),
 | 
			
		||||
 | 
			
		||||
    // OIDC RP-Initiated Logout endpoint URL.
 | 
			
		||||
    // A false value force-disables RP-Initiated Logout.
 | 
			
		||||
    // A true value gets the URL from discovery, if active.
 | 
			
		||||
    // A string value is used as the URL.
 | 
			
		||||
    'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', false),
 | 
			
		||||
 | 
			
		||||
    // 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,6 +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),
 | 
			
		||||
];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,8 +29,14 @@
 | 
			
		|||
        </li>
 | 
			
		||||
        <li><hr></li>
 | 
			
		||||
        <li>
 | 
			
		||||
            <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
 | 
			
		||||
                  method="post">
 | 
			
		||||
            @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')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -332,6 +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']);
 | 
			
		||||
Route::post('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
 | 
			
		||||
 | 
			
		||||
// User invitation routes
 | 
			
		||||
Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ class OidcTest extends TestCase
 | 
			
		|||
            'oidc.groups_claim'           => 'group',
 | 
			
		||||
            'oidc.remove_from_groups'     => false,
 | 
			
		||||
            'oidc.external_id_claim'      => 'sub',
 | 
			
		||||
            'oidc.end_session_endpoint'   => false,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -478,6 +479,128 @@ class OidcTest extends TestCase
 | 
			
		|||
        $this->assertTrue($user->hasRole($roleA->id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_oidc_logout_form_active_when_oidc_active()
 | 
			
		||||
    {
 | 
			
		||||
        $this->runLogin();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->get('/');
 | 
			
		||||
        $this->withHtml($resp)->assertElementExists('header form[action$="/oidc/logout"] button');
 | 
			
		||||
    }
 | 
			
		||||
    public function test_logout_with_autodiscovery_with_oidc_logout_enabled()
 | 
			
		||||
    {
 | 
			
		||||
        config()->set(['oidc.end_session_endpoint' => true]);
 | 
			
		||||
        $this->withAutodiscovery();
 | 
			
		||||
 | 
			
		||||
        $transactions = $this->mockHttpClient([
 | 
			
		||||
            $this->getAutoDiscoveryResponse(),
 | 
			
		||||
            $this->getJwksResponse(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
        $resp->assertRedirect('https://auth.example.com/oidc/logout?post_logout_redirect_uri=' . urlencode(url('/')));
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(2, $transactions->requestCount());
 | 
			
		||||
        $this->assertFalse(auth()->check());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_logout_with_autodiscovery_with_oidc_logout_disabled()
 | 
			
		||||
    {
 | 
			
		||||
        $this->withAutodiscovery();
 | 
			
		||||
        config()->set(['oidc.end_session_endpoint' => false]);
 | 
			
		||||
 | 
			
		||||
        $this->mockHttpClient([
 | 
			
		||||
            $this->getAutoDiscoveryResponse(),
 | 
			
		||||
            $this->getJwksResponse(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
        $resp->assertRedirect('/');
 | 
			
		||||
        $this->assertFalse(auth()->check());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_logout_without_autodiscovery_but_with_endpoint_configured()
 | 
			
		||||
    {
 | 
			
		||||
        config()->set(['oidc.end_session_endpoint' => 'https://example.com/logout']);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
        $resp->assertRedirect('https://example.com/logout?post_logout_redirect_uri=' . urlencode(url('/')));
 | 
			
		||||
        $this->assertFalse(auth()->check());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_logout_without_autodiscovery_with_configured_endpoint_adds_to_query_if_existing()
 | 
			
		||||
    {
 | 
			
		||||
        config()->set(['oidc.end_session_endpoint' => 'https://example.com/logout?a=b']);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
        $resp->assertRedirect('https://example.com/logout?a=b&post_logout_redirect_uri=' . urlencode(url('/')));
 | 
			
		||||
        $this->assertFalse(auth()->check());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_logout_with_autodiscovery_and_auto_initiate_returns_to_auto_prevented_login()
 | 
			
		||||
    {
 | 
			
		||||
        $this->withAutodiscovery();
 | 
			
		||||
        config()->set([
 | 
			
		||||
            'auth.auto_initiate' => true,
 | 
			
		||||
            'services.google.client_id' => false,
 | 
			
		||||
            'services.github.client_id' => false,
 | 
			
		||||
            'oidc.end_session_endpoint' => true,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->mockHttpClient([
 | 
			
		||||
            $this->getAutoDiscoveryResponse(),
 | 
			
		||||
            $this->getJwksResponse(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
 | 
			
		||||
        $redirectUrl = url('/login?prevent_auto_init=true');
 | 
			
		||||
        $resp->assertRedirect('https://auth.example.com/oidc/logout?post_logout_redirect_uri=' . urlencode($redirectUrl));
 | 
			
		||||
        $this->assertFalse(auth()->check());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_logout_endpoint_url_overrides_autodiscovery_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        config()->set(['oidc.end_session_endpoint' => 'https://a.example.com']);
 | 
			
		||||
        $this->withAutodiscovery();
 | 
			
		||||
 | 
			
		||||
        $transactions = $this->mockHttpClient([
 | 
			
		||||
            $this->getAutoDiscoveryResponse(),
 | 
			
		||||
            $this->getJwksResponse(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
        $resp->assertRedirect('https://a.example.com?post_logout_redirect_uri=' . urlencode(url('/')));
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(2, $transactions->requestCount());
 | 
			
		||||
        $this->assertFalse(auth()->check());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_logout_with_autodiscovery_does_not_use_rp_logout_if_no_url_via_autodiscovery()
 | 
			
		||||
    {
 | 
			
		||||
        config()->set(['oidc.end_session_endpoint' => true]);
 | 
			
		||||
        $this->withAutodiscovery();
 | 
			
		||||
 | 
			
		||||
        $this->mockHttpClient([
 | 
			
		||||
            $this->getAutoDiscoveryResponse(['end_session_endpoint' => null]),
 | 
			
		||||
            $this->getJwksResponse(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
        $resp->assertRedirect('/');
 | 
			
		||||
        $this->assertFalse(auth()->check());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_logout_redirect_contains_id_token_hint_if_existing()
 | 
			
		||||
    {
 | 
			
		||||
        config()->set(['oidc.end_session_endpoint' => 'https://example.com/logout']);
 | 
			
		||||
 | 
			
		||||
        $this->runLogin();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post('/oidc/logout');
 | 
			
		||||
        $query = 'id_token_hint=' . urlencode(OidcJwtHelper::idToken()) .  '&post_logout_redirect_uri=' . urlencode(url('/'));
 | 
			
		||||
        $resp->assertRedirect('https://example.com/logout?' . $query);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_oidc_id_token_pre_validate_theme_event_without_return()
 | 
			
		||||
    {
 | 
			
		||||
        $args = [];
 | 
			
		||||
| 
						 | 
				
			
			@ -563,6 +686,7 @@ class OidcTest extends TestCase
 | 
			
		|||
            'authorization_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/authorize',
 | 
			
		||||
            'jwks_uri'               => OidcJwtHelper::defaultIssuer() . '/oidc/keys',
 | 
			
		||||
            'issuer'                 => OidcJwtHelper::defaultIssuer(),
 | 
			
		||||
            'end_session_endpoint'   => OidcJwtHelper::defaultIssuer() . '/oidc/logout',
 | 
			
		||||
        ], $responseOverrides)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
 | 
			
		||||
namespace Tests;
 | 
			
		||||
 | 
			
		||||
use BookStack\Access\SocialAuthService;
 | 
			
		||||
use BookStack\Access\SocialDriverManager;
 | 
			
		||||
use Illuminate\Testing\TestResponse;
 | 
			
		||||
 | 
			
		||||
class DebugViewTest extends TestCase
 | 
			
		||||
| 
						 | 
				
			
			@ -46,8 +46,8 @@ class DebugViewTest extends TestCase
 | 
			
		|||
    protected function getDebugViewForException(\Exception $exception): TestResponse
 | 
			
		||||
    {
 | 
			
		||||
        // Fake an error via social auth service used on login page
 | 
			
		||||
        $mockService = $this->mock(SocialAuthService::class);
 | 
			
		||||
        $mockService->shouldReceive('getActiveDrivers')->andThrow($exception);
 | 
			
		||||
        $mockService = $this->mock(SocialDriverManager::class);
 | 
			
		||||
        $mockService->shouldReceive('getActive')->andThrow($exception);
 | 
			
		||||
 | 
			
		||||
        return $this->get('/login');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue