mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
fix(auth): let every fresh-factor refusal open the re-proof dialog
A Guardian signed in through single sign-on with a second factor enrolled could not hand a managed profile over once five minutes had passed since sign-in: the handover answered 401 auth.stepup.required, and the panel printed "Confirm your second factor, then try again" with no way to do so. The same held for every action behind requireFreshMfa: creating, editing and ending a managed profile, adding and removing a Guardian, disabling the second factor, rotating recovery codes, removing a security key or a passkey, changing the password, the data wipe, account deletion and the key rotation. The recent-proof routes had the dialog; these had text. - requireFreshMfa names the proofs that clear a stale stamp in meta.methods (the possession proofs the account holds, never a password, which stamps reproofAt only). - The re-proof flow treats that refusal like auth.reproof.required: open the dialog, post the proof to /api/auth/reproof, send the request once more. The flow moves into a React-free module so it can be driven in tests; the hook binds it to the dialog. - The managed-profile mutation hooks take a required proof guard, and every caller passes recentProof.run. The other surfaces wrap their requests in recentProof.run; disabling the second factor asks before its confirmation opens, so the code typed there is not spent twice. - An account with nothing to re-prove in place (single sign-on only, no factor) is told to sign in again when adding a factor, instead of being offered a password it does not have. - step-up-web-caller-guard freezes the gated route methods and fails when a web request to one of them runs outside the flow. An account whose only second factor is the identity provider's is still asked for an app factor on the managed-profile acts: the app cannot see the provider's factor. Enrolling right after a sign-in needs no further proof, and the confirmation stamps the session, so the act follows. Refs #959
This commit is contained in:
1 parent
59f910df5e
commit
99f0607a69
36 files changed
+2122
-466
No files matched your search
@@ -1089,7 +1089,7 @@ export const accountSharingPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
"401": {
|
||||
description:
|
||||
"No fresh second-factor proof (`meta.errorCode: auth.stepup.required`), or none enrolled at all (`meta.errorCode: auth.stepup.mfa_not_enrolled`) — this gate is unconditional, so an account without a second factor cannot create a managed profile until it enrols one. The ordinary unauthenticated 401 arrives here too, including for a Bearer caller, whose transport this gate does not read.",
|
||||
"No fresh second-factor proof (`meta.errorCode: auth.stepup.required`, with `meta.methods` naming the proofs that clear it at POST /api/auth/reproof), or none enrolled at all (`meta.errorCode: auth.stepup.mfa_not_enrolled`) — this gate is unconditional, so an account without a second factor cannot create a managed profile until it enrols one. The ordinary unauthenticated 401 arrives here too, including for a Bearer caller, whose transport this gate does not read.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
"429": {
|
||||
|
||||
@@ -1099,7 +1099,7 @@ export const authPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Auth"],
|
||||
summary: "Re-prove a credential on a browser session (cookie only)",
|
||||
description:
|
||||
"Stamps the calling browser session as recently re-proved, so for the next five minutes it may run the actions that ask for a fresh proof: GET /api/export/full-backup, POST /api/export/encrypted, POST /api/share-links, POST /api/mcp/tokens, POST /api/tokens/measurements, POST /api/tokens/workouts, POST /api/tokens/documents, and the admin backup download, upload and restore, the admin data wipe and the admin password reset. Those answer 401 `auth.reproof.required` with `meta.methods` (the proofs this account can give) when the session signed in or re-proved more than five minutes ago.\n\n" +
|
||||
"Stamps the calling browser session as recently re-proved, so for the next five minutes it may run the actions that ask for a fresh proof: GET /api/export/full-backup, POST /api/export/encrypted, POST /api/share-links, POST /api/mcp/tokens, POST /api/tokens/measurements, POST /api/tokens/workouts, POST /api/tokens/documents, and the admin backup download, upload and restore, the admin data wipe and the admin password reset. Those answer 401 `auth.reproof.required` with `meta.methods` (the proofs this account can give) when the session signed in or re-proved more than five minutes ago. A `totp`, `webauthn` or `passkey` proof here also clears the routes that ask for a fresh second factor (the managed-profile acts, the second-factor and passkey changes, the password change, account deletion, the data wipe and key rotation): on a browser session those answer 401 `auth.stepup.required` with `meta.methods` naming the possession proofs the account holds, and the web re-proves here and retries.\n\n" +
|
||||
'The body takes the same shapes as POST /api/auth/step-up. On an account with a second factor only `totp`, `webauthn` (a security key) or `passkey` are accepted, because a password has never stood in for the second factor; on one without, `password` or `passkey`. A method the account cannot use here is refused with 422 `auth.reproof.too_weak` and `meta.methods`, before any guess is spent. A second-factor or passkey proof also refreshes the session\'s second-factor stamp, exactly as signing in again would. Assertions begin at POST /api/auth/passkey/register-options with `{ method: "passkey" | "webauthn" }`.\n\n' +
|
||||
"Proofs draw on the account's shared re-proof budget (five per fifteen minutes, 429 once spent). A proof that does not verify is 401 `auth.reproof.failed` and is audited. Bearer callers use POST /api/auth/step-up instead.",
|
||||
requestBody: {
|
||||
|
||||
Reference in new issue
Block a user