As per #3047.
Also made some SAML specific fixes:
- IDP initiated login was broken due to forced default session value.
Double checked against OneLogin lib docs that this reverted logic was fine.
- Changed how the saml login flow works to use 'withoutMiddleware' on
the route instead of hacking out the session driver. This was due to
the array driver (previously used for the hack) no longer being
considered non-persistent.
* @throws JsonDebugException
* @throws UserRegistrationException
*/
- public function processAcsResponse(string $requestId, string $samlResponse): ?User
+ public function processAcsResponse(?string $requestId, string $samlResponse): ?User
{
// The SAML2 toolkit expects the response to be within the $_POST superglobal
// so we need to manually put it back there at this point.
use BookStack\Auth\Access\Saml2Service;
use BookStack\Http\Controllers\Controller;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Cache;
-use Str;
+use Illuminate\Support\Str;
class Saml2Controller extends Controller
{
*/
public function startAcs(Request $request)
{
- // Note: This is a bit of a hack to prevent a session being stored
- // on the response of this request. Within Laravel7+ this could instead
- // be done via removing the StartSession middleware from the route.
- config()->set('session.driver', 'array');
-
$samlResponse = $request->get('SAMLResponse', null);
if (empty($samlResponse)) {
$samlResponse = decrypt(cache()->pull($cacheKey));
} catch (\Exception $exception) {
}
- $requestId = session()->pull('saml2_request_id', 'unset');
+ $requestId = session()->pull('saml2_request_id', null);
if (empty($acsId) || empty($samlResponse)) {
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
<a href="{{ $currentUser->getEditUrl() }}">@icon('edit'){{ trans('common.edit_profile') }}</a>
</li>
<li>
- @if(config('auth.method') === 'saml2')
- <a href="{{ url('/saml2/logout') }}">@icon('logout'){{ trans('auth.logout') }}</a>
- @else
- <a href="{{ url('/logout') }}">@icon('logout'){{ trans('auth.logout') }}</a>
- @endif
+ <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
+ method="post">
+ {{ csrf_field() }}
+ <button class="text-muted icon-list-item text-primary">
+ @icon('logout'){{ trans('auth.logout') }}
+ </button>
+ </form>
</li>
<li><hr></li>
<li>
// Login/Logout routes
Route::get('/login', [Auth\LoginController::class, 'getLogin']);
Route::post('/login', [Auth\LoginController::class, 'login']);
-Route::get('/logout', [Auth\LoginController::class, 'logout']);
+Route::post('/logout', [Auth\LoginController::class, 'logout']);
Route::get('/register', [Auth\RegisterController::class, 'getRegister']);
Route::get('/register/confirm', [Auth\ConfirmEmailController::class, 'show']);
Route::get('/register/confirm/awaiting', [Auth\ConfirmEmailController::class, 'showAwaiting']);
// SAML routes
Route::post('/saml2/login', [Auth\Saml2Controller::class, 'login']);
-Route::get('/saml2/logout', [Auth\Saml2Controller::class, 'logout']);
+Route::post('/saml2/logout', [Auth\Saml2Controller::class, 'logout']);
Route::get('/saml2/metadata', [Auth\Saml2Controller::class, 'metadata']);
Route::get('/saml2/sls', [Auth\Saml2Controller::class, 'sls']);
-Route::post('/saml2/acs', [Auth\Saml2Controller::class, 'startAcs']);
+Route::post('/saml2/acs', [Auth\Saml2Controller::class, 'startAcs'])->withoutMiddleware([
+ \Illuminate\Session\Middleware\StartSession::class,
+ \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \BookStack\Http\Middleware\VerifyCsrfToken::class,
+]);
Route::get('/saml2/acs', [Auth\Saml2Controller::class, 'processAcs']);
// OIDC routes
public function test_logout()
{
$this->asAdmin()->get('/')->assertOk();
- $this->get('/logout')->assertRedirect('/');
+ $this->post('/logout')->assertRedirect('/');
$this->get('/')->assertRedirect('/login');
}
$mfaSession->markVerifiedForUser($user);
$this->assertTrue($mfaSession->isVerifiedForUser($user));
- $this->asAdmin()->get('/logout');
+ $this->asAdmin()->post('/logout');
$this->assertFalse($mfaSession->isVerifiedForUser($user));
}
public function test_logout_route_functions()
{
$this->actingAs($this->getEditor());
- $this->get('/logout');
+ $this->post('/logout');
$this->assertFalse(auth()->check());
}
]);
$resp = $this->actingAs($this->getEditor())->get('/');
- $resp->assertElementExists('a[href$="/saml2/logout"]');
- $resp->assertElementContains('a[href$="/saml2/logout"]', 'Logout');
+ $resp->assertElementContains('form[action$="/saml2/logout"] button', 'Logout');
}
public function test_logout_sls_flow()
$this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
- $req = $this->get('/saml2/logout');
+ $req = $this->post('/saml2/logout');
$redirect = $req->headers->get('location');
$this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);
$this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse);
$this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$this->assertTrue($this->isAuthenticated());
- $req = $this->get('/saml2/logout');
+ $req = $this->post('/saml2/logout');
$req->assertRedirect('/');
$this->assertFalse($this->isAuthenticated());
}
public function test_saml_routes_are_only_active_if_saml_enabled()
{
config()->set(['auth.method' => 'standard']);
- $getRoutes = ['/logout', '/metadata', '/sls'];
+ $getRoutes = ['/metadata', '/sls'];
foreach ($getRoutes as $route) {
$req = $this->get('/saml2' . $route);
$this->assertPermissionError($req);
}
- $postRoutes = ['/login', '/acs'];
+ $postRoutes = ['/login', '/acs', '/logout'];
foreach ($postRoutes as $route) {
$req = $this->post('/saml2' . $route);
$this->assertPermissionError($req);
$resp = $this->post('/login');
$this->assertPermissionError($resp);
- $resp = $this->get('/logout');
+ $resp = $this->post('/logout');
$this->assertPermissionError($resp);
}