fix(auth): require a fresh proof before a second factor is added

A browser session on its own could start TOTP enrollment or register a
security key. Combined with a pending secret counting as a factor, that let
a stolen session give itself a second factor, register a passkey with it,
and keep access after the owner changed the password.

TOTP setup and security-key registration now pass on the cookie path only
when the session signed in or completed a second factor in the last five
minutes, or when the body carries a proof in the step-up shape (password,
a confirmed authenticator code, or a passkey or security-key assertion).
The settings cards ask for that proof when the server answers
auth.reproof.required and retry the same call with it. The Bearer path is
unchanged: its elevation already is the proof.

Every route that checks a password or a factor as a re-proof now uses one
verifier and one per-account budget of five attempts per fifteen minutes,
shared with the step-up mint, and records each refused proof as
auth.mfa.failed. Passkey enrollment and password change had neither the
limit (passkey enrollment) nor the audit row (both). A structural test
freezes the set of credential-verifying routes and requires a throttle and
an audit call in each.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 15:55:57 +02:00
1 parent e1718e3c30
commit 7b5bc130d8
27 files changed
+1763 -584

No files matched your search

+2
View File
@@ -60,6 +60,8 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
"auth.refresh.invalid",
"auth.refresh.reuse",
"auth.refresh.revoked",
"auth.reproof.failed",
"auth.reproof.required",
"auth.scope.insufficient",
"auth.stepup.required",
"auth.token.expired",