Skip to content

Commit ae46582

Browse files
deocagunotclaude
andcommitted
fix(2fa): remove non-functional backup recovery code UI
Enrollment generated 10 recovery codes with Math.random(), showed them in a modal telling users to store them safely, and never persisted them. The 2FA challenge screen's backup-code branch was a placeholder that rejected every code unconditionally, so no code could ever work. Users who enabled 2FA and lost their authenticator were left holding codes that did nothing. Removes the backup-code path from the challenge form and replaces the recovery-code modal with an honest confirmation that says recovery codes are not available yet and losing the device means asking an admin to reset 2FA. The TOTP flow is unchanged — the middleware assurance-level check and challengeAndVerify were correct and are left alone. Real recovery codes are specced separately in docs/tasks/Jury-FS/2FA-Recovery-Codes-Are-Non-Functional.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b661a02 commit ae46582

2 files changed

Lines changed: 20 additions & 53 deletions

File tree

‎apps/codebility/app/auth/2fa-challenge/_components/TwoFactorForm.tsx‎

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import { useRouter, useSearchParams } from "next/navigation";
55
import { createClientClientComponent } from "@/utils/supabase/client";
66
import { Button } from "@/components/ui/button";
77
import { Input } from "@codevs/ui/input";
8-
import { ShieldCheck, KeyRound, ArrowLeft } from "lucide-react";
8+
import { ArrowLeft } from "lucide-react";
99
import toast from "react-hot-toast";
1010

