diff options
| author | Fuwn <[email protected]> | 2026-02-09 00:47:35 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-09 00:47:35 -0800 |
| commit | 3473f17b9063ea0ebd2ee93609d6f5750688a0b4 (patch) | |
| tree | 2e15d1b32e7ccb28d1523da9e4016063e2bd251f /apps | |
| parent | format(worker): Apply Iku formatting (diff) | |
| download | asa.news-3473f17b9063ea0ebd2ee93609d6f5750688a0b4.tar.xz asa.news-3473f17b9063ea0ebd2ee93609d6f5750688a0b4.zip | |
fix: elevate to AAL2 before password/email change when MFA is enabled
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/app/reader/settings/_components/account-settings.tsx | 64 |
1 files changed, 62 insertions, 2 deletions
diff --git a/apps/web/app/reader/settings/_components/account-settings.tsx b/apps/web/app/reader/settings/_components/account-settings.tsx index 0a8d403..84b8414 100644 --- a/apps/web/app/reader/settings/_components/account-settings.tsx +++ b/apps/web/app/reader/settings/_components/account-settings.tsx @@ -24,6 +24,8 @@ export function AccountSettings() { const [currentPassword, setCurrentPassword] = useState("") const [newPassword, setNewPassword] = useState("") const [confirmNewPassword, setConfirmNewPassword] = useState("") + const [passwordMfaCode, setPasswordMfaCode] = useState("") + const [emailMfaCode, setEmailMfaCode] = useState("") const [enrolledFactors, setEnrolledFactors] = useState<Factor[]>([]) const [isTotpLoading, setIsTotpLoading] = useState(true) const [enrollmentState, setEnrollmentState] = useState<EnrollmentState>({ step: "idle" }) @@ -59,13 +61,39 @@ export function AccountSettings() { }, }) + async function elevateToAal2(mfaCode: string) { + const { data: factorsData } = await supabaseClient.auth.mfa.listFactors() + const verifiedFactors = factorsData?.totp.filter((factor) => factor.status === "verified") ?? [] + + if (verifiedFactors.length === 0) return + + if (!mfaCode || mfaCode.length !== 6) { + throw new Error("enter your 6-digit authenticator code") + } + + const { data: challengeData, error: challengeError } = + await supabaseClient.auth.mfa.challenge({ factorId: verifiedFactors[0].id }) + + if (challengeError) throw new Error("mfa challenge failed: " + challengeError.message) + + const { error: verifyError } = await supabaseClient.auth.mfa.verify({ + factorId: verifiedFactors[0].id, + challengeId: challengeData.id, + code: mfaCode, + }) + + if (verifyError) throw new Error("invalid authenticator code") + } + const updateEmailAddress = useMutation({ mutationFn: async ({ emailAddress, password, + mfaCode, }: { emailAddress: string password: string + mfaCode: string }) => { const { data: { user }, @@ -80,6 +108,8 @@ export function AccountSettings() { if (signInError) throw new Error("incorrect password") + await elevateToAal2(mfaCode) + const { error } = await supabaseClient.auth.updateUser({ email: emailAddress, }) @@ -89,6 +119,7 @@ export function AccountSettings() { onSuccess: () => { setNewEmailAddress("") setEmailPassword("") + setEmailMfaCode("") notify("confirmation email sent to your new address") }, onError: (error: Error) => { @@ -100,9 +131,11 @@ export function AccountSettings() { mutationFn: async ({ currentPassword: current, newPassword: updated, + mfaCode, }: { currentPassword: string newPassword: string + mfaCode: string }) => { const { data: { user }, @@ -117,6 +150,8 @@ export function AccountSettings() { if (signInError) throw new Error("current password is incorrect") + await elevateToAal2(mfaCode) + const { error } = await supabaseClient.auth.updateUser({ password: updated, }) @@ -127,6 +162,7 @@ export function AccountSettings() { setCurrentPassword("") setNewPassword("") setConfirmNewPassword("") + setPasswordMfaCode("") notify("password updated — signing out all sessions") await supabaseClient.auth.signOut({ scope: "global" }) router.push("/sign-in") @@ -295,7 +331,7 @@ export function AccountSettings() { event.preventDefault() const trimmedEmail = newEmailAddress.trim() if (!trimmedEmail || !emailPassword) return - updateEmailAddress.mutate({ emailAddress: trimmedEmail, password: emailPassword }) + updateEmailAddress.mutate({ emailAddress: trimmedEmail, password: emailPassword, mfaCode: emailMfaCode }) } function handleUpdatePassword(event: React.FormEvent) { @@ -312,7 +348,7 @@ export function AccountSettings() { notify("password must be at least 8 characters") return } - updatePassword.mutate({ currentPassword, newPassword }) + updatePassword.mutate({ currentPassword, newPassword, mfaCode: passwordMfaCode }) } return ( @@ -383,6 +419,18 @@ export function AccountSettings() { placeholder="current password" className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" /> + {enrolledFactors.length > 0 && ( + <input + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={6} + value={emailMfaCode} + onChange={(event) => setEmailMfaCode(event.target.value.replace(/\D/g, ""))} + placeholder="authenticator code" + className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" + /> + )} <button type="submit" disabled={updateEmailAddress.isPending || !newEmailAddress.trim() || !emailPassword} @@ -416,6 +464,18 @@ export function AccountSettings() { placeholder="confirm new password" className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" /> + {enrolledFactors.length > 0 && ( + <input + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={6} + value={passwordMfaCode} + onChange={(event) => setPasswordMfaCode(event.target.value.replace(/\D/g, ""))} + placeholder="authenticator code" + className="w-full border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" + /> + )} <button type="submit" disabled={updatePassword.isPending || !currentPassword || !newPassword || newPassword !== confirmNewPassword} |