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' && (