
Two-Factor Authentication in Lovable: Implementation Guide
Introduction
In today’s digital landscape, a username and password combination is no longer sufficient to protect user accounts from unauthorized access. Two-factor authentication (2FA) has become an essential security feature for modern applications, adding an extra layer of protection by requiring users to verify their identity through two different methods.
Two-factor authentication is a security process that requires users to provide two different authentication factors to verify their identity. These factors typically fall into three categories:
- Something you know - like a password or PIN
- Something you have - like a mobile phone or hardware token
- Something you are - like a fingerprint or facial recognition
By requiring two different types of authentication factors, 2FA significantly reduces the risk of unauthorized access even if one factor (like a password) is compromised.
Common 2FA Implementation Issues in Lovable Applications
Lovable is a powerful vibe coding platform that makes it easy to build full-stack applications quickly. However, when it comes to implementing 2FA, developers using Lovable often encounter several security pitfalls:
1. Incomplete Implementation
Many Lovable applications implement 2FA as an afterthought, resulting in partial implementations that don’t fully protect all authentication pathways:
// Incomplete 2FA implementation in Lovable
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Verify primary credentials
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check if 2FA is enabled for user
if (user.twoFactorEnabled) {
// Set session to indicate primary authentication passed
req.session.pendingSecondFactor = true;
req.session.userId = user._id;
return res.status(200).json({ requireTwoFactor: true });
}
// If 2FA not enabled, complete login immediately
// Problem: Password reset and account recovery flows bypass 2FA
generateAuthToken(user, req, res);
});
2. Insecure Token Generation and Validation
Lovable applications often use weak methods for generating and validating 2FA tokens:
// Insecure token generation in Lovable
function generateTOTP(user) {
// Problem: Using weak random number generation
const code = Math.floor(100000 + Math.random() * 900000);
// Problem: Short expiration time not enforced properly
const expiry = Date.now() + 5 * 60 * 1000; // 5 minutes
// Store in memory (lost on server restart)
activeCodes[user.id] = { code, expiry };
return code;
}
3. Poor Recovery Options
Many Lovable applications implement recovery options that can be easily exploited to bypass 2FA:
// Insecure recovery implementation
app.post('/reset-2fa', async (req, res) => {
const { email, recoveryCode } = req.body;
// Problem: Simple recovery code that can be brute-forced
const user = await User.findOne({ email, recoveryCode });
if (user) {
// Problem: Immediately disables 2FA without additional verification
user.twoFactorEnabled = false;
await user.save();
return res.status(200).json({ message: '2FA has been disabled' });
}
return res.status(400).json({ message: 'Invalid recovery code' });
});
Implementing Secure 2FA in Lovable Applications
Let’s explore how to implement robust 2FA in Lovable applications using specific prompts that will generate secure code.
Prompt 1: Implementing TOTP-Based 2FA with Proper Setup Flow
Use this prompt to generate a secure TOTP-based 2FA implementation in your Lovable application:
Create a secure TOTP-based two-factor authentication system for my Lovable application that:
1. Uses the otpauth library for generating and validating TOTP codes
2. Implements a complete setup flow including secret generation, QR code display, and verification
3. Securely stores the TOTP secret using proper encryption
4. Generates secure backup/recovery codes during setup
5. Includes rate limiting to prevent brute force attacks
6. Ensures all authentication pathways (login, password reset, etc.) respect 2FA requirements
This prompt will generate code similar to:
// Server-side implementation
const crypto = require('crypto');
const { authenticator } = require('otpauth');
const qrcode = require('qrcode');
const bcrypt = require('bcrypt');
const { encryptData, decryptData } = require('./encryption');
// Rate limiting setup
const rateLimit = require('express-rate-limit');
const twoFactorLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many 2FA attempts, please try again later'
});
// 2FA Setup Endpoint
app.post('/setup-2fa', async (req, res) => {
try {
// Verify user is authenticated with primary factor
const userId = req.user.id;
const user = await User.findById(userId);
if (!user) {
return res.status(401).json({ message: 'Authentication required' });
}
// Generate a secure random secret
const secret = authenticator.generateSecret(20);
// Create otpauth URL for QR code
const otpauthUrl = authenticator.keyuri(
user.email,
'LovableApp',
secret
);
// Generate QR code
const qrCodeDataUrl = await qrcode.toDataURL(otpauthUrl);
// Generate recovery codes (one-time use backup codes)
const recoveryCodes = [];
for (let i = 0; i < 10; i++) {
const code = crypto.randomBytes(10).toString('hex');
recoveryCodes.push(code);
}
// Hash recovery codes before storing
const hashedRecoveryCodes = await Promise.all(
recoveryCodes.map(code => bcrypt.hash(code, 10))
);
// Encrypt the TOTP secret before storing
const encryptedSecret = encryptData(secret);
// Store encrypted secret and hashed recovery codes in user record
// but don't activate 2FA until user verifies
user.twoFactorSecret = encryptedSecret;
user.twoFactorRecoveryCodes = hashedRecoveryCodes;
user.twoFactorEnabled = false;
user.twoFactorPending = true;
await user.save();
// Return QR code and recovery codes to user
return res.status(200).json({
qrCode: qrCodeDataUrl,
recoveryCodes,
message: 'Scan the QR code with your authenticator app, then verify with a code to activate 2FA'
});
} catch (error) {
console.error('2FA setup error:', error);
return res.status(500).json({ message: 'Error setting up 2FA' });
}
});
// 2FA Verification Endpoint
app.post('/verify-2fa-setup', async (req, res) => {
try {
const { token } = req.body;
const userId = req.user.id;
const user = await User.findById(userId);
if (!user || !user.twoFactorPending) {
return res.status(400).json({ message: 'No pending 2FA setup found' });
}
// Decrypt the stored secret
const secret = decryptData(user.twoFactorSecret);
// Verify the provided token
const isValid = authenticator.check(token, secret);
if (!isValid) {
return res.status(400).json({ message: 'Invalid verification code' });
}
// Activate 2FA
user.twoFactorEnabled = true;
user.twoFactorPending = false;
await user.save();
return res.status(200).json({ message: '2FA has been successfully activated' });
} catch (error) {
console.error('2FA verification error:', error);
return res.status(500).json({ message: 'Error verifying 2FA setup' });
}
});
Prompt 2: Implementing Secure Login Flow with 2FA
Use this prompt to generate a secure login flow that properly integrates 2FA:
Create a secure login flow for my Lovable application that:
1. Properly separates primary authentication from 2FA verification
2. Implements secure session handling during the 2FA verification process
3. Applies rate limiting to prevent brute force attacks
4. Supports both TOTP and recovery code verification
5. Provides clear error messages without leaking sensitive information
6. Logs all authentication attempts for security auditing
This prompt will generate code similar to:
// Primary authentication endpoint
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user by email
const user = await User.findOne({ email });
if (!user) {
// Use consistent error messages to prevent user enumeration
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
// Log failed attempt
await logAuthAttempt(user.id, 'password', false, req.ip);
return res.status(401).json({ message: 'Invalid credentials' });
}
// Log successful primary authentication
await logAuthAttempt(user.id, 'password', true, req.ip);
// Check if 2FA is enabled
if (user.twoFactorEnabled) {
// Create a temporary session for 2FA verification
// Use a signed token with short expiry instead of storing in server memory
const twoFactorToken = jwt.sign(
{
userId: user.id,
requiresTwoFactor: true,
exp: Math.floor(Date.now() / 1000) + (5 * 60) // 5 minute expiry
},
process.env.JWT_SECRET
);
return res.status(200).json({
requiresTwoFactor: true,
twoFactorToken,
message: 'Please enter your 2FA code'
});
}
// If 2FA not enabled, complete authentication
const authToken = generateAuthToken(user);
return res.status(200).json({
message: 'Authentication successful',
token: authToken,
user: {
id: user.id,
email: user.email,
name: user.name
}
});
} catch (error) {
console.error('Login error:', error);
return res.status(500).json({ message: 'An error occurred during login' });
}
});
// 2FA verification endpoint with rate limiting
app.post('/verify-2fa', twoFactorLimiter, async (req, res) => {
try {
const { twoFactorToken, verificationCode } = req.body;
// Verify the temporary token
let decodedToken;
try {
decodedToken = jwt.verify(twoFactorToken, process.env.JWT_SECRET);
} catch (error) {
return res.status(401).json({ message: 'Invalid or expired session' });
}
// Check if token is for 2FA verification
if (!decodedToken.requiresTwoFactor) {
return res.status(400).json({ message: 'Invalid token type' });
}
// Get user
const user = await User.findById(decodedToken.userId);
if (!user || !user.twoFactorEnabled) {
return res.status(400).json({ message: 'Two-factor authentication not enabled' });
}
// Check if input is a recovery code
const isRecoveryCode = verificationCode.length > 10;
let isValid = false;
if (isRecoveryCode) {
// Verify recovery code
const recoveryCodeIndex = await findValidRecoveryCode(user, verificationCode);
if (recoveryCodeIndex >= 0) {
isValid = true;
// Remove used recovery code
user.twoFactorRecoveryCodes.splice(recoveryCodeIndex, 1);
await user.save();
}
} else {
// Verify TOTP code
const secret = decryptData(user.twoFactorSecret);
isValid = authenticator.check(verificationCode, secret);
}
if (!isValid) {
// Log failed attempt
await logAuthAttempt(user.id, '2fa', false, req.ip);
return res.status(401).json({ message: 'Invalid verification code' });
}
// Log successful 2FA verification
await logAuthAttempt(user.id, '2fa', true, req.ip);
// Generate authentication token
const authToken = generateAuthToken(user);
return res.status(200).json({
message: 'Authentication successful',
token: authToken,
user: {
id: user.id,
email: user.email,
name: user.name
}
});
} catch (error) {
console.error('2FA verification error:', error);
return res.status(500).json({ message: 'An error occurred during verification' });
}
});
// Helper function to find and validate recovery code
async function findValidRecoveryCode(user, providedCode) {
for (let i = 0; i < user.twoFactorRecoveryCodes.length; i++) {
const isMatch = await bcrypt.compare(
providedCode,
user.twoFactorRecoveryCodes[i]
);
if (isMatch) {
return i;
}
}
return -1;
}
// Helper function to log authentication attempts
async function logAuthAttempt(userId, method, success, ipAddress) {
try {
await AuthLog.create({
userId,
method,
success,
ipAddress,
timestamp: new Date()
});
} catch (error) {
console.error('Error logging auth attempt:', error);
}
}
Prompt 3: Implementing Secure 2FA Management and Recovery
Use this prompt to generate secure 2FA management and recovery functionality:
Create secure 2FA management and recovery functionality for my Lovable application that:
1. Allows users to disable 2FA with proper authentication
2. Implements a secure recovery process using pre-generated recovery codes
3. Provides functionality to regenerate recovery codes
4. Includes an option for admin-assisted recovery with proper security controls
5. Ensures all management actions are properly logged for audit purposes
6. Notifies users via email when 2FA settings are changed
This prompt will generate code similar to:
// Disable 2FA endpoint
app.post('/disable-2fa', async (req, res) => {
try {
const { password, verificationCode } = req.body;
const userId = req.user.id;
// Get user
const user = await User.findById(userId);
if (!user) {
return res.status(401).json({ message: 'Authentication required' });
}
// Verify password as additional security measure
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
await logSecurityEvent(userId, 'disable_2fa_attempt', {
success: false,
reason: 'invalid_password'
});
return res.status(401).json({ message: 'Invalid password' });
}
// Verify 2FA code
const secret = decryptData(user.twoFactorSecret);
const isValid = authenticator.check(verificationCode, secret);
if (!isValid) {
await logSecurityEvent(userId, 'disable_2fa_attempt', {
success: false,
reason: 'invalid_2fa_code'
});
return res.status(401).json({ message: 'Invalid verification code' });
}
// Disable 2FA
user.twoFactorEnabled = false;
user.twoFactorSecret = null;
user.twoFactorRecoveryCodes = [];
await user.save();
// Log successful 2FA disabling
await logSecurityEvent(userId, 'disable_2fa', {
success: true
});
// Send email notification
await sendSecurityEmail(
user.email,
'Two-Factor Authentication Disabled',
`Two-factor authentication was disabled for your account on ${new Date().toLocaleString()}.
If you did not make this change, please contact support immediately.`
);
return res.status(200).json({ message: 'Two-factor authentication has been disabled' });
} catch (error) {
console.error('Disable 2FA error:', error);
return res.status(500).json({ message: 'Error disabling 2FA' });
}
});
// Recovery using backup codes
app.post('/recover-2fa', twoFactorLimiter, async (req, res) => {
try {
const { email, recoveryCode } = req.body;
// Find user by email
const user = await User.findOne({ email });
if (!user || !user.twoFactorEnabled) {
// Use consistent error messages to prevent user enumeration
return res.status(400).json({ message: 'Invalid recovery attempt' });
}
// Verify recovery code
const recoveryCodeIndex = await findValidRecoveryCode(user, recoveryCode);
if (recoveryCodeIndex < 0) {
await logSecurityEvent(user.id, 'recovery_attempt', {
success: false,
reason: 'invalid_recovery_code'
});
return res.status(401).json({ message: 'Invalid recovery code' });
}
// Remove used recovery code
user.twoFactorRecoveryCodes.splice(recoveryCodeIndex, 1);
// Generate authentication token
const authToken = generateAuthToken(user);
// Log successful recovery
await logSecurityEvent(user.id, 'recovery_success', {
success: true,
method: 'recovery_code'
});
// Send email notification
await sendSecurityEmail(
user.email,
'Account Recovery Successful',
`Your account was recovered using a recovery code on ${new Date().toLocaleString()}.
If you did not perform this action, please contact support immediately.`
);
return res.status(200).json({
message: 'Recovery successful',
token: authToken,
remainingRecoveryCodes: user.twoFactorRecoveryCodes.length
});
} catch (error) {
console.error('Recovery error:', error);
return res.status(500).json({ message: 'An error occurred during recovery' });
}
});
// Generate new recovery codes
app.post('/regenerate-recovery-codes', async (req, res) => {
try {
const { password, verificationCode } = req.body;
const userId = req.user.id;
// Get user
const user = await User.findById(userId);
if (!user || !user.twoFactorEnabled) {
return res.status(400).json({ message: 'Two-factor authentication not enabled' });
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
await logSecurityEvent(userId, 'regenerate_codes_attempt', {
success: false,
reason: 'invalid_password'
});
return res.status(401).json({ message: 'Invalid password' });
}
// Verify 2FA code
const secret = decryptData(user.twoFactorSecret);
const isValid = authenticator.check(verificationCode, secret);
if (!isValid) {
await logSecurityEvent(userId, 'regenerate_codes_attempt', {
success: false,
reason: 'invalid_2fa_code'
});
return res.status(401).json({ message: 'Invalid verification code' });
}
// Generate new recovery codes
const recoveryCodes = [];
for (let i = 0; i < 10; i++) {
const code = crypto.randomBytes(10).toString('hex');
recoveryCodes.push(code);
}
// Hash recovery codes before storing
const hashedRecoveryCodes = await Promise.all(
recoveryCodes.map(code => bcrypt.hash(code, 10))
);
// Update user record
user.twoFactorRecoveryCodes = hashedRecoveryCodes;
await user.save();
// Log successful regeneration
await logSecurityEvent(userId, 'regenerate_recovery_codes', {
success: true
});
// Send email notification
await sendSecurityEmail(
user.email,
'Recovery Codes Regenerated',
`New recovery codes were generated for your account on ${new Date().toLocaleString()}.
If you did not make this change, please contact support immediately.`
);
return res.status(200).json({
message: 'Recovery codes regenerated successfully',
recoveryCodes
});
} catch (error) {
console.error('Regenerate recovery codes error:', error);
return res.status(500).json({ message: 'Error regenerating recovery codes' });
}
});
// Helper function to log security events
async function logSecurityEvent(userId, eventType, details) {
try {
await SecurityLog.create({
userId,
eventType,
details,
timestamp: new Date(),
ipAddress: req.ip
});
} catch (error) {
console.error('Error logging security event:', error);
}
}
// Helper function to send security emails
async function sendSecurityEmail(email, subject, message) {
try {
// Implementation depends on your email service
await emailService.send({
to: email,
subject,
text: message
});
} catch (error) {
console.error('Error sending security email:', error);
}
}
Best Practices for 2FA in Lovable Applications
When implementing 2FA in your Lovable applications, follow these best practices:
1. Use Time-Based One-Time Passwords (TOTP)
TOTP is the industry standard for 2FA and is supported by popular authenticator apps like Google Authenticator, Authy, and Microsoft Authenticator. It’s more secure than SMS-based 2FA, which is vulnerable to SIM swapping attacks.
2. Implement Proper Secret Storage
Always encrypt 2FA secrets before storing them in your database. Use a strong encryption algorithm and store the encryption key securely, separate from the database.
3. Provide Recovery Options
Always generate and provide recovery codes during 2FA setup. These should be one-time use, securely hashed, and users should be encouraged to store them safely offline.
4. Apply Rate Limiting
Implement rate limiting on all 2FA verification endpoints to prevent brute force attacks. Limit the number of verification attempts within a specific time window.
5. Secure All Authentication Pathways
Ensure that all authentication pathways, including password resets and account recovery, respect 2FA requirements. Don’t allow users to bypass 2FA through alternative login methods.
6. Log All Authentication Events
Maintain detailed logs of all authentication events, including successful and failed attempts, 2FA setup, and changes to 2FA settings. These logs are crucial for security auditing and incident response.
7. Notify Users of Security Events
Send email notifications to users whenever significant security events occur, such as enabling or disabling 2FA, using recovery codes, or generating new recovery codes.
Testing Your 2FA Implementation
After implementing 2FA in your Lovable application, thoroughly test the following scenarios:
-
Setup Flow: Verify that users can successfully set up 2FA, including scanning the QR code and verifying with a code.
-
Login Flow: Test the complete login flow with 2FA, ensuring that users are prompted for a verification code after entering their password.
-
Recovery Flow: Test the recovery process using backup codes, ensuring that used codes are invalidated.
-
Edge Cases: Test various edge cases, such as expired sessions, invalid codes, and rate limiting.
-
Security Bypass Attempts: Attempt to bypass 2FA through various means, such as password reset flows or account recovery options.
Conclusion
Implementing robust two-factor authentication in Lovable applications is essential for protecting user accounts from unauthorized access. By using the prompts provided in this blog post, you can generate secure 2FA implementations that follow industry best practices.
Remember that security is an ongoing process. Regularly review and update your 2FA implementation to address new threats and vulnerabilities as they emerge. By prioritizing security in your Lovable applications, you can provide your users with the protection they need while still leveraging the rapid development capabilities of vibe coding.
Best Practices
-
TOTP Implementation:
- Use cryptographically secure random number generation
- Implement proper rate limiting
- Store secrets securely using encryption
- Generate secure backup codes
-
Authentication Flow:
- Separate primary authentication from 2FA
- Use secure session handling
- Implement proper error messages
- Log all authentication attempts
-
Recovery Process:
- Generate secure recovery codes
- Hash recovery codes before storage
- Limit recovery attempts
- Implement admin recovery process
-
Security Measures:
- Use HTTPS for all communications
- Implement proper session management
- Add rate limiting for all endpoints
- Monitor and alert on suspicious activity