From 3547873bc4ed689371d75c933cb171d732e5841d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 15 Aug 2025 18:55:49 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20refactor:?= =?UTF-8?q?=20Secure=20Field=20Selection=20for=202FA=20&=20API=20Build=20S?= =?UTF-8?q?ourcemap=20(#9087)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: `packages/api` build scripts for better inline debugging * refactor: Explicitly select secure fields as no longer returned by default, exclude backupCodes from user data retrieval in authentication and 2FA processes * refactor: Backup Codes UI to not expect backup codes, only regeneration * refactor: Ensure secure fields are deleted from user data in getUserController --- api/server/controllers/AuthController.js | 2 +- api/server/controllers/TwoFactorController.js | 6 ++-- api/server/controllers/UserController.js | 6 ++++ .../auth/TwoFactorAuthController.js | 7 ++-- api/strategies/jwtStrategy.js | 2 +- api/strategies/localStrategy.js | 2 +- .../SettingsTabs/Account/BackupCodesItem.tsx | 22 ++++++++---- client/src/data-provider/Auth/mutations.ts | 2 -- client/src/locales/en/translation.json | 6 ++-- package-lock.json | 3 +- packages/api/package.json | 6 ++-- packages/api/rollup.config.js | 34 ++++++++++++++++--- packages/api/tsconfig.build.json | 12 +++++++ packages/data-schemas/package.json | 2 +- packages/data-schemas/src/schema/user.ts | 1 + 15 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 packages/api/tsconfig.build.json diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index b06ba8a8c..59d7ec7ab 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -84,7 +84,7 @@ const refreshController = async (req, res) => { } try { const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET); - const user = await getUserById(payload.id, '-password -__v -totpSecret'); + const user = await getUserById(payload.id, '-password -__v -totpSecret -backupCodes'); if (!user) { return res.status(401).redirect('/login'); } diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index 7b1fc9291..9ef271810 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -47,7 +47,7 @@ const verify2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; - const user = await getUserById(userId); + const user = await getUserById(userId, '_id totpSecret backupCodes'); if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA not initiated' }); @@ -79,7 +79,7 @@ const confirm2FA = async (req, res) => { try { const userId = req.user.id; const { token } = req.body; - const user = await getUserById(userId); + const user = await getUserById(userId, '_id totpSecret'); if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA not initiated' }); @@ -105,7 +105,7 @@ const disable2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; - const user = await getUserById(userId); + const user = await getUserById(userId, '_id totpSecret backupCodes'); if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA is not setup for this user' }); diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 9912f79ae..cd88cb2de 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -24,7 +24,13 @@ const { getMCPManager } = require('~/config'); const getUserController = async (req, res) => { /** @type {MongoUser} */ const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user }; + /** + * These fields should not exist due to secure field selection, but deletion + * is done in case of alternate database incompatibility with Mongo API + * */ + delete userData.password; delete userData.totpSecret; + delete userData.backupCodes; if (req.app.locals.fileStrategy === FileSources.s3 && userData.avatar) { const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600); if (!avatarNeedsRefresh) { diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js index b37c89a99..9e4af2e98 100644 --- a/api/server/controllers/auth/TwoFactorAuthController.js +++ b/api/server/controllers/auth/TwoFactorAuthController.js @@ -22,10 +22,11 @@ const verify2FAWithTempToken = async (req, res) => { try { payload = jwt.verify(tempToken, process.env.JWT_SECRET); } catch (err) { + logger.error('Failed to verify temporary token:', err); return res.status(401).json({ message: 'Invalid or expired temporary token' }); } - const user = await getUserById(payload.userId); + const user = await getUserById(payload.userId, '+totpSecret +backupCodes'); if (!user || !user.twoFactorEnabled) { return res.status(400).json({ message: '2FA is not enabled for this user' }); } @@ -42,11 +43,11 @@ const verify2FAWithTempToken = async (req, res) => { return res.status(401).json({ message: 'Invalid 2FA code or backup code' }); } - // Prepare user data to return (omit sensitive fields). const userData = user.toObject ? user.toObject() : { ...user }; - delete userData.password; delete userData.__v; + delete userData.password; delete userData.totpSecret; + delete userData.backupCodes; userData.id = user._id.toString(); const authToken = await setAuthTokens(user._id, res); diff --git a/api/strategies/jwtStrategy.js b/api/strategies/jwtStrategy.js index 6793873ee..386b7bafa 100644 --- a/api/strategies/jwtStrategy.js +++ b/api/strategies/jwtStrategy.js @@ -12,7 +12,7 @@ const jwtLogin = () => }, async (payload, done) => { try { - const user = await getUserById(payload?.id, '-password -__v -totpSecret'); + const user = await getUserById(payload?.id, '-password -__v -totpSecret -backupCodes'); if (user) { user.id = user._id.toString(); if (!user.role) { diff --git a/api/strategies/localStrategy.js b/api/strategies/localStrategy.js index bc84e7c6b..26b3b8197 100644 --- a/api/strategies/localStrategy.js +++ b/api/strategies/localStrategy.js @@ -22,7 +22,7 @@ async function passportLogin(req, email, password, done) { return done(null, false, { message: validationError }); } - const user = await findUser({ email: email.trim() }); + const user = await findUser({ email: email.trim() }, '+password'); if (!user) { logError('Passport Local Strategy - User Not Found', { email }); logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`); diff --git a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx index cb0567073..fc3b545f0 100644 --- a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { RefreshCcw, ShieldX } from 'lucide-react'; +import { RefreshCcw } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { TBackupCode, TRegenerateBackupCodesResponse, type TUser } from 'librechat-data-provider'; import { @@ -73,8 +73,8 @@ const BackupCodesItem: React.FC = () => { - @@ -93,6 +93,16 @@ const BackupCodesItem: React.FC = () => { > {Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? ( <> +
+

+ {localize('com_ui_backup_codes_security_info')} +

+
+ +

+ {localize('com_ui_backup_codes_status')} +

+
{user?.backupCodes.map((code, index) => { const isUsed = code.used; @@ -125,7 +135,7 @@ const BackupCodesItem: React.FC = () => { >