Skip to content

OIDC RP-Initiated logout #4714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example.complete
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 11 additions & 37 deletions app/Access/Controllers/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,34 @@
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;
}

/**
* Show the application login form.
*/
public function getLogin(Request $request)
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$socialDrivers = $this->socialDriverManager->getActive();
$authMethod = config('auth.method');
$preventInitiation = $request->get('prevent_auto_init') === 'true';

Expand All @@ -52,7 +44,7 @@ public function getLogin(Request $request)
// 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,
]);
Expand Down Expand Up @@ -101,15 +93,9 @@ public function login(Request $request)
/**
* 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());
}

/**
Expand Down Expand Up @@ -200,7 +186,7 @@ protected function updateIntendedFromPrevious(): void
{
// 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;
}
Expand All @@ -211,23 +197,11 @@ protected function updateIntendedFromPrevious(): void
];

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 changes: 8 additions & 3 deletions app/Access/Controllers/OidcController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ class OidcController extends Controller
{
protected OidcService $oidcService;

/**
* OpenIdController constructor.
*/
public function __construct(OidcService $oidcService)
{
$this->oidcService = $oidcService;
Expand Down Expand Up @@ -63,4 +60,12 @@ public function callback(Request $request)

return redirect()->intended();
}

/**
* Log the user out then start the OIDC RP-initiated logout process.
*/
public function logout()
{
return redirect($this->oidcService->logout());
}
}
10 changes: 5 additions & 5 deletions app/Access/Controllers/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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;
Expand All @@ -15,22 +15,22 @@

class RegisterController extends Controller
{
protected SocialAuthService $socialAuthService;
protected SocialDriverManager $socialDriverManager;
protected RegistrationService $registrationService;
protected LoginService $loginService;

/**
* 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;
}
Expand All @@ -43,7 +43,7 @@ public function __construct(
public function getRegister()
{
$this->registrationService->ensureRegistrationAllowed();
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$socialDrivers = $this->socialDriverManager->getActive();

return view('auth.register', [
'socialDrivers' => $socialDrivers,
Expand Down
4 changes: 2 additions & 2 deletions app/Access/Controllers/SocialController.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public function callback(Request $request, string $socialDriver)
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);
}

Expand Down Expand Up @@ -114,7 +114,7 @@ protected function socialRegisterCallback(string $socialDriver, SocialUser $soci
{
$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 = [
Expand Down
41 changes: 34 additions & 7 deletions app/Access/LoginService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}

/**
Expand Down Expand Up @@ -163,4 +161,33 @@ public function attempt(array $credentials, string $method, bool $remember = fal

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']);
}
}
5 changes: 5 additions & 0 deletions app/Access/Oidc/OidcProviderSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class OidcProviderSettings
public ?string $redirectUri;
public ?string $authorizationEndpoint;
public ?string $tokenEndpoint;
public ?string $endSessionEndpoint;

/**
* @var string[]|array[]
Expand Down Expand Up @@ -132,6 +133,10 @@ protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient):
$discoveredSettings['keys'] = $this->filterKeys($keys);
}

if (!empty($result['end_session_endpoint'])) {
$discoveredSettings['endSessionEndpoint'] = $result['end_session_endpoint'];
}

return $discoveredSettings;
}

Expand Down
37 changes: 37 additions & 0 deletions app/Access/Oidc/OidcService.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ protected function getProviderSettings(): OidcProviderSettings
'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
Expand All @@ -100,6 +101,14 @@ protected function getProviderSettings(): OidcProviderSettings
}
}

// 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;
Expand Down Expand Up @@ -217,6 +226,8 @@ protected function processAccessTokenCallback(OidcAccessToken $accessToken, Oidc
$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(),
Expand Down Expand Up @@ -284,4 +295,30 @@ protected function shouldSyncGroups(): bool
{
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);
}
}
14 changes: 2 additions & 12 deletions app/Access/Saml2Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ public function logout(User $user): array
throw $error;
}

$this->actionLogout();
$url = '/';
$url = $this->loginService->logout();
$id = null;
}

Expand Down Expand Up @@ -140,20 +139,11 @@ public function processSlsResponse(?string $requestId): ?string
);
}

$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.
*
Expand Down
Loading