# Session is invalidated on password change

- **Pattern:** `ab-002205` (`saas-authentication.session-management.session-invalidation-password-change`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002205
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002205)

## Why it matters

When a user changes their password they are often responding to a suspected compromise. Failing to invalidate existing sessions at that moment means an attacker who already has a session token retains access even after the user takes corrective action. CWE-613 and CWE-287 both cover this failure mode, and OWASP ASVS V3.3.1 requires that sessions be terminated on credential change. OWASP A07 lists this as an authentication lifecycle gap. In short: a password change that does not invalidate sessions is not a password change — it is a password addition.

## Severity rationale

Medium because exploitation requires an attacker to already possess a valid session token, but that prior access makes this a critical path in incident recovery.

## Remediation

Combine the password update and session revocation in a single atomic transaction so neither can succeed without the other:

```ts
// src/app/api/account/password/route.ts
async function changePassword(userId: string, newPassword: string) {
  const hash = await bcrypt.hash(newPassword, 12)
  await db.$transaction([
    db.user.update({ where: { id: userId }, data: { passwordHash: hash } }),
    db.session.deleteMany({ where: { userId } })
  ])
}
```

For JWT-only systems, store a `passwordChangedAt` timestamp on the user record and add a check in token validation that rejects any JWT with `iat < passwordChangedAt`.

## Detection

- **ID:** `session-invalidation-password-change`
- **Severity:** `medium`
- **What to look for:** Find the password change handler (typically in an account settings API route). Check whether it invalidates existing sessions for that user after the password is updated. With database sessions: look for a call to delete/revoke sessions by userId. With JWT sessions: look for a `passwordChangedAt` timestamp field on the user record that is compared against the JWT's `iat` claim. With managed auth providers (Clerk, Auth0): they handle this automatically. Count all instances found and enumerate each.
- **Pass criteria:** After a password change, all existing sessions for that user are invalidated (or the JWT validation logic rejects tokens issued before the password change). The current session may be re-established with the new password, but old sessions on other devices should be terminated. At least 1 implementation must be confirmed.
- **Fail criteria:** Password change API route updates the password hash but takes no action on existing sessions — an attacker who has compromised a session retains access even after the user resets their password.
- **Skip (N/A) when:** No password-based authentication — the project uses social auth only (OAuth only, no local passwords). Signal: no password field in user schema, no password change endpoint.
- **Detail on fail:** `"Password change handler at /api/account/password updates password hash but does not revoke existing sessions in the sessions table"` or `"JWT strategy used with no passwordChangedAt check — stolen tokens remain valid after password reset"`.
- **Remediation:** When a user changes their password, they are often doing so because they suspect their account is compromised. Invalidating all sessions is the right response:

  ```ts
  // Database sessions approach
  async function changePassword(userId: string, newPassword: string) {
    const hash = await bcrypt.hash(newPassword, 12)
    await db.$transaction([
      db.user.update({ where: { id: userId }, data: { passwordHash: hash } }),
      db.session.deleteMany({ where: { userId } }) // Revoke ALL sessions
    ])
  }
  ```

  For JWT-only systems, store a `passwordChangedAt` timestamp and reject any token with `iat < passwordChangedAt`.

## External references

- cwe CWE-613
- cwe CWE-287
- owasp A07

Taxons: access-control

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