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.
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.
Combine the password update and session revocation in a single atomic transaction so neither can succeed without the other:
// 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.
ID: saas-authentication.session-management.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:
// 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.