diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index f145d69d9..36bc603ae 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -11,17 +11,19 @@ const { encryptV2 } = require('~/server/utils/crypto'); const enable2FAController = async (req, res) => { const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); - try { const userId = req.user.id; const secret = generateTOTPSecret(); const { plainCodes, codeObjects } = await generateBackupCodes(); - const encryptedSecret = await encryptV2(secret); - const user = await updateUser(userId, { totpSecret: encryptedSecret, backupCodes: codeObjects }); + // Set twoFactorEnabled to false until the user confirms 2FA. + const user = await updateUser(userId, { + totpSecret: encryptedSecret, + backupCodes: codeObjects, + twoFactorEnabled: false, + }); const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; - res.status(200).json({ otpauthUrl, backupCodes: plainCodes, @@ -37,6 +39,7 @@ const verify2FAController = async (req, res) => { const userId = req.user.id; const { token, backupCode } = req.body; const user = await getUserById(userId); + // Ensure that 2FA is enabled for this user. if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA not initiated' }); } @@ -52,7 +55,6 @@ const verify2FAController = async (req, res) => { return res.status(200).json(); } } - return res.status(400).json({ message: 'Invalid token.' }); } catch (err) { logger.error('[verify2FAController]', err); @@ -74,6 +76,8 @@ const confirm2FAController = async (req, res) => { const secret = await getTOTPSecret(user.totpSecret); if (await verifyTOTP(secret, token)) { + // Upon successful verification, enable 2FA. + await updateUser(userId, { twoFactorEnabled: true }); return res.status(200).json(); } @@ -87,7 +91,7 @@ const confirm2FAController = async (req, res) => { const disable2FAController = async (req, res) => { try { const userId = req.user.id; - await updateUser(userId, { totpSecret: null, backupCodes: [] }); + await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false }); res.status(200).json(); } catch (err) { logger.error('[disable2FAController]', err); diff --git a/api/server/controllers/auth/LoginController.js b/api/server/controllers/auth/LoginController.js index 8ab9a99dd..226b5605c 100644 --- a/api/server/controllers/auth/LoginController.js +++ b/api/server/controllers/auth/LoginController.js @@ -8,7 +8,7 @@ const loginController = async (req, res) => { return res.status(400).json({ message: 'Invalid credentials' }); } - if (req.user.backupCodes != null && req.user.backupCodes.length > 0) { + if (req.user.twoFactorEnabled) { const tempToken = generate2FATempToken(req.user._id); return res.status(200).json({ twoFAPending: true, tempToken }); } diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js index 78c5c0314..169078336 100644 --- a/api/server/controllers/auth/TwoFactorAuthController.js +++ b/api/server/controllers/auth/TwoFactorAuthController.js @@ -1,5 +1,9 @@ const jwt = require('jsonwebtoken'); -const { verifyTOTP, verifyBackupCode, getTOTPSecret } = require('~/server/services/twoFactorService'); +const { + verifyTOTP, + verifyBackupCode, + getTOTPSecret, +} = require('~/server/services/twoFactorService'); const { setAuthTokens } = require('~/server/services/AuthService'); const { getUserById } = require('~/models/userMethods'); const { logger } = require('~/config'); @@ -19,12 +23,12 @@ const verify2FA = async (req, res) => { } const user = await getUserById(payload.userId); - // Ensure that the user exists and has backup codes (i.e. 2FA enabled) - if (!user || !(user.backupCodes && user.backupCodes.length > 0)) { + // Ensure that the user exists and has 2FA enabled + if (!user || !user.twoFactorEnabled) { return res.status(400).json({ message: '2FA is not enabled for this user' }); } - // Use the new getTOTPSecret function to retrieve (and decrypt if necessary) the TOTP secret. + // Retrieve (and decrypt if necessary) the TOTP secret. const secret = await getTOTPSecret(user.totpSecret); let verified = false; @@ -39,9 +43,7 @@ const verify2FA = async (req, res) => { } // Prepare user data for response. - // If the user is a plain object (from lean queries), we create a shallow copy. const userData = user.toObject ? user.toObject() : { ...user }; - // Remove sensitive fields. delete userData.password; delete userData.__v; delete userData.totpSecret; diff --git a/client/src/components/Nav/SettingsTabs/Account/Account.tsx b/client/src/components/Nav/SettingsTabs/Account/Account.tsx index 68168f7f7..c1dfad190 100644 --- a/client/src/components/Nav/SettingsTabs/Account/Account.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/Account.tsx @@ -22,7 +22,7 @@ function Account() {
- {Array.isArray(user.user?.backupCodes) && user.user?.backupCodes.length > 0 && ( + {user?.user?.twoFactorEnabled && (
diff --git a/client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx b/client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx index 5dfad770d..e6e44f213 100644 --- a/client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { motion } from 'framer-motion'; -import { LockIcon, UnlockIcon } from 'lucide-react'; +// import { motion } from 'framer-motion'; +// import { LockIcon, UnlockIcon } from 'lucide-react'; import { Label, Button } from '~/components'; import { useLocalize } from '~/hooks'; diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx index bd46e8024..eb88b594c 100644 --- a/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx @@ -37,7 +37,7 @@ const TwoFactorAuthentication: React.FC = () => { const [backupCodes, setBackupCodes] = useState([]); const [isDialogOpen, setDialogOpen] = useState(false); const [verificationToken, setVerificationToken] = useState(''); - const [phase, setPhase] = useState(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup'); + const [phase, setPhase] = useState(user?.twoFactorEnabled ? 'disable' : 'setup'); const { mutate: confirm2FAMutate } = useConfirmTwoFactorMutation(); const { mutate: enable2FAMutate, isLoading: isGenerating } = useEnableTwoFactorMutation(); @@ -56,7 +56,7 @@ const TwoFactorAuthentication: React.FC = () => { const currentStep = steps.indexOf(phasesLabel[phase]); const resetState = useCallback(() => { - if (Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && otpauthUrl) { + if (user?.twoFactorEnabled && otpauthUrl) { disable2FAMutate(undefined, { onError: () => showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }), @@ -68,7 +68,7 @@ const TwoFactorAuthentication: React.FC = () => { setBackupCodes([]); setVerificationToken(''); setDisableToken(''); - setPhase(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup'); + setPhase(user?.twoFactorEnabled ? 'disable' : 'setup'); setDownloaded(false); }, [user, otpauthUrl, disable2FAMutate, localize, showToast]); @@ -136,6 +136,7 @@ const TwoFactorAuthentication: React.FC = () => { used: false, usedAt: null, })), + twoFactorEnabled: true, }) as TUser, ); }, [setUser, localize, showToast, backupCodes]); @@ -171,6 +172,7 @@ const TwoFactorAuthentication: React.FC = () => { ...prev, totpSecret: '', backupCodes: [], + twoFactorEnabled: false, }) as TUser, ); setPhase('setup'); @@ -183,7 +185,7 @@ const TwoFactorAuthentication: React.FC = () => { onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }), }); }, - [disableToken, verify2FAMutate, disable2FAMutate, showToast, localize, setUser], + [verify2FAMutate, disable2FAMutate, showToast, localize, setUser], ); return ( @@ -197,7 +199,7 @@ const TwoFactorAuthentication: React.FC = () => { }} > 0} + enabled={!!user?.twoFactorEnabled} onChange={() => setDialogOpen(true)} disabled={isVerifying || isDisabling || isGenerating} /> @@ -215,9 +217,11 @@ const TwoFactorAuthentication: React.FC = () => { - {Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? localize('com_ui_2fa_disable') : localize('com_ui_2fa_setup')} + {user?.twoFactorEnabled + ? localize('com_ui_2fa_disable') + : localize('com_ui_2fa_setup')} - {Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && phase !== 'disable' && ( + {user?.twoFactorEnabled && phase !== 'disable' && (
= ({ onDisable, isDisabli disabled={isDisabling || token.length !== (useBackup ? 8 : 6)} className="w-full rounded-xl px-6 py-3 transition-all disabled:opacity-50" > - {isDisabling === true && } + {isDisabling && } {isDisabling ? localize('com_ui_disabling') : localize('com_ui_2fa_disable')}