feat(auth): keep remember tokens on session timeout
- add timeout-specific logout path without remember-token revocation - reset session timers after remember-me auto login - compact session policy wording in admin settings (DE/EN) - extend auth tests for timeout/manual logout behavior
This commit is contained in:
@@ -1018,10 +1018,10 @@
|
||||
"Counters": "Zähler",
|
||||
"User lifecycle policy": "Benutzer-Lifecycle-Regel",
|
||||
"Session policy": "Sitzungsrichtlinie",
|
||||
"Session limits define how long logged-in users stay signed in.": "Sitzungslimits bestimmen, wie lange angemeldete Benutzer eingeloggt bleiben.",
|
||||
"Idle timeout signs users out after no activity.": "Das Inaktivitäts-Timeout meldet Benutzer bei fehlender Aktivität ab.",
|
||||
"Session limits define how long logged-in users stay signed in.": "Hier legen Sie Inaktivitäts- und Absolute-Timeout für aktive Sitzungen fest.",
|
||||
"Idle timeout signs users out after no activity.": "Inaktivitäts-Timeout beendet inaktive Sitzungen, absolutes Timeout beendet lange Sitzungen auch bei Aktivität.",
|
||||
"Absolute timeout signs users out after total session duration, even with activity.": "Das absolute Timeout meldet Benutzer nach der gesamten Sitzungsdauer ab, auch bei Aktivität.",
|
||||
"New limits apply on each user's next request.": "Neue Limits gelten mit der nächsten Anfrage jedes Benutzers.",
|
||||
"New limits apply on each user's next request.": "Änderungen gelten ab der nächsten Anfrage je Benutzer. Angemeldet-bleiben-Tokens bleiben aktiv.",
|
||||
"Session idle timeout (minutes)": "Sitzungs-Inaktivitäts-Timeout (Minuten)",
|
||||
"Session absolute timeout (hours)": "Absolutes Sitzungs-Timeout (Stunden)",
|
||||
"Allowed range: 5-1440 minutes (default: 30)": "Erlaubter Bereich: 5-1440 Minuten (Standard: 30)",
|
||||
@@ -1218,7 +1218,7 @@
|
||||
"Top lifecycle reason codes (7d)": "Top-Lifecycle-Fehlercodes (7d)",
|
||||
"Recent lifecycle risks (7d)": "Aktuelle Lifecycle-Risiken (7d)",
|
||||
"Login progress": "Anmeldefortschritt",
|
||||
"Your session has expired. Please log in again.": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
"Your session has expired. Please log in again.": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an. Mit „Angemeldet bleiben“ erfolgt die Anmeldung ggf. automatisch.",
|
||||
"Failed to update role permissions": "Rollenberechtigungen konnten nicht aktualisiert werden",
|
||||
"Failed to update permission roles": "Berechtigungsrollen konnten nicht aktualisiert werden"
|
||||
}
|
||||
|
||||
@@ -1018,10 +1018,10 @@
|
||||
"Counters": "Counters",
|
||||
"User lifecycle policy": "User lifecycle policy",
|
||||
"Session policy": "Session policy",
|
||||
"Session limits define how long logged-in users stay signed in.": "Session limits define how long logged-in users stay signed in.",
|
||||
"Idle timeout signs users out after no activity.": "Idle timeout signs users out after no activity.",
|
||||
"Session limits define how long logged-in users stay signed in.": "Set idle and absolute limits for active sessions.",
|
||||
"Idle timeout signs users out after no activity.": "Idle timeout ends inactive sessions; absolute timeout ends long sessions even with activity.",
|
||||
"Absolute timeout signs users out after total session duration, even with activity.": "Absolute timeout signs users out after total session duration, even with activity.",
|
||||
"New limits apply on each user's next request.": "New limits apply on each user's next request.",
|
||||
"New limits apply on each user's next request.": "Changes apply from each user's next request. Remember-me tokens stay active.",
|
||||
"Session idle timeout (minutes)": "Session idle timeout (minutes)",
|
||||
"Session absolute timeout (hours)": "Session absolute timeout (hours)",
|
||||
"Allowed range: 5-1440 minutes (default: 30)": "Allowed range: 5-1440 minutes (default: 30)",
|
||||
@@ -1218,7 +1218,7 @@
|
||||
"Top lifecycle reason codes (7d)": "Top lifecycle reason codes (7d)",
|
||||
"Recent lifecycle risks (7d)": "Recent lifecycle risks (7d)",
|
||||
"Login progress": "Login progress",
|
||||
"Your session has expired. Please log in again.": "Your session has expired. Please log in again.",
|
||||
"Your session has expired. Please log in again.": "Your session timed out. Please sign in again. If you chose Remember me, sign-in can happen automatically.",
|
||||
"Failed to update role permissions": "Failed to update role permissions",
|
||||
"Failed to update permission roles": "Failed to update permission roles"
|
||||
}
|
||||
|
||||
@@ -268,6 +268,19 @@ class AuthService
|
||||
]);
|
||||
}
|
||||
|
||||
public function logoutDueToSessionTimeout(): void
|
||||
{
|
||||
$actorUserId = (int) ($this->sessionUser()['id'] ?? 0);
|
||||
$actorTenantId = $this->currentTenantIdFromSession();
|
||||
$this->sessionStore->remove('session_started_at');
|
||||
$this->sessionStore->remove('session_last_activity');
|
||||
Auth::logout();
|
||||
$this->recordAuthEvent('auth.session.timeout', 'success', [
|
||||
'actor_user_id' => $actorUserId > 0 ? $actorUserId : null,
|
||||
'actor_tenant_id' => $actorTenantId > 0 ? $actorTenantId : null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function logoutAndRedirect(string $target = 'login'): void
|
||||
{
|
||||
$this->logout();
|
||||
|
||||
@@ -92,6 +92,9 @@ class RememberMeService
|
||||
|
||||
Session::regenerate();
|
||||
$this->sessionStore->set('user', $user);
|
||||
$now = time();
|
||||
$this->sessionStore->set('session_started_at', $now);
|
||||
$this->sessionStore->set('session_last_activity', $now);
|
||||
if (!empty($user['locale'])) {
|
||||
I18n::$locale = (string) $user['locale'];
|
||||
}
|
||||
|
||||
@@ -334,7 +334,6 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
<?php e(t('Session limits define how long logged-in users stay signed in.')); ?>
|
||||
</blockquote>
|
||||
<small class="muted"><?php e(t('Idle timeout signs users out after no activity.')); ?></small>
|
||||
<small class="muted"><?php e(t('Absolute timeout signs users out after total session duration, even with activity.')); ?></small>
|
||||
<small class="muted"><?php e(t('New limits apply on each user\'s next request.')); ?></small>
|
||||
<div class="grid">
|
||||
<label class="app-field">
|
||||
|
||||
@@ -349,6 +349,80 @@ class AuthServiceTest extends TestCase
|
||||
$this->assertSame('login_not_verified', $result['flash_key']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// logout behavior
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testLogoutDueToSessionTimeoutKeepsRememberTokenAndClearsSessionTimers(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$removedKeys = [];
|
||||
$session = $this->createMock(SessionStoreInterface::class);
|
||||
$session->method('get')->willReturnMap([
|
||||
['user', [], ['id' => 5]],
|
||||
['current_tenant', [], ['id' => 9]],
|
||||
]);
|
||||
$session->method('remove')->willReturnCallback(function (string $key) use (&$removedKeys): void {
|
||||
$removedKeys[] = $key;
|
||||
});
|
||||
|
||||
$rememberMeService = $this->createMock(RememberMeService::class);
|
||||
$rememberMeService->expects($this->never())->method('forgetCurrentUser');
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())->method('record')
|
||||
->with('auth.session.timeout', 'success', $this->callback(function (array $context): bool {
|
||||
return (int) ($context['actor_user_id'] ?? 0) === 5
|
||||
&& (int) ($context['actor_tenant_id'] ?? 0) === 9;
|
||||
}));
|
||||
|
||||
$service = $this->newService(
|
||||
rememberMeService: $rememberMeService,
|
||||
systemAuditService: $audit,
|
||||
sessionStore: $session
|
||||
);
|
||||
|
||||
$service->logoutDueToSessionTimeout();
|
||||
sort($removedKeys);
|
||||
$this->assertSame(['session_last_activity', 'session_started_at'], $removedKeys);
|
||||
}
|
||||
|
||||
public function testLogoutStillForgetsRememberTokenOnManualLogout(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$rememberMeService = $this->createMock(RememberMeService::class);
|
||||
$rememberMeService->expects($this->once())->method('forgetCurrentUser');
|
||||
|
||||
$session = $this->createMock(SessionStoreInterface::class);
|
||||
$session->method('get')->willReturnMap([
|
||||
['user', [], ['id' => 5]],
|
||||
['current_tenant', [], ['id' => 11]],
|
||||
]);
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())->method('record')
|
||||
->with('auth.logout', 'success', $this->callback(function (array $context): bool {
|
||||
return (int) ($context['actor_user_id'] ?? 0) === 5
|
||||
&& (int) ($context['actor_tenant_id'] ?? 0) === 11;
|
||||
}));
|
||||
|
||||
$service = $this->newService(
|
||||
rememberMeService: $rememberMeService,
|
||||
systemAuditService: $audit,
|
||||
sessionStore: $session
|
||||
);
|
||||
|
||||
$service->logout();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helper
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -273,11 +273,14 @@ class RememberMeServiceTest extends TestCase
|
||||
$_SESSION = [];
|
||||
|
||||
$session = $this->createMock(SessionStoreInterface::class);
|
||||
$setKeys = [];
|
||||
$session->method('get')->willReturnMap([
|
||||
['user', [], []],
|
||||
['no_active_tenant', false, false],
|
||||
]);
|
||||
$session->expects($this->once())->method('set');
|
||||
$session->method('set')->willReturnCallback(function (string $key, mixed $value) use (&$setKeys): void {
|
||||
$setKeys[] = $key;
|
||||
});
|
||||
|
||||
$cookie = $this->createMock(CookieStoreInterface::class);
|
||||
$cookie->method('get')->willReturn('selector:token');
|
||||
@@ -318,6 +321,9 @@ class RememberMeServiceTest extends TestCase
|
||||
authSessionTenantContextService: $tenantCtx
|
||||
);
|
||||
$this->assertTrue($service->autoLoginFromCookie());
|
||||
$this->assertContains('user', $setKeys);
|
||||
$this->assertContains('session_started_at', $setKeys);
|
||||
$this->assertContains('session_last_activity', $setKeys);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -79,7 +79,7 @@ if (!empty($_SESSION['user']['id'])) {
|
||||
$isAbsolute = ($now - $sessionStartedAt) > $absoluteTimeoutSeconds;
|
||||
|
||||
if ($isIdle || $isAbsolute) {
|
||||
$authService->logout();
|
||||
$authService->logoutDueToSessionTimeout();
|
||||
Flash::error(t('Your session has expired. Please log in again.'), 'login', 'session_timeout');
|
||||
Router::redirect('login');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user