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:
2026-03-09 20:47:21 +01:00
parent 792af5a532
commit af8ee10930
8 changed files with 106 additions and 11 deletions

View File

@@ -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
// ---------------------------------------------------------------

View File

@@ -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);
}
// ---------------------------------------------------------------