Skip to content

security: revoke sessions on self-service password reset#669

Merged
lakhansamani merged 1 commit into
mainfrom
security/reset-password-session-revocation
Jul 11, 2026
Merged

security: revoke sessions on self-service password reset#669
lakhansamani merged 1 commit into
mainfrom
security/reset-password-session-revocation

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

The gap

The self-service password reset flow (internal/service/reset_password.go) updated the user's password but never revoked their existing sessions or refresh tokens. Anyone holding a live session before the reset (e.g. the attacker whose access you're resetting the password to kill) kept full access afterward — the reset was cosmetic against a live intruder (CWE-613: Insufficient Session Expiration).

Two sibling flows already do the right thing on a similar sensitive change:

  • internal/service/update_profile.go (on email change)
  • internal/service/deactivate_account.go

Both call MemoryStoreProvider.DeleteAllUserSessions(user.ID). Password reset was the odd one out.

The fix

Add the same DeleteAllUserSessions call after the password update succeeds, matching the existing pattern exactly — fire-and-forget in a goroutine, so a memory-store hiccup logs and continues rather than failing the reset:

go func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }()

This covers both token and OTP reset paths (single shared code path in ResetPassword).

Test

Extended TestResetPassword with should revoke existing sessions after reset:

  1. Sign up + log in (mints a session and an offline_access refresh token).
  2. Reset the password via the forgot-password verification token.
  3. Assert the user's sessions are gone from the memory store, then assert the pre-existing refresh token is rejected (401) by /oauth/token.

The test discriminates: it fails on main (session survives) and passes with the fix. Refresh tokens are single-use (rotated on success), so the test polls the non-mutating memory store to detect revocation rather than polling the refresh endpoint (which would self-invalidate the token and mask the bug).

make test-sqlite passes clean.

ResetPassword updated the credential but never dropped the user's
existing sessions and refresh tokens. An attacker holding a live
session before the reset kept full access after it, defeating the
purpose of resetting a compromised password (CWE-613).

Terminate all sessions after the password update via
MemoryStoreProvider.DeleteAllUserSessions, matching the existing
UpdateProfile and DeactivateAccount pattern (fire-and-forget, does
not block or fail the reset). Add a regression test asserting the
user's sessions are gone from the store and the pre-existing refresh
token is rejected by /oauth/token after a reset.
@lakhansamani
lakhansamani merged commit d1f3fd4 into main Jul 11, 2026
4 checks passed
@lakhansamani
lakhansamani deleted the security/reset-password-session-revocation branch July 11, 2026 05:34
pull Bot pushed a commit to bhardwajRahul/authorizer that referenced this pull request Jul 11, 2026
Follow-up to authorizerdev#669 (already merged): session revocation on self-service
password reset was fire-and-forget (a goroutine dispatched before the
response returned), leaving a small window where an attacker holding
a pre-existing session/refresh token could still use it between the
response going out and the goroutine actually running. Made it a
synchronous call - the revocation is a single cheap memory-store
operation, there's no latency reason for it to be async - and logged
its error instead of silently discarding it, since a failure here
means old sessions may still be live even though the reset "succeeded".

Also tightened the regression test to assert immediately rather than
polling with require.Eventually, since revocation is no longer
eventual. Note: an in-memory single-map-delete goroutine typically
completes before the surrounding code (audit log, metrics, building
the response) finishes anyway, so this assertion alone doesn't
reliably distinguish sync from fire-and-forget in practice — verified
empirically by reverting the fix and confirming the test still
passed. The guarantee here comes from the source (no goroutine, no
race, full stop), documented in the test so a future reader doesn't
over-trust this assertion's discriminating power.
pull Bot pushed a commit to bhardwajRahul/authorizer that referenced this pull request Jul 22, 2026
Added to README:
- WebAuthn / passkey login
- Enterprise SSO (SAML 2.0 IdP & SP, OIDC broker, verified domains, home realm discovery)
- SCIM 2.0 provisioning
- Multi-tenant / org-scoped admin
- Migration guides (planned)

Added to CHANGELOG [Unreleased]:
- Service accounts as FGA subjects (authorizerdev#665)
- SAML ACS CSRF exemption fix (authorizerdev#666)
- Trusted-issuer token review config (authorizerdev#667)
- Session revocation on password reset (authorizerdev#669, authorizerdev#673)
- Multi-tenant SSO phases (authorizerdev#672, authorizerdev#674, authorizerdev#675)
- OIDC discovery caching + Twitter PKCE fix (authorizerdev#668)
- WebAuthn passkey support (authorizerdev#671)
- Per-user TOTP lockout + hashed recovery codes (authorizerdev#670)
- Server-side user search + org membership (authorizerdev#678, authorizerdev#680)
- Per-method MFA availability signals (authorizerdev#681)
- Consolidated MFA redesign narrative (authorizerdev#682-686)
- SAML 2.0 IdP role (authorizerdev#691)
- OAuth 2.1 / MCP hardening (authorizerdev#693)
- SCIM group provisioning + FGA + SAML assertions (authorizerdev#694)
- Provider template parity (authorizerdev#695)
- Recent fixes (async tracking, AGENTS.md rules, security fixes, storage parity, error typing, panics, frontend fixes) (authorizerdev#696-702)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant