Idle session timeout
Sign out users who go AFK
For higher-security environments (financial, healthcare), sessions shouldn't last forever. After N minutes of inactivity, sign out the user.
Different from session expiry
- Absolute expiry: session valid for X hours regardless. Kratos default: 24h.
- Idle expiry: session expires after Y minutes of inactivity. Resets on each action.
Both useful, often combined: "logged out after 4h absolute OR 30 min idle."
Server-side enforcement
Kratos's session has a fixed expiry. To add idle:
// On every request
async function checkIdleSession(req) {
const session = await kratos.toSession(req);
if (!session) return null;
const lastActivity = session.metadata?.last_activity ?? session.created_at;
const idleMinutes = (Date.now() - new Date(lastActivity).getTime()) / 60_000;
if (idleMinutes > IDLE_TIMEOUT_MINUTES) {
await kratos.adminRevokeSession(session.id);
return null;
}
// Update activity
await kratos.adminUpdateSession(session.id, { metadata: { last_activity: new Date().toISOString() } });
return session;
}Server tracks last activity. Auto-revokes after idle.
Client-side warning
Show user a warning before session ends:
"use client";
useEffect(() => {
let idleSeconds = 0;
const reset = () => idleSeconds = 0;
document.addEventListener("mousemove", reset);
document.addEventListener("keypress", reset);
const interval = setInterval(() => {
idleSeconds++;
if (idleSeconds === IDLE_TIMEOUT_SECONDS - 60) {
showWarningModal(); // "1 minute left"
}
if (idleSeconds >= IDLE_TIMEOUT_SECONDS) {
forceLogout();
}
}, 1000);
return () => clearInterval(interval);
}, []);User clicks "Continue" in modal → reset.
Warning modal
<Modal>
<h2>Are you still here?</h2>
<p>You'll be signed out in {countdown} seconds for security.</p>
<Button onClick={extend}>I'm here, keep me signed in</Button>
<Button variant="ghost" onClick={signOut}>Sign me out</Button>
</Modal>UX-friendly. Don't suddenly log them out without warning.
Tab visibility
Don't penalize background tabs unfairly:
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
// Just returned to tab. Reset idle.
idleSeconds = 0;
}
});User Alt-Tab'd; that doesn't mean they're AFK.
Per-app vs global
If user has many tabs open:
- Closing one tab shouldn't sign out all.
- But all should reflect signed-out state if it occurs.
// In one tab on sign-out
localStorage.setItem("session_ended", Date.now());
// Other tabs:
window.addEventListener("storage", (e) => {
if (e.key === "session_ended") {
forceSignOut();
}
});Per-page timeout
Some pages might have different idle policies:
const IDLE_TIMEOUT = {
"/billing": 5 * 60, // 5 min on sensitive
"/admin": 10 * 60, // 10 min on admin
"/dashboard": 30 * 60, // 30 min normal
default: 30 * 60,
};Higher security → shorter idle.
Mobile considerations
Mobile apps go to background a lot. Adjust:
- Mobile: longer idle (60 min).
- Desktop: shorter (30 min).
const isMobile = navigator.userAgent.match(/Mobile|Android|iPhone/);
const IDLE_TIMEOUT = isMobile ? 60 * 60 : 30 * 60;Don't over-aggressively timeout
For low-stakes apps, 30 min idle is friction without benefit. Adjust per:
- User satisfaction surveys.
- Customer complaints.
- Industry norms.
Banking: 5-15 min idle. SaaS: 60-240 min idle. Casual: rarely.
Step-up vs sign out
For some sensitive actions, instead of full sign-out, just require re-auth:
async function deleteAccount() {
if (isSessionStale()) {
await stepUpAuth(); // re-enter password / MFA
}
// proceed
}User stays in app; just re-authenticates for the specific action.
Audit
INSERT INTO security_audit (event_type, identity_id, metadata)
VALUES ('session_expired_idle', $id, '{"idle_minutes": $minutes}');Track idle expirations. Pattern detection.
Settings: let user choose
Some apps:
<Select label="Auto sign-out after:">
<option value="0">Never (until I sign out)</option>
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
</Select>User picks their comfort. Store in traits.
For admin / sensitive accounts: enforce minimum.
Sleep mode (mobile)
When OS suspends app (mobile):
- Visibility hidden / pagehide events fire.
- App resumes: visibility visible.
window.addEventListener("pageshow", (e) => {
if (e.persisted) {
// App was in bfcache, came back. Re-check session.
refreshSession();
}
});Don't timeout during long operations
If user is uploading a 100MB file, don't sign them out mid-upload:
const uploadInProgress = ...;
if (uploadInProgress) {
idleSeconds = 0; // reset
}Activity is happening, just not user-mouse activity.
Test
Manually:
- Sign in.
- Walk away.
- Verify warning appears.
- Verify auto-sign-out.
Confirm UX is acceptable.