diff --git a/electron/services/mfa.cjs b/electron/services/mfa.cjs index f1a7251..f44a136 100644 --- a/electron/services/mfa.cjs +++ b/electron/services/mfa.cjs @@ -216,7 +216,17 @@ function beginEnrollment({ userId, orgId, userEmail }) { const label = encodeURIComponent(`${ISSUER}:${userEmail}`); const issuer = encodeURIComponent(ISSUER); const otpauth = `otpauth://totp/${label}?secret=${secret}&issuer=${issuer}&algorithm=SHA1&digits=${TOTP_DIGITS}&period=${TOTP_PERIOD}`; - return { secret, otpauth, issuer: ISSUER, account: userEmail, algorithm: 'SHA1', digits: TOTP_DIGITS, period: TOTP_PERIOD }; + return { + secret, + secret_base32: secret, + otpauth, + otpauth_url: otpauth, + issuer: ISSUER, + account: userEmail, + algorithm: 'SHA1', + digits: TOTP_DIGITS, + period: TOTP_PERIOD, + }; } /** @@ -249,7 +259,7 @@ function verifyAndEnableEnrollment({ userId, orgId, secret, code }) { return codes; }); const codes = tx(); - return { enabled: true, backupCodes: codes }; + return { enabled: true, backupCodes: codes, backup_codes: codes }; } /** diff --git a/src/pages/AccountSecurity.jsx b/src/pages/AccountSecurity.jsx index 2a2cfc7..72ffb43 100644 --- a/src/pages/AccountSecurity.jsx +++ b/src/pages/AccountSecurity.jsx @@ -38,16 +38,22 @@ function MfaPanel() { const beginMutation = useMutation({ mutationFn: () => api.mfa.beginEnrollment(), onSuccess: (data) => { - setEnrollment(data); + const secret = data?.secret_base32 || data?.secret || ''; + const otpauth_url = data?.otpauth_url || data?.otpauth || ''; + setEnrollment({ ...data, secret_base32: secret, otpauth_url }); setSavedBackupCodes(null); }, onError: (e) => toast({ title: 'Could not start MFA enrollment', description: e.message, variant: 'destructive' }), }); const confirmMutation = useMutation({ - mutationFn: (code) => api.mfa.confirmEnrollment({ code }), + mutationFn: (code) => + api.mfa.confirmEnrollment({ + code, + secret: enrollment?.secret_base32 || enrollment?.secret, + }), onSuccess: (data) => { - setSavedBackupCodes(data.backup_codes || enrollment?.backup_codes || []); + setSavedBackupCodes(data.backup_codes || data.backupCodes || enrollment?.backup_codes || []); setEnrollment(null); setConfirmCode(''); queryClient.invalidateQueries({ queryKey: ['mfa-status'] }); diff --git a/src/pages/ForceMfaEnrollment.jsx b/src/pages/ForceMfaEnrollment.jsx index e620f23..88e1566 100644 --- a/src/pages/ForceMfaEnrollment.jsx +++ b/src/pages/ForceMfaEnrollment.jsx @@ -10,6 +10,19 @@ function copyToClipboard(text) { if (navigator?.clipboard?.writeText) navigator.clipboard.writeText(text); } +/** Normalize desktop/server MFA begin payloads to one shape. */ +function normalizeEnrollment(data) { + if (!data) return null; + const secret = data.secret_base32 || data.secret || ''; + const otpauthUrl = data.otpauth_url || data.otpauth || ''; + return { + ...data, + secret, + secret_base32: secret, + otpauth_url: otpauthUrl, + }; +} + export default function ForceMfaEnrollment() { const { logout, clearMfaEnrollmentRequired } = useAuth(); const [enrollment, setEnrollment] = useState(null); @@ -19,14 +32,23 @@ export default function ForceMfaEnrollment() { const beginMutation = useMutation({ mutationFn: () => api.mfa.beginEnrollment(), - onSuccess: (data) => setEnrollment(data), + onSuccess: (data) => { + setError(null); + setEnrollment(normalizeEnrollment(data)); + }, onError: (e) => setError(e.message), }); const confirmMutation = useMutation({ - mutationFn: (code) => api.mfa.confirmEnrollment({ code }), + mutationFn: (code) => + api.mfa.confirmEnrollment({ + code, + secret: enrollment?.secret_base32 || enrollment?.secret, + }), onSuccess: (data) => { - setBackupCodes(data.backup_codes || enrollment?.backup_codes || []); + const codes = data?.backup_codes || data?.backupCodes || []; + setBackupCodes(Array.isArray(codes) ? codes : []); + setError(null); }, onError: (e) => setError(e.message), }); @@ -50,14 +72,18 @@ export default function ForceMfaEnrollment() {
- {backupCodes.map((code, i) => ( -
- {code} - -
- ))} + {backupCodes.length === 0 ? ( +

No backup codes returned — MFA is still enabled.

+ ) : ( + backupCodes.map((code, i) => ( +
+ {code} + +
+ )) + )}
- +
+

+ 1. Add TransTrack in your authenticator app +

+

+ Choose “Enter a setup key” (or similar) and paste this secret: +

+
+ + {enrollment.secret_base32} + +
- )} +

+ Account: TransTrack · Type: Time-based · Digits: 6 +

+ {enrollment.otpauth_url ? ( +
+ Advanced: otpauth URI + + {enrollment.otpauth_url} + +
+ ) : null} +
+
- + setConfirmCode(e.target.value.replace(/\D/g, ''))} placeholder="000000" + autoFocus className="w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm text-center font-mono text-lg tracking-widest focus:outline-none focus:ring-2 focus:ring-cyan-500" />
+ )}