getTimestampMs(); Cache::put( $this->cacheKey((int) $user->getKey()), $revokedAt, now()->addMinutes(max(1, (int) config('session.lifetime', 120)) + 5), ); return $revokedAt; } /** * Alle anderen Sessions des Kontos werden ungültig, die übergebene * (aktuelle) bleibt angemeldet: Sie erhält einen Anmeldezeitpunkt nach * der Sperre und eine neue Session-ID (Sicherheitsaudit 07.10.2026, S-04). */ public function revokeOthersFor(User $user, Session $current): void { $revokedAt = $this->revokeAllFor($user); $current->put(self::SESSION_KEY, $revokedAt + 1); $current->migrate(true); } /** * Listener für `Login`: Zeitpunkt der Anmeldung in der Session. * Die Methode heißt bewusst nicht `handle*` (Event-Discovery). */ public function stampLogin(Login $event): void { if (! app()->bound('request') || ! request()->hasSession()) { return; } request()->session()->put(self::SESSION_KEY, now()->getTimestampMs()); } /** * Session stammt aus der Zeit vor der letzten Sperre dieses Kontos. * Ohne Anmeldezeitpunkt (Session von vor dem Deploy) gilt sie als alt. */ public function isRevoked(int $userId, Session $session): bool { $revokedAt = Cache::get($this->cacheKey($userId)); if ($revokedAt === null) { return false; } return (int) $session->get(self::SESSION_KEY, 0) < (int) $revokedAt; } private function cacheKey(int $userId): string { return 'auth.sessions-revoked-at.'.$userId; } }