forked from danny-avila/LibreChat
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
π feat: Two-Factor Authentication with Backup Codes & QR support (danβ¦
β¦ny-avila#5685) * π feat: add Two-Factor Authentication (2FA) with backup codes & QR support (danny-avila#5684) * working version for generating TOTP and authenticate. * better looking UI * refactored + better TOTP logic * fixed issue with UI * fixed issue: remove initial setup when closing window before completion. * added: onKeyDown for verify and disable * refactored some code and cleaned it up a bit. * refactored some code and cleaned it up a bit. * refactored some code and cleaned it up a bit. * refactored some code and cleaned it up a bit. * fixed issue after updating to new main branch * updated example * refactored controllers * removed `passport-totp` not used. * update the generateBackupCodes function to generate 10 codes by default: * update the backup codes to an object. * fixed issue with backup codes not working * be able to disable 2FA with backup codes. * removed new env. replaced with JWT_SECRET * β¨ style: improved a11y and style for TwoFactorAuthentication * π fix: small types checks * β¨ feat: improve 2FA UI components * fix: remove unnecessary console log * add option to disable 2FA with backup codes * - add option to refresh backup codes - (optional) maybe show the user which backup codes have already been used? * removed text to be able to merge the main. * removed eng tx to be able to merge * fix: migrated lang to new format. * feat: rewrote whole 2FA UI + refactored 2FA backend * chore: resolving conflicts * chore: resolving conflicts * fix: missing packages, because of resolving conflicts. * fix: UI issue and improved a11y * fix: 2FA backup code not working * fix: update localization keys for UI consistency * fix: update button label to use localized text * fix: refactor backup codes regeneration and update localization keys * fix: remove outdated translation for shared links management * fix: remove outdated 2FA code prompts from translation.json * fix: add cursor styles for backup codes item based on usage state * fix: resolve conflict issue * fix: resolve conflict issue * fix: resolve conflict issue * fix: missing packages in package-lock.json * fix: add disabled opacity to the verify button in TwoFactorScreen * β fix: update 2FA logic to rely on backup codes instead of TOTP status * βοΈ fix: Simplify user retrieval in 2FA logic by removing unnecessary TOTP secret query * βοΈ test: Add unit tests for TwoFactorAuthController and twoFactorControllers * βοΈ fix: Ensure backup codes are validated as an array before usage in 2FA components * βοΈ fix: Update module path mappings in tests to use relative paths * βοΈ fix: Update moduleNameMapper in jest.config.js to remove the caret from path mapping * βοΈ refactor: Simplify import paths in TwoFactorAuthController and twoFactorControllers test files * βοΈ test: Mock twoFactorService methods in twoFactorControllers tests * βοΈ refactor: Comment out unused imports and mock setups in test files for two-factor authentication * βοΈ refactor: removed files * refactor: Exclude totpSecret from user data retrieval in AuthController, LoginController, and jwtStrategy * refactor: Consolidate backup code verification to apply DRY and remove default array in user schema * refactor: Enhance two-factor authentication ux/flow with improved error handling and loading state management, prevent redirect to /login --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> Co-authored-by: Danny Avila <danny@librechat.ai>
- Loading branch information
1 parent
46ceae1
commit f0f0913
Showing
63 changed files
with
1,976 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
const { | ||
verifyTOTP, | ||
verifyBackupCode, | ||
generateTOTPSecret, | ||
generateBackupCodes, | ||
} = require('~/server/services/twoFactorService'); | ||
const { updateUser, getUserById } = require('~/models'); | ||
const { logger } = require('~/config'); | ||
|
||
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 user = await updateUser(userId, { totpSecret: secret, backupCodes: codeObjects }); | ||
|
||
const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; | ||
|
||
res.status(200).json({ | ||
otpauthUrl, | ||
backupCodes: plainCodes, | ||
}); | ||
} catch (err) { | ||
logger.error('[enable2FAController]', err); | ||
res.status(500).json({ message: err.message }); | ||
} | ||
}; | ||
|
||
const verify2FAController = async (req, res) => { | ||
try { | ||
const userId = req.user.id; | ||
const { token, backupCode } = req.body; | ||
const user = await getUserById(userId); | ||
if (!user || !user.totpSecret) { | ||
return res.status(400).json({ message: '2FA not initiated' }); | ||
} | ||
|
||
let verified = false; | ||
if (token && (await verifyTOTP(user.totpSecret, token))) { | ||
return res.status(200).json(); | ||
} else if (backupCode) { | ||
verified = await verifyBackupCode({ user, backupCode }); | ||
} | ||
if (verified) { | ||
return res.status(200).json(); | ||
} | ||
|
||
return res.status(400).json({ message: 'Invalid token.' }); | ||
} catch (err) { | ||
logger.error('[verify2FAController]', err); | ||
res.status(500).json({ message: err.message }); | ||
} | ||
}; | ||
|
||
const confirm2FAController = async (req, res) => { | ||
try { | ||
const userId = req.user.id; | ||
const { token } = req.body; | ||
const user = await getUserById(userId); | ||
|
||
if (!user || !user.totpSecret) { | ||
return res.status(400).json({ message: '2FA not initiated' }); | ||
} | ||
|
||
if (await verifyTOTP(user.totpSecret, token)) { | ||
return res.status(200).json(); | ||
} | ||
|
||
return res.status(400).json({ message: 'Invalid token.' }); | ||
} catch (err) { | ||
logger.error('[confirm2FAController]', err); | ||
res.status(500).json({ message: err.message }); | ||
} | ||
}; | ||
|
||
const disable2FAController = async (req, res) => { | ||
try { | ||
const userId = req.user.id; | ||
await updateUser(userId, { totpSecret: null, backupCodes: [] }); | ||
res.status(200).json(); | ||
} catch (err) { | ||
logger.error('[disable2FAController]', err); | ||
res.status(500).json({ message: err.message }); | ||
} | ||
}; | ||
|
||
const regenerateBackupCodesController = async (req, res) => { | ||
try { | ||
const userId = req.user.id; | ||
const { plainCodes, codeObjects } = await generateBackupCodes(); | ||
await updateUser(userId, { backupCodes: codeObjects }); | ||
res.status(200).json({ | ||
backupCodes: plainCodes, | ||
backupCodesHash: codeObjects, | ||
}); | ||
} catch (err) { | ||
logger.error('[regenerateBackupCodesController]', err); | ||
res.status(500).json({ message: err.message }); | ||
} | ||
}; | ||
|
||
module.exports = { | ||
enable2FAController, | ||
verify2FAController, | ||
confirm2FAController, | ||
disable2FAController, | ||
regenerateBackupCodesController, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
const jwt = require('jsonwebtoken'); | ||
const { verifyTOTP, verifyBackupCode } = require('~/server/services/twoFactorService'); | ||
const { setAuthTokens } = require('~/server/services/AuthService'); | ||
const { getUserById } = require('~/models/userMethods'); | ||
const { logger } = require('~/config'); | ||
|
||
const verify2FA = async (req, res) => { | ||
try { | ||
const { tempToken, token, backupCode } = req.body; | ||
if (!tempToken) { | ||
return res.status(400).json({ message: 'Missing temporary token' }); | ||
} | ||
|
||
let payload; | ||
try { | ||
payload = jwt.verify(tempToken, process.env.JWT_SECRET); | ||
} catch (err) { | ||
return res.status(401).json({ message: 'Invalid or expired temporary token' }); | ||
} | ||
|
||
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)) { | ||
return res.status(400).json({ message: '2FA is not enabled for this user' }); | ||
} | ||
|
||
let verified = false; | ||
|
||
if (token && (await verifyTOTP(user.totpSecret, token))) { | ||
verified = true; | ||
} else if (backupCode) { | ||
verified = await verifyBackupCode({ user, backupCode }); | ||
} | ||
|
||
if (!verified) { | ||
return res.status(401).json({ message: 'Invalid 2FA code or backup code' }); | ||
} | ||
|
||
// 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; | ||
userData.id = user._id.toString(); | ||
|
||
const authToken = await setAuthTokens(user._id, res); | ||
return res.status(200).json({ token: authToken, user: userData }); | ||
} catch (err) { | ||
logger.error('[verify2FA]', err); | ||
return res.status(500).json({ message: 'Something went wrong' }); | ||
} | ||
}; | ||
|
||
module.exports = { verify2FA }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.