1111
export default function TwoFactorForm() {
1212
const [supabase] = useState(() => createClientClientComponent());
1313
const [code, setCode] = useState("");
14-
const [isBackupMode, setIsBackupMode] = useState(false);
1514
const [isLoading, setIsLoading] = useState(false);
1615
const [factorId, setFactorId] = useState<string | null>(null);
1716
const router = useRouter();
@@ -45,21 +44,14 @@ export default function TwoFactorForm() {
4544
return;
4645
}
4746

48-
if (!factorId && !isBackupMode) {
47+
if (!factorId) {
4948
toast.error("Authentication factor not ready. Please try logging in again.");
5049
return;
5150
}
5251

5352
setIsLoading(true);
5453

5554
try {
56-
if (isBackupMode) {
57-
// Backup Recovery Code check placeholder / code verification
58-
toast.error("Invalid recovery code. Please check your backup codes or try TOTP.");
59-
setIsLoading(false);
60-
return;
61-
}
62-
6355
// Standard TOTP Challenge & Verify
6456
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
6557
factorId: factorId!,
@@ -90,14 +82,14 @@ export default function TwoFactorForm() {
9082
<form onSubmit={handleSubmit} className="mt-6 flex flex-col gap-4">
9183
<div className="space-y-2">
9284
<label htmlFor="2fa-code" className="text-sm text-gray font-medium block text-center">
93-
{isBackupMode ? "Enter 9-character Recovery Code" : "Enter 6-digit Authenticator Code"}
85+
Enter 6-digit Authenticator Code
9486
</label>
9587
<Input
9688
id="2fa-code"
9789
type="text"
98-
placeholder={isBackupMode ? "XXXX-XXXX" : "123456"}
90+
placeholder="123456"
9991
value={code}
100-
maxLength={isBackupMode ? 10 : 6}
92+
maxLength={6}
10193
onChange={(e) => setCode(e.target.value.trim())}
10294
className="text-center font-mono text-xl tracking-widest bg-dark-200 text-white border-dark-100 h-12"
10395
autoFocus
@@ -106,24 +98,17 @@ export default function TwoFactorForm() {
10698

10799
<Button
108100
type="submit"
109-
disabled={isLoading || (!isBackupMode && code.length < 6)}
101+
disabled={isLoading || code.length < 6}
110102
className="w-full bg-customBlue-100 text-white hover:bg-customBlue-200 h-11"
111103
>
112104
{isLoading ? "Verifying Identity..." : "Verify & Continue"}
113105
</Button>
114106

115107
<div className="flex flex-col gap-2 pt-2 text-center text-xs text-gray">
116-
<button
117-
type="button"
118-
onClick={() => {
119-
setIsBackupMode(!isBackupMode);
120-
setCode("");
121-
}}
122-
className="text-customBlue-100 hover:underline flex items-center justify-center gap-1"
123-
>
124-
<KeyRound className="w-3.5 h-3.5" />
125-
{isBackupMode ? "Use Authenticator App Code instead" : "Use Backup Recovery Code"}
126-
</button>
108+
<p>
109+
Lost access to your authenticator app? Contact an admin to have 2FA
110+
reset on your account.
111+
</p>
127112

128113
<button
129114
type="button"

‎apps/codebility/app/home/account-settings/_components/AccountSettings2FA.tsx‎

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ export default function AccountSettings2FA() {
4242
const [verificationCode, setVerificationCode] = useState<string>("");
4343
const [submitting, setSubmitting] = useState(false);
4444

45-
// Recovery Codes
46-
const [recoveryCodes, setRecoveryCodes] = useState<string[]>([]);
47-
4845
const fetchMfaFactors = async () => {
4946
try {
5047
setLoading(true);
@@ -115,12 +112,6 @@ export default function AccountSettings2FA() {
115112
toast.success("Two-Factor Authentication successfully enabled!");
116113
setIsEnrollOpen(false);
117114

118-
// Generate 10 random single-use backup recovery codes
119-
const generatedCodes = Array.from({ length: 10 }, () =>
120-
Math.random().toString(36).substring(2, 6).toUpperCase() + "-" +
121-
Math.random().toString(36).substring(2, 6).toUpperCase()
122-
);
123-
setRecoveryCodes(generatedCodes);
124115
setIsRecoveryOpen(true);
125116

126117
await fetchMfaFactors();
@@ -277,36 +268,27 @@ export default function AccountSettings2FA() {
277268
</DialogContent>
278269
</Dialog>
279270

280-
{/* RECOVERY CODES MODAL */}
271+
{/* ENABLED CONFIRMATION MODAL */}
281272
<Dialog open={isRecoveryOpen} onOpenChange={setIsRecoveryOpen}>
282273
<DialogContent className="background-box text-foreground sm:max-w-md">
283274
<DialogHeader>
284275
<DialogTitle className="flex items-center gap-2 text-green-500">
285-
<CheckCircle2 className="w-5 h-5" /> Save Backup Recovery Codes
276+
<CheckCircle2 className="w-5 h-5" /> Two-Factor Authentication Enabled
286277
</DialogTitle>
287278
<DialogDescription>
288-
Keep these emergency backup codes in a secure location. You can use them to access your account if you lose your phone or authenticator device.
279+
You will now be asked for a code from your authenticator app each time you sign in.
289280
</DialogDescription>
290281
</DialogHeader>
291282

292-
<div className="bg-muted p-4 rounded-lg border border-border space-y-2">
293-
<div className="grid grid-cols-2 gap-2 text-center font-mono text-xs">
294-
{recoveryCodes.map((code, idx) => (
295-
<div key={idx} className="bg-background/80 p-1.5 rounded border border-border/50">
296-
{code}
297-
</div>
298-
))}
299-
</div>
283+
<div className="bg-muted p-4 rounded-lg border border-border">
284+
<p className="text-sm">
285+
Keep your authenticator app safe. Backup recovery codes are not
286+
available yet, so if you lose access to your device you will need
287+
an admin to reset 2FA on your account.
288+
</p>
300289
</div>
301290

302-
<DialogFooter className="flex flex-col sm:flex-row gap-2">
303-
<Button
304-
variant="outline"
305-
onClick={() => copyToClipboard(recoveryCodes.join("\n"), "Backup codes")}
306-
className="w-full sm:w-auto"
307-
>
308-
<Copy className="w-4 h-4 mr-2" /> Copy All
309-
</Button>
291+
<DialogFooter>
310292
<Button
311293
onClick={() => setIsRecoveryOpen(false)}
312294
className="bg-customBlue-200 text-white hover:bg-customBlue-300 w-full sm:w-auto"

0 commit comments

Comments
 (0)