Indefinite sessions leave authenticated tokens valid on shared or stolen devices long after the user leaves — a session cookie captured from an unattended workstation grants full account access with no expiry. NIST 800-53 rev5 AC-12 (Session Termination) requires automatic logout after inactivity, and AC-11 mandates re-authentication before sensitive operations. CWE-613 (Insufficient Session Expiration) and OWASP A07 both cite missing session timeouts as enabling session hijacking attacks. Federal system guidance caps inactivity timeout at 30 minutes; operations like password changes require step-up authentication even within an active session.
Medium because indefinite sessions enable session hijacking from unattended devices but require an attacker to already possess a valid token — exploitation is not fully remote.
Set maxAge to 1800 seconds in your auth library config and require current-password re-verification before sensitive operations like password changes or account deletion.
// lib/auth.ts (next-auth)
export const { handlers, auth } = NextAuth({
session: {
strategy: 'jwt',
maxAge: 30 * 60, // 30 minutes absolute expiry
updateAge: 5 * 60 // slide the window every 5 minutes of activity
}
})
// app/api/account/delete/route.ts
export const POST = async (req: Request) => {
const session = await auth()
const { currentPassword } = await req.json()
const user = await db.user.findUnique({ where: { id: session!.user.id } })
const ok = await bcrypt.compare(currentPassword, user!.passwordHash)
if (!ok) return Response.json({ error: 'Re-authentication required' }, { status: 401 })
// proceed with deletion
}
For Supabase Auth, set SUPABASE_AUTH_JWT_EXPIRY to 1800. Clerk and Auth0 both expose session lifetime controls in their dashboard settings.
ID: gov-fisma-fedramp.audit-accountability.session-timeout
Severity: medium
What to look for: Examine session management code. Quote the exact session configuration values found (e.g., maxAge: 1800, expiresIn: '30m'). Look for session timeout configuration in auth libraries (next-auth, Clerk, Auth0, etc.), middleware that tracks inactivity, or cookie settings that define session lifetime. Check for maxAge, expiresIn, or similar timeout parameters. Count every sensitive operation and for each classify whether re-authentication is required.
Pass criteria: User sessions automatically expire after no more than 30 minutes (1800 seconds) of inactivity. Sensitive operations (password reset, data deletion, role changes) require the user to re-authenticate or provide current password. Quote the actual timeout value configured.
Fail criteria: Sessions have no timeout, or timeout is longer than 30 minutes, or sensitive operations proceed without additional authentication. Do not pass when session timeout is set to 0 or explicitly disabled.
Skip (N/A) when: The project has no user authentication.
Detail on fail: Specify the timeout issue. Example: "Session timeout not configured in next-auth — sessions persist until manual logout. No re-authentication required for password change." or "Session maxAge set to 1 hour (3600s) — exceeds 30-minute requirement"
Remediation: Configure session timeout and require re-authentication for sensitive operations:
// lib/auth.ts (next-auth example)
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const { handlers, auth } = NextAuth({
providers: [
Credentials({
credentials: { email: {}, password: {} },
async authorize(credentials) {
// Verify email + password
return user
}
})
],
session: {
maxAge: 30 * 60, // 30 minutes
updateAge: 5 * 60 // Refresh token every 5 minutes
},
callbacks: {
async session({ session, token }) {
return session
}
}
})
// app/api/auth/password-change/route.ts
export const POST = async (req: Request) => {
const session = await getServerSession()
const { currentPassword, newPassword } = await req.json()
// Re-verify user password before allowing change
const user = await db.user.findUnique({ where: { email: session.user.email } })
const isValid = await bcrypt.compare(currentPassword, user.passwordHash)
if (!isValid) {
return Response.json({ error: 'Current password incorrect' }, { status: 401 })
}
// Proceed with password change
}