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 SocialDriverManager $socialDriverManager;
- protected LoginService $loginService;
-
- /**
- * Create a new controller instance.
- */
- public function __construct(SocialDriverManager $driverManager, 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->socialDriverManager = $driverManager;
- $this->loginService = $loginService;
}
/**
// 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,
]);
{
// 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;
}
];
foreach ($ignorePrefixList as $ignorePrefix) {
- if (strpos($previous, url($ignorePrefix)) === 0) {
+ if (str_starts_with($previous, url($ignorePrefix))) {
return;
}
}
}
/**
- * Check if login auto-initiate should be valid based upon authentication config.
+ * Check if login auto-initiate should be active based upon authentication config.
*/
- protected function shouldAutoInitiate(): bool
+ public function shouldAutoInitiate(): bool
{
+ $autoRedirect = config('auth.auto_initiate');
+ if (!$autoRedirect) {
+ return false;
+ }
+
$socialDrivers = $this->socialDriverManager->getActive();
$authMethod = config('auth.method');
- $autoRedirect = config('auth.auto_initiate');
- return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
+ return count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
}
}
public ?string $redirectUri;
public ?string $authorizationEndpoint;
public ?string $tokenEndpoint;
+ public ?string $endSessionEndpoint;
/**
* @var string[]|array[]
$discoveredSettings['keys'] = $this->filterKeys($keys);
}
+ if (!empty($result['end_session_endpoint'])) {
+ $discoveredSettings['endSessionEndpoint'] = $result['end_session_endpoint'];
+ }
+
return $discoveredSettings;
}
'redirectUri' => url('/oidc/callback'),
'authorizationEndpoint' => $config['authorization_endpoint'],
'tokenEndpoint' => $config['token_endpoint'],
+ 'endSessionEndpoint' => $config['end_session_endpoint'],
]);
// Use keys if configured
}
}
+ // Prevent use of RP-initiated logout if specifically disabled
+ if ($config['end_session_endpoint'] === false) {
+ $settings->endSessionEndpoint = null;
+ }
+
$settings->validate();
return $settings;
* 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
{
- $endSessionEndpoint = $this->config()["end_session_endpoint"];
-
- // TODO - Add autodiscovery and false/null config value support.
-
$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,
];
- return $endSessionEndpoint . '?' . http_build_query($endpointParams);
+ return $oidcSettings->endSessionEndpoint . '?' . http_build_query($endpointParams);
}
}
'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null),
- // OIDC RP-Initiated Logout endpoint
+ // OIDC RP-Initiated Logout endpoint URL.
// 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
'oidc.groups_claim' => 'group',
'oidc.remove_from_groups' => false,
'oidc.external_id_claim' => 'sub',
+ 'oidc.end_session_endpoint' => null,
]);
}
$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()
+ {
+ $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());
+ }
+
+ public function test_logout_with_autodiscovery_but_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('/');
+ }
+
+ 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('/')));
+ }
+
+ 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,
+ ]);
+
+ $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));
+ }
+
+ 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 = [];
'authorization_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/authorize',
'jwks_uri' => OidcJwtHelper::defaultIssuer() . '/oidc/keys',
'issuer' => OidcJwtHelper::defaultIssuer(),
+ 'end_session_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/logout',
], $responseOverrides)));
}