mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +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
+14
-1
@@ -1321,7 +1321,20 @@ export async function requireFreshMfa(
|
||||
});
|
||||
const verifiedAt = row?.mfaVerifiedAt ?? null;
|
||||
if (!verifiedAt || verifiedAt.getTime() < Date.now() - maxAgeSeconds * 1000) {
|
||||
throw new StepUpRequiredError();
|
||||
// Name the proofs that clear this gate, the way the recent-proof gate
|
||||
// does, so the browser can open the re-proof dialog instead of printing a
|
||||
// sentence with nowhere to act on it. Only the possession proofs: a
|
||||
// `POST /api/auth/reproof` with one of them stamps `mfaVerifiedAt`, a
|
||||
// password stamps `reproofAt` only and would leave this gate shut.
|
||||
const methods = (await recentProofMethods(sessionData.user)).filter(
|
||||
(method) => method !== "password",
|
||||
);
|
||||
annotate({ meta: { step_up_methods: methods.join(",") } });
|
||||
throw new StepUpRequiredError(
|
||||
"auth.stepup.required",
|
||||
"Recent second-factor verification required",
|
||||
{ methods },
|
||||
);
|
||||
}
|
||||
|
||||
return { ...sessionData, authMethod: "cookie", mfaVerifiedAt: verifiedAt };
|
||||
|
||||
Reference in new issue
Block a user