# Sessions expire after ≤30min inactivity; re-auth for sensitive ops

- **Pattern:** `ab-001575` (`gov-fisma-fedramp.audit-accountability.session-timeout`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001575
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001575)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```ts
// 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.

## Detection

- **ID:** `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:

  ```ts
  // 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
  }
  ```

## External references

- nist AC-12
- nist AC-11
- fedramp AC-12
- cwe CWE-613
- owasp A07

Taxons: access-control, regulatory-conformance

HTML version: https://auditbuffet.com/patterns/ab-001575
