fix(api): clear bound passkeys on password change to close a takeover foothold

handleChangePassword revoked other sessions but never cleared webauthn_credentials, and enrollment needs no step-up. A passkey planted through a transiently-hijacked session needs no password, so it survived the reset + session-revoke as a standing login foothold. Add DeleteAllPasskeyCredentialsForUser and call it in the change-password remediation so every passkey is unbound alongside the session revoke. Removing zero rows is a successful no-op. Email-OTP remains the fallback factor, so this never locks anyone out; the user re-enrolls a passkey afterward if they want one.
This commit is contained in:
flyemoji committed 2026-07-02 06:55:21 +09:00
1 parent 7278cd7c6a
commit 54bc6ef211
5 files changed
+45

No files matched your search

+6
View File
@@ -291,6 +291,12 @@ type Repo interface {
// can only unbind their OWN credential. No matching (user, id) row → ErrNotFound,
// so a stale or cross-user id cannot silently no-op as success.
DeletePasskeyCredential(ctx context.Context, userID, id string) error
// DeleteAllPasskeyCredentialsForUser unbinds every passkey a user holds. The
// change-password flow calls it so a passkey planted via a transiently-hijacked
// session does not survive the remediation (password reset + session revoke) as a
// standing login foothold. Removing zero rows is success, not an error — an account
// with no passkeys is the intended post-condition either way.
DeleteAllPasskeyCredentialsForUser(ctx context.Context, userID string) error
// ---- player game-login: username-collision reclaim (spec §B3) ----