Stop the console logout from destroying a shared portal session
Where the console and the portal share a host (shared/fallback mode — this VM's own mode), both guards keep their login key in the SAME session. session()->invalidate() is flush() + migrate(true) — it discards every attribute in the session, not only the operator's, so signing out of the console silently signed a customer out of the portal too if the same browser carried both. Replaced with session()->regenerate(): a new session id, so there is no fixation risk, while attributes belonging to any other guard survive untouched.feat/operator-identity
parent
5e22e16291
commit
2b2bb439a5
|
|
@ -44,7 +44,17 @@ Route::get('/settings', Admin\Settings::class)->name('settings');
|
||||||
// POST so Laravel's CSRF middleware protects it, like every other state change.
|
// POST so Laravel's CSRF middleware protects it, like every other state change.
|
||||||
Route::post('/logout', function () {
|
Route::post('/logout', function () {
|
||||||
Auth::guard('operator')->logout();
|
Auth::guard('operator')->logout();
|
||||||
session()->invalidate();
|
|
||||||
|
// NOT session()->invalidate(): where the console and the portal share a
|
||||||
|
// host (shared/fallback mode — this VM's own mode), both guards keep
|
||||||
|
// their login key in the SAME session. invalidate() is flush() +
|
||||||
|
// migrate(true) — it discards every attribute in the session, not just
|
||||||
|
// the operator's, so it would silently sign a customer out of the portal
|
||||||
|
// too if the same browser happened to carry both. regenerate() issues a
|
||||||
|
// new session id (no fixation risk) without touching the other
|
||||||
|
// attributes, so the 'web' guard's own login key — already removed for
|
||||||
|
// 'operator' by the logout() call above — survives untouched.
|
||||||
|
session()->regenerate();
|
||||||
session()->regenerateToken();
|
session()->regenerateToken();
|
||||||
|
|
||||||
return redirect()->route('admin.login');
|
return redirect()->route('admin.login');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Operator;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
it('signs the operator out without destroying a portal session sharing the same browser', function () {
|
||||||
|
// Shared/fallback mode (this suite's default, ADMIN_HOSTS="" — also this
|
||||||
|
// VM's own mode) is exactly where this bites: the console and the portal
|
||||||
|
// answer on the same host, so both guards keep their login key in ONE
|
||||||
|
// session. A customer signed in beforehand, in the same browser, must
|
||||||
|
// not be silently logged out just because an operator elsewhere in that
|
||||||
|
// same session signs out of the console.
|
||||||
|
//
|
||||||
|
// Auth::guard(...)->login() rather than actingAs(): actingAs() only sets
|
||||||
|
// an in-memory user on the guard object, it never writes to the session
|
||||||
|
// — so it cannot tell session()->invalidate() and session()->regenerate()
|
||||||
|
// apart. login() does the same session write a real sign-in performs.
|
||||||
|
$operator = Operator::factory()->role('Owner')->create();
|
||||||
|
$customer = User::factory()->create();
|
||||||
|
|
||||||
|
Auth::guard('web')->login($customer);
|
||||||
|
Auth::guard('operator')->login($operator);
|
||||||
|
|
||||||
|
$this->post(route('admin.logout'))->assertRedirect(route('admin.login'));
|
||||||
|
|
||||||
|
// Auth::guard('web') is still the SAME cached guard instance from the
|
||||||
|
// login() call above — its check() would say "authenticated" from its
|
||||||
|
// own in-memory $user property regardless of what happened to the
|
||||||
|
// session, which is exactly the blind spot that would make this
|
||||||
|
// assertion pass whether the fix is there or not. forgetGuards() drops
|
||||||
|
// that cache, forcing the next guard() call to rebuild from the CURRENT
|
||||||
|
// session data — the same thing a genuinely new request would do.
|
||||||
|
Auth::forgetGuards();
|
||||||
|
|
||||||
|
expect(Auth::guard('operator')->check())->toBeFalse()
|
||||||
|
->and(Auth::guard('web')->check())->toBeTrue()
|
||||||
|
->and(Auth::guard('web')->id())->toBe($customer->id);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue