mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +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
@@ -45,6 +45,11 @@ import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
|
||||
import { getApiErrorMessage } from "./_shared";
|
||||
import {
|
||||
recentProofErrorMessage,
|
||||
throwIfReproofRequired,
|
||||
useRecentProof,
|
||||
} from "@/components/settings/security-section/use-recent-proof";
|
||||
import { KeyBackupCard } from "./key-backup-card";
|
||||
import { useKeyBackupStatus } from "./use-key-backup-status";
|
||||
|
||||
@@ -105,12 +110,22 @@ export function EncryptionSection() {
|
||||
staleTime: 15_000,
|
||||
});
|
||||
|
||||
// Rotation needs the admin's second factor proved within five minutes. When
|
||||
// it is older the server names the proofs that clear it, the shared dialog
|
||||
// collects one, and the rotation is queued once more.
|
||||
const recentProof = useRecentProof({
|
||||
description: t("settings.reproof.descriptionChange"),
|
||||
});
|
||||
const rotate = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await apiFetchRaw("/api/admin/encryption/rotate", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const res = await recentProof.run(async () =>
|
||||
throwIfReproofRequired(
|
||||
await apiFetchRaw("/api/admin/encryption/rotate", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
throw new Error(t("admin.section.encryption.stepUpNeeded"));
|
||||
@@ -126,10 +141,12 @@ export function EncryptionSection() {
|
||||
});
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
showErrorToast(
|
||||
const message = recentProofErrorMessage(
|
||||
err,
|
||||
err.message || t("admin.section.encryption.rotateFailed"),
|
||||
{ cause: err },
|
||||
);
|
||||
if (message === null) return;
|
||||
showErrorToast(message, { cause: err });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -182,6 +199,7 @@ export function EncryptionSection() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{recentProof.dialog}
|
||||
<KeyBackupCard />
|
||||
{/* ── Coverage summary ───────────────────────────────────────── */}
|
||||
<SettingsCard>
|
||||
|
||||
Reference in new issue
Block a user