
Authentication Security in Vibe Coding: Best Practices and Implementation
Introduction
Authentication is the cornerstone of application security, serving as the primary gateway that controls access to your application’s features and data. In the rapidly evolving landscape of vibe coding, where AI tools generate code based on natural language prompts, authentication systems often receive inadequate attention to security details. This oversight can lead to significant vulnerabilities that malicious actors can exploit to gain unauthorized access to user accounts and sensitive information.
According to the OWASP Top 10, broken authentication remains one of the most critical web application security risks. A recent study by Security Boulevard found that applications built with AI coding assistants were 35% more likely to contain authentication vulnerabilities compared to traditionally coded applications. This alarming statistic highlights the importance of understanding and addressing authentication security in vibe-coded applications.
When developers instruct AI to “add user authentication” or “create a login system,” the resulting code often implements basic functionality without incorporating crucial security measures like proper password hashing, protection against brute force attacks, secure session management, or multi-factor authentication options. These omissions create significant security gaps that can compromise the entire application.
This article provides a comprehensive guide to implementing secure authentication in vibe-coded applications built with popular full-stack app builders like Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit. We’ll examine common authentication vulnerabilities in AI-generated code, demonstrate secure implementation approaches for each platform, and provide practical techniques for enhancing authentication security. By following these practices, you can ensure that your vibe-coded applications maintain robust protection against unauthorized access while still benefiting from the rapid development that AI tools enable.
Common Authentication Vulnerabilities in AI-Generated Code
AI coding assistants typically follow certain patterns when generating authentication systems. Understanding these patterns and their associated vulnerabilities is the first step toward implementing secure authentication.
Weak Password Storage
One of the most common and dangerous vulnerabilities in AI-generated authentication code is inadequate password storage:
// Example of insecure password storage in AI-generated code
const User = mongoose.model('User', new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }, // Stored as plain text or with weak hashing
name: String,
createdAt: { type: Date, default: Date.now }
}));
// Registration handler
app.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Create new user with plain text password or weak hashing
const user = new User({
email,
password, // Plain text password
name
});
await user.save();
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
This approach creates several security risks:
- Passwords stored in plain text can be directly read if the database is compromised
- Even with basic hashing (like MD5 or SHA-1), passwords can be easily cracked using rainbow tables
- No salt is used to protect against precomputed hash attacks
- No work factor to make brute force attacks computationally expensive
Missing Brute Force Protection
AI-generated authentication systems often lack protection against brute force attacks:
// Example of login without brute force protection
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user by email
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Check password
if (user.password !== password) { // Plain text comparison
return res.status(400).json({ message: 'Invalid credentials' });
}
// Generate JWT token
const token = jwt.sign({ userId: user._id }, 'your_jwt_secret', { expiresIn: '1h' });
res.json({ token });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
This code is vulnerable because:
- No limit on login attempts allows attackers to try unlimited password combinations
- No account lockout mechanism after failed attempts
- No delay between login attempts to slow down automated attacks
- No IP-based restrictions or CAPTCHA for suspicious activity
Insecure Session Management
AI tools often generate code with weak session management practices:
// Example of insecure session management
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Verify credentials (assuming this works correctly)
if (isValidUser(username, password)) {
// Create session with insufficient security
req.session.user = {
id: getUserId(username),
username: username,
role: getUserRole(username)
};
// Set cookie with weak configuration
res.cookie('sessionId', req.session.id, {
// Missing secure flag
// Missing httpOnly flag
// Missing SameSite attribute
// Excessive expiration time
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
});
res.redirect('/dashboard');
} else {
res.render('login', { error: 'Invalid credentials' });
}
});
The vulnerabilities here include:
- Missing secure flag allows cookies to be transmitted over unencrypted connections
- Missing httpOnly flag makes cookies accessible to JavaScript, enabling XSS attacks
- Missing SameSite attribute increases CSRF vulnerability
- Excessive session duration increases the risk if a session is compromised
- No session regeneration after authentication state changes
Inadequate Input Validation
AI-generated authentication code often lacks proper input validation:
// Example of inadequate input validation
app.post('/register', (req, res) => {
const { email, password, name } = req.body;
// Missing validation for email format
// Missing validation for password strength
// Missing sanitization of inputs
// Create user in database
db.query(
'INSERT INTO users (email, password, name) VALUES (?, ?, ?)',
[email, md5(password), name], // Weak hashing with md5
(err, result) => {
if (err) {
return res.status(500).json({ error: 'Registration failed' });
}
res.status(201).json({ message: 'User registered successfully' });
}
);
});
This code is problematic because:
- No validation of email format allows invalid emails to be registered
- No password strength requirements permits weak passwords
- No input sanitization increases vulnerability to injection attacks
- Weak hashing algorithm (MD5) is cryptographically broken
Missing Multi-Factor Authentication
AI tools rarely include multi-factor authentication (MFA) in generated code:
// Example of authentication without MFA support
function authenticateUser(email, password) {
// Find user and verify password
// No support for additional authentication factors
// No option for enabling MFA
// No recovery mechanisms that maintain security
}
This limitation means:
- Applications rely solely on passwords, which can be compromised
- No additional verification layer for high-risk operations
- No protection against credential stuffing attacks
- No compliance with security standards that require MFA
Platform-Specific Authentication Vulnerabilities
Each full-stack app builder has its own patterns of authentication implementation. Let’s examine specific examples from each platform.
Lovable.dev
Lovable.dev integrates with Supabase for authentication, but AI-generated code often misses important security configurations:
// Lovable.dev generated authentication code with Supabase
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'https://xyzcompany.supabase.co';
const supabaseKey = 'public-anon-key';
export const supabase = createClient(supabaseUrl, supabaseKey);
// Sign up function
export async function signUp(email, password) {
const { user, error } = await supabase.auth.signUp({
email,
password,
});
if (error) throw error;
return user;
}
// Sign in function
export async function signIn(email, password) {
const { user, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
return user;
}
While Supabase provides secure authentication infrastructure, the AI-generated code often misses:
- Password strength requirements
- Email verification enforcement
- Rate limiting configuration
- MFA setup options
- Proper error handling that doesn’t leak information
Bolt.new
Bolt.new also uses Supabase but may generate code with insufficient security measures:
// Bolt.new generated authentication code
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { useState } from 'react';
// Initialize Supabase client
const supabase: SupabaseClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''
);
// Authentication component
export function AuthForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
// No password strength validation
// No rate limiting for sign-up attempts
const { error } = await supabase.auth.signUp({
email,
password,
});
if (error) setError(error.message);
setLoading(false);
};
// Form JSX implementation
}
The issues here include:
- No client-side password strength validation
- No protection against automated sign-up attempts
- Potential information leakage through error messages
- No consideration for account recovery security
Tempo Labs
Tempo Labs may generate custom authentication code with security gaps:
// Tempo Labs generated authentication code
import { useState } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
try {
// No rate limiting
// No CSRF protection
const response = await axios.post('/api/login', { email, password });
// Store token in localStorage (insecure)
localStorage.setItem('token', response.data.token);
router.push('/dashboard');
} catch (err) {
setError('Invalid credentials');
}
};
// Form JSX implementation
}
// API route implementation
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).end();
}
const { email, password } = req.body;
// Find user in database
const user = await db.findUserByEmail(email);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Compare password with stored hash
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate JWT with weak configuration
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET || 'fallback_secret',
{ expiresIn: '30d' } // Excessive token lifetime
);
res.status(200).json({ token });
}
The vulnerabilities include:
- Storing authentication tokens in localStorage (vulnerable to XSS)
- No rate limiting for login attempts
- No CSRF protection for authentication endpoints
- Excessive JWT expiration time
- Potential use of a fallback secret if environment variable is missing
Base44
Base44 often generates more bare-bones authentication code with significant security gaps:
// Base44 generated authentication code
const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
// User model
const User = require('../models/User');
// Register route
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password with weak method
const hashedPassword = crypto
.createHash('sha256')
.update(password)
.digest('hex');
// Create new user
const newUser = new User({
username,
email,
password: hashedPassword
});
await newUser.save();
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
// Login route
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Hash password for comparison
const hashedPassword = crypto
.createHash('sha256')
.update(password)
.digest('hex');
// Compare passwords
if (user.password !== hashedPassword) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Create token
const token = jwt.sign(
{ id: user._id },
'your_jwt_secret',
{ expiresIn: '1d' }
);
res.json({ token });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
module.exports = router;
The security issues include:
- Using SHA-256 without salt for password hashing (vulnerable to rainbow table attacks)
- Hardcoded JWT secret
- No rate limiting for authentication attempts
- No account lockout mechanism
- No session management considerations
Replit
Replit’s AI assistant often generates Python authentication code with security weaknesses:
# Replit generated authentication code
from flask import Flask, request, jsonify, session
import hashlib
import uuid
app = Flask(__name__)
app.secret_key = 'your_secret_key_here' # Hardcoded secret key
# In-memory user database (in a real app, this would be a database)
users = {}
@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'error': 'Username and password are required'}), 400
if username in users:
return jsonify({'error': 'Username already exists'}), 400
# Weak password hashing
hashed_password = hashlib.md5(password.encode()).hexdigest()
# Create user
user_id = str(uuid.uuid4())
users[username] = {
'id': user_id,
'username': username,
'password': hashed_password
}
return jsonify({'message': 'User registered successfully'}), 201
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'error': 'Username and password are required'}), 400
if username not in users:
return jsonify({'error': 'Invalid credentials'}), 401
# Weak password hashing
hashed_password = hashlib.md5(password.encode()).hexdigest()
if users[username]['password'] != hashed_password:
return jsonify({'error': 'Invalid credentials'}), 401
# Set session
session['user_id'] = users[username]['id']
return jsonify({'message': 'Login successful'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True) # Debug mode enabled in production
The vulnerabilities include:
- Using MD5 for password hashing (cryptographically broken)
- Hardcoded secret key
- In-memory user storage (in this example)
- No rate limiting or brute force protection
- Debug mode enabled in production
- No secure cookie configuration
Secure Authentication Implementation Across Platforms
Now that we’ve identified common vulnerabilities, let’s explore secure authentication implementations for each platform.
Secure Password Storage
The foundation of secure authentication is proper password storage using modern hashing algorithms:
// Secure password hashing with bcrypt
const bcrypt = require('bcrypt');
async function hashPassword(password) {
// Generate a salt with appropriate cost factor (higher is more secure but slower)
const salt = await bcrypt.genSalt(12);
// Hash the password with the salt
const hashedPassword = await bcrypt.hash(password, salt);
return hashedPassword;
}
async function verifyPassword(plainPassword, hashedPassword) {
// Compare the provided password with the stored hash
const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
return isMatch;
}
Benefits of this approach:
- Bcrypt automatically incorporates salt to protect against rainbow table attacks
- The work factor (12 in this example) makes brute force attacks computationally expensive
- The salt is different for each password, so identical passwords have different hashes
- Bcrypt is specifically designed for password hashing and is resistant to hardware acceleration
Brute Force Protection
Implementing rate limiting and account lockout mechanisms is essential for preventing brute force attacks:
// Rate limiting middleware using Express Rate Limit
const rateLimit = require('express-rate-limit');
const { RateLimiterMongo } = require('rate-limiter-flexible');
const mongoose = require('mongoose');
// Simple IP-based rate limiting
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
standardHeaders: true,
legacyHeaders: false,
message: 'Too many login attempts, please try again after 15 minutes'
});
// More advanced rate limiting with MongoDB
const mongoConn = mongoose.connection;
const rateLimiterMongo = new RateLimiterMongo({
storeClient: mongoConn,
keyPrefix: 'login_fail',
points: 5, // 5 attempts
duration: 60 * 15, // 15 minutes
blockDuration: 60 * 60, // 1 hour block after too many attempts
});
// Middleware for advanced rate limiting
async function rateLimiterMiddleware(req, res, next) {
const key = req.ip;
try {
await rateLimiterMongo.consume(key);
next();
} catch (error) {
if (error.remainingPoints <= 0) {
res.status(429).json({
message: 'Too many login attempts, please try again later'
});
} else {
res.set('Retry-After', error.msBeforeNext / 1000);
res.status(429).json({
message: `Too many login attempts. ${error.remainingPoints} attempts left.`
});
}
}
}
// Apply to login route
app.post('/login', loginLimiter, async (req, res) => {
// Login logic
});
// Or with advanced rate limiting
app.post('/login', rateLimiterMiddleware, async (req, res) => {
// Login logic
});
This implementation:
- Limits the number of login attempts from a single IP address
- Provides clear feedback about remaining attempts
- Blocks access after too many failed attempts
- Can be extended to track attempts by username as well as IP
Secure Session Management
Proper session management is critical for maintaining authentication security:
// Secure session configuration with Express
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const app = express();
// Secure session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Custom cookie name instead of default
cookie: {
maxAge: 1000 * 60 * 60 * 4, // 4 hours
httpOnly: true, // Prevents JavaScript access
secure: process.env.NODE_ENV === 'production', // Requires HTTPS in production
sameSite: 'strict' // Prevents CSRF
},
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
ttl: 60 * 60 * 4 // 4 hours (matching cookie maxAge)
})
}));
// Regenerate session on authentication state change
app.post('/login', async (req, res) => {
// Verify credentials
// Regenerate session to prevent session fixation
const regenerateSession = () => {
return new Promise((resolve, reject) => {
req.session.regenerate((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
try {
await regenerateSession();
// Set user data in new session
req.session.userId = user.id;
req.session.userRole = user.role;
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Session error' });
}
});
// Secure logout
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('sessionId');
res.json({ success: true });
});
});
This implementation:
- Uses environment variables for session secrets
- Sets secure cookie attributes (httpOnly, secure, sameSite)
- Uses a reasonable session timeout
- Stores sessions in a database rather than memory
- Regenerates the session on login to prevent session fixation
- Properly destroys the session on logout
Input Validation and Sanitization
Proper input validation is essential for authentication security:
// Input validation with express-validator
const { body, validationResult } = require('express-validator');
// Registration route with validation
app.post('/register', [
// Username validation
body('username')
.trim()
.isLength({ min: 3, max: 30 })
.withMessage('Username must be between 3 and 30 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores'),
// Email validation
body('email')
.trim()
.isEmail()
.withMessage('Must be a valid email address')
.normalizeEmail(),
// Password validation
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters long')
.matches(/[a-z]/)
.withMessage('Password must contain at least one lowercase letter')
.matches(/[A-Z]/)
.withMessage('Password must contain at least one uppercase letter')
.matches(/[0-9]/)
.withMessage('Password must contain at least one number')
.matches(/[^a-zA-Z0-9]/)
.withMessage('Password must contain at least one special character'),
// Password confirmation
body('confirmPassword')
.custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('Password confirmation does not match password');
}
return true;
})
], async (req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Registration logic with validated inputs
});
This implementation:
- Validates username format and length
- Ensures email is properly formatted
- Enforces strong password requirements
- Confirms password matches
- Sanitizes inputs to prevent injection attacks
Multi-Factor Authentication
Implementing MFA adds an essential layer of security:
// Multi-factor authentication with speakeasy
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate MFA secret for a user
app.post('/mfa/setup', authenticate, async (req, res) => {
const { userId } = req.user;
// Generate a secret
const secret = speakeasy.generateSecret({
name: `YourApp:${req.user.email}`
});
// Store the secret in the user's record
await User.findByIdAndUpdate(userId, {
mfaSecret: secret.base32,
mfaEnabled: false // Not enabled until verified
});
// Generate QR code
QRCode.toDataURL(secret.otpauth_url, (err, dataUrl) => {
if (err) {
return res.status(500).json({ error: 'Could not generate QR code' });
}
res.json({
secret: secret.base32,
qrCode: dataUrl
});
});
});
// Verify and enable MFA
app.post('/mfa/verify', authenticate, async (req, res) => {
const { token } = req.body;
const { userId } = req.user;
// Get user with MFA secret
const user = await User.findById(userId);
// Verify the token
const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token
});
if (!verified) {
return res.status(400).json({ error: 'Invalid verification code' });
}
// Enable MFA for the user
await User.findByIdAndUpdate(userId, {
mfaEnabled: true
});
res.json({ success: true });
});
// Login with MFA
app.post('/login', async (req, res) => {
const { email, password, mfaToken } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check if MFA is enabled
if (user.mfaEnabled) {
// If no MFA token provided in initial request
if (!mfaToken) {
return res.status(200).json({
requireMfa: true,
message: 'Please provide MFA token'
});
}
// Verify MFA token
const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token: mfaToken
});
if (!verified) {
return res.status(401).json({ error: 'Invalid MFA token' });
}
}
// Generate session/token
// ...
res.json({ success: true, token });
});
This implementation:
- Generates a unique secret for each user
- Creates a QR code for easy setup with authenticator apps
- Verifies the token before enabling MFA
- Handles the two-step login process when MFA is enabled
- Provides appropriate responses at each step
Platform-Specific Secure Authentication Implementations
Let’s look at secure authentication implementations for each full-stack app builder.
Lovable.dev Secure Authentication
Lovable.dev uses Supabase for authentication, which provides a solid foundation. Here’s how to enhance its security:
// Secure Lovable.dev authentication with Supabase
import { createClient } from '@supabase/supabase-js';
import { useState } from 'react';
// Initialize Supabase client
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
// Password strength validation
function isStrongPassword(password) {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChars = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return (
password.length >= minLength &&
hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasSpecialChars
);
}
// Secure sign-up component
export function SignUp() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [passwordStrength, setPasswordStrength] = useState({
isStrong: false,
message: ''
});
// Check password strength on change
const handlePasswordChange = (e) => {
const newPassword = e.target.value;
setPassword(newPassword);
if (!newPassword) {
setPasswordStrength({
isStrong: false,
message: ''
});
return;
}
const isStrong = isStrongPassword(newPassword);
setPasswordStrength({
isStrong,
message: isStrong
? 'Strong password'
: 'Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters'
});
};
const handleSignUp = async (e) => {
e.preventDefault();
// Client-side validation
if (!email || !password) {
setError('Email and password are required');
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (!passwordStrength.isStrong) {
setError(passwordStrength.message);
return;
}
setLoading(true);
setError(null);
try {
// Sign up with email confirmation
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
}
});
if (error) throw error;
// Success message
alert('Check your email for the confirmation link!');
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
// Form JSX implementation
}
// Secure sign-in with MFA support
export function SignIn() {
// Implementation with MFA support using Supabase Auth UI
return (
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
providers={[]}
redirectTo={`${window.location.origin}/auth/callback`}
/>
);
}
// Server-side authentication check
export async function getServerSideProps(context) {
const { req } = context;
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
return {
props: { user },
};
}
Key security enhancements:
- Client-side password strength validation
- Password confirmation check
- Email verification requirement
- Server-side authentication verification
- Using Supabase Auth UI for standardized, secure authentication flows
Bolt.new Secure Authentication
Bolt.new also uses Supabase but can be enhanced with additional security measures:
// Secure Bolt.new authentication
import { createClient, SupabaseClient, User } from '@supabase/supabase-js';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
// Initialize Supabase client
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
const supabase: SupabaseClient = createClient(supabaseUrl, supabaseKey);
// Authentication hook
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
// Get initial session
const session = supabase.auth.session();
setUser(session?.user ?? null);
setLoading(false);
// Listen for auth changes
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
setLoading(false);
// Update auth cookie
fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, session }),
});
}
);
return () => {
authListener?.unsubscribe();
};
}, []);
// Sign out function
const signOut = async () => {
await supabase.auth.signOut();
router.push('/login');
};
return { user, loading, signOut };
}
// Secure login component
export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [passwordVisible, setPasswordVisible] = useState(false);
const router = useRouter();
// Track failed attempts
const [attempts, setAttempts] = useState(0);
const MAX_ATTEMPTS = 5;
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (attempts >= MAX_ATTEMPTS) {
setError('Too many failed attempts. Please try again later.');
return;
}
setLoading(true);
setError(null);
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
router.push('/dashboard');
} catch (error: any) {
setError(error.message);
setAttempts(prev => prev + 1);
// If approaching max attempts, show warning
if (attempts + 1 >= MAX_ATTEMPTS - 1) {
setError(`Login failed. You have ${MAX_ATTEMPTS - attempts - 1} attempts remaining.`);
}
} finally {
setLoading(false);
}
};
// Form JSX implementation with password visibility toggle
}
// Protected route component
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !user) {
router.push('/login');
}
}, [user, loading, router]);
if (loading || !user) {
return <div>Loading...</div>;
}
return <>{children}</>;
}
Key security enhancements:
- Authentication state management with proper session handling
- Client-side attempt limiting
- Password visibility toggle for usability
- Protected route component for access control
- Server-side session verification
Tempo Labs Secure Authentication
For Tempo Labs, implementing secure JWT authentication with proper storage:
// Secure Tempo Labs authentication
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';
import Cookies from 'js-cookie';
// Auth context for state management
export const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
// Check for existing token on mount
useEffect(() => {
const checkAuth = async () => {
const token = Cookies.get('auth_token');
if (token) {
try {
// Set default auth header
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// Verify token with backend
const { data } = await axios.get('/api/auth/me');
setUser(data.user);
} catch (error) {
// Clear invalid token
Cookies.remove('auth_token');
delete axios.defaults.headers.common['Authorization'];
}
}
setLoading(false);
};
checkAuth();
}, []);
// Login function
const login = async (email, password) => {
try {
const { data } = await axios.post('/api/auth/login', {
email,
password
});
// Store token in httpOnly cookie (set by server)
axios.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
// Get user data
const userResponse = await axios.get('/api/auth/me');
setUser(userResponse.data.user);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data?.message || 'Login failed'
};
}
};
// Logout function
const logout = async () => {
try {
await axios.post('/api/auth/logout');
} catch (error) {
console.error('Logout error:', error);
}
// Clear auth state
setUser(null);
delete axios.defaults.headers.common['Authorization'];
router.push('/login');
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Secure login component
export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useContext(AuthContext);
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
const result = await login(email, password);
if (result.success) {
router.push('/dashboard');
} else {
setError(result.error);
setLoading(false);
}
};
// Form JSX implementation
}
// API route for login
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).end();
}
try {
const { email, password } = req.body;
// Find user
const user = await db.findUserByEmail(email);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check if MFA is required
if (user.mfaEnabled) {
// Handle MFA flow
return res.status(200).json({
requireMfa: true,
userId: user.id
});
}
// Generate JWT
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '1h' } // Short expiration time
);
// Set httpOnly cookie
res.setHeader(
'Set-Cookie',
serialize('auth_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60, // 1 hour
path: '/'
})
);
res.status(200).json({ token });
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Internal server error' });
}
}
Key security enhancements:
- Storing tokens in httpOnly cookies instead of localStorage
- Short JWT expiration time
- Proper authentication context management
- Server-side token verification
- Support for MFA flow
- Secure cookie settings
Base44 Secure Authentication
For Base44, implementing proper password hashing and authentication flow:
// Secure Base44 authentication
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const rateLimit = require('express-rate-limit');
// User model
const User = require('../models/User');
// Rate limiting for registration
const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 registrations per IP per hour
message: 'Too many accounts created from this IP, please try again after an hour'
});
// Rate limiting for login
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per IP per 15 minutes
message: 'Too many login attempts, please try again after 15 minutes'
});
// Register route with validation
router.post(
'/register',
registerLimiter,
[
body('username')
.trim()
.isLength({ min: 3, max: 30 })
.withMessage('Username must be between 3 and 30 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores'),
body('email')
.trim()
.isEmail()
.withMessage('Must be a valid email address')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters long')
.matches(/[a-z]/)
.withMessage('Password must contain at least one lowercase letter')
.matches(/[A-Z]/)
.withMessage('Password must contain at least one uppercase letter')
.matches(/[0-9]/)
.withMessage('Password must contain at least one number')
.matches(/[^a-zA-Z0-9]/)
.withMessage('Password must contain at least one special character')
],
async (req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { username, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
message: 'User already exists'
});
}
// Hash password with bcrypt
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(password, salt);
// Create new user
const newUser = new User({
username,
email,
password: hashedPassword,
emailVerified: false
});
await newUser.save();
// Generate verification token
const verificationToken = jwt.sign(
{ userId: newUser._id },
process.env.EMAIL_VERIFICATION_SECRET,
{ expiresIn: '1d' }
);
// Send verification email (implementation omitted)
// sendVerificationEmail(email, verificationToken);
res.status(201).json({
message: 'User created successfully. Please check your email to verify your account.'
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ message: 'Server error' });
}
}
);
// Login route with rate limiting
router.post('/login', loginLimiter, async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check if email is verified
if (!user.emailVerified) {
return res.status(401).json({
message: 'Please verify your email before logging in'
});
}
// Compare passwords
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
// Increment failed login attempts
user.failedLoginAttempts = (user.failedLoginAttempts || 0) + 1;
// Lock account after 5 failed attempts
if (user.failedLoginAttempts >= 5) {
user.accountLocked = true;
user.lockUntil = Date.now() + 30 * 60 * 1000; // 30 minutes
}
await user.save();
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check if account is locked
if (user.accountLocked && user.lockUntil > Date.now()) {
const remainingTime = Math.ceil((user.lockUntil - Date.now()) / 1000 / 60);
return res.status(401).json({
message: `Account is locked. Try again in ${remainingTime} minutes`
});
}
// Reset failed attempts on successful login
user.failedLoginAttempts = 0;
user.accountLocked = false;
user.lockUntil = null;
await user.save();
// Create token
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// Set httpOnly cookie
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 1000 // 1 hour
});
res.json({
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error' });
}
});
// Email verification route
router.get('/verify-email/:token', async (req, res) => {
try {
const { token } = req.params;
// Verify token
const decoded = jwt.verify(token, process.env.EMAIL_VERIFICATION_SECRET);
// Update user
const user = await User.findByIdAndUpdate(
decoded.userId,
{ emailVerified: true },
{ new: true }
);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.redirect('/login?verified=true');
} catch (error) {
console.error('Email verification error:', error);
res.status(400).json({ message: 'Invalid or expired token' });
}
});
module.exports = router;
Key security enhancements:
- Proper password hashing with bcrypt
- Input validation and sanitization
- Rate limiting for registration and login
- Email verification requirement
- Account lockout after failed attempts
- Secure cookie settings for token storage
- JWT with reasonable expiration time
Replit Secure Authentication
For Replit, implementing secure authentication in Python:
# Secure Replit authentication
from flask import Flask, request, jsonify, session, make_response
import bcrypt
import jwt
import datetime
import os
import re
from functools import wraps
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'fallback_secret_for_development')
# Setup rate limiting
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
# Database connection (using SQLAlchemy in a real app)
from flask_sqlalchemy import SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///users.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# User model
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(128), nullable=False)
email_verified = db.Column(db.Boolean, default=False)
mfa_secret = db.Column(db.String(32), nullable=True)
mfa_enabled = db.Column(db.Boolean, default=False)
failed_login_attempts = db.Column(db.Integer, default=0)
account_locked = db.Column(db.Boolean, default=False)
lock_until = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
# Create tables
with app.app_context():
db.create_all()
# Token required decorator
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Check for token in headers
if 'Authorization' in request.headers:
auth_header = request.headers['Authorization']
if auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
# Check for token in cookies
if not token:
token = request.cookies.get('token')
if not token:
return jsonify({'message': 'Token is missing'}), 401
try:
# Decode token
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
current_user = User.query.filter_by(id=data['user_id']).first()
except:
return jsonify({'message': 'Token is invalid'}), 401
return f(current_user, *args, **kwargs)
return decorated
# Password validation
def is_strong_password(password):
if len(password) < 8:
return False, "Password must be at least 8 characters long"
if not re.search(r'[A-Z]', password):
return False, "Password must contain at least one uppercase letter"
if not re.search(r'[a-z]', password):
return False, "Password must contain at least one lowercase letter"
if not re.search(r'[0-9]', password):
return False, "Password must contain at least one number"
if not re.search(r'[^A-Za-z0-9]', password):
return False, "Password must contain at least one special character"
return True, "Password is strong"
# Registration route with rate limiting
@app.route('/register', methods=['POST'])
@limiter.limit("5 per hour")
def register():
data = request.get_json()
# Validate required fields
if not data or not data.get('username') or not data.get('email') or not data.get('password'):
return jsonify({'message': 'Missing required fields'}), 400
# Check if user exists
if User.query.filter_by(email=data['email']).first():
return jsonify({'message': 'User with this email already exists'}), 400
if User.query.filter_by(username=data['username']).first():
return jsonify({'message': 'Username already taken'}), 400
# Validate password strength
is_strong, message = is_strong_password(data['password'])
if not is_strong:
return jsonify({'message': message}), 400
# Hash password
hashed_password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt(12))
# Create new user
new_user = User(
username=data['username'],
email=data['email'],
password=hashed_password.decode('utf-8'),
email_verified=False
)
db.session.add(new_user)
db.session.commit()
# Generate verification token
verification_token = jwt.encode(
{
'user_id': new_user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1)
},
app.config['SECRET_KEY'],
algorithm='HS256'
)
# In a real app, send verification email
# send_verification_email(new_user.email, verification_token)
return jsonify({'message': 'User created successfully. Please verify your email.'}), 201
# Login route with rate limiting
@app.route('/login', methods=['POST'])
@limiter.limit("5 per 15 minute")
def login():
data = request.get_json()
if not data or not data.get('email') or not data.get('password'):
return jsonify({'message': 'Missing email or password'}), 400
# Find user
user = User.query.filter_by(email=data['email']).first()
if not user:
return jsonify({'message': 'Invalid credentials'}), 401
# Check if account is locked
if user.account_locked and user.lock_until and user.lock_until > datetime.datetime.utcnow():
remaining_seconds = (user.lock_until - datetime.datetime.utcnow()).total_seconds()
remaining_minutes = int(remaining_seconds / 60) + 1
return jsonify({'message': f'Account is locked. Try again in {remaining_minutes} minutes'}), 401
# Check password
if not bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
# Increment failed login attempts
user.failed_login_attempts += 1
# Lock account after 5 failed attempts
if user.failed_login_attempts >= 5:
user.account_locked = True
user.lock_until = datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
db.session.commit()
return jsonify({'message': 'Invalid credentials'}), 401
# Check if email is verified
if not user.email_verified:
return jsonify({'message': 'Please verify your email before logging in'}), 401
# Reset failed attempts on successful login
user.failed_login_attempts = 0
user.account_locked = False
user.lock_until = None
db.session.commit()
# Check if MFA is required
if user.mfa_enabled:
if not data.get('mfa_token'):
return jsonify({
'message': 'MFA token required',
'require_mfa': True
}), 200
# Verify MFA token (implementation omitted)
# if not verify_totp(user.mfa_secret, data['mfa_token']):
# return jsonify({'message': 'Invalid MFA token'}), 401
# Generate JWT token
token = jwt.encode(
{
'user_id': user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
},
app.config['SECRET_KEY'],
algorithm='HS256'
)
# Create response with httpOnly cookie
response = make_response(jsonify({
'message': 'Login successful',
'token': token
}))
response.set_cookie(
'token',
token,
httponly=True,
secure=os.environ.get('FLASK_ENV') == 'production',
samesite='Strict',
max_age=60*60 # 1 hour
)
return response
# Protected route example
@app.route('/profile', methods=['GET'])
@token_required
def get_profile(current_user):
return jsonify({
'username': current_user.username,
'email': current_user.email,
'mfa_enabled': current_user.mfa_enabled
})
# Email verification route
@app.route('/verify-email/<token>', methods=['GET'])
def verify_email(token):
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
user = User.query.filter_by(id=data['user_id']).first()
if not user:
return jsonify({'message': 'User not found'}), 404
user.email_verified = True
db.session.commit()
return jsonify({'message': 'Email verified successfully'}), 200
except:
return jsonify({'message': 'Invalid or expired token'}), 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=False) # Debug mode disabled in production
Key security enhancements:
- Proper password hashing with bcrypt
- Strong password validation
- Rate limiting for registration and login
- Account lockout mechanism
- Email verification requirement
- Secure cookie settings for token storage
- JWT with reasonable expiration time
- MFA support
- Debug mode disabled in production
Best Practices for Secure Authentication in Vibe Coding
To implement secure authentication in vibe-coded applications, follow these best practices:
1. Use Specific Prompts for Security
When using AI tools to generate authentication code, include security requirements in your prompts:
Instead of:
Create a user authentication system
Use:
Create a secure user authentication system with the following features:
1. Password hashing using bcrypt with a cost factor of 12
2. Input validation for email and password strength
3. Protection against brute force attacks with rate limiting
4. Secure session management with httpOnly cookies
5. Email verification requirement
6. Support for multi-factor authentication
2. Implement Proper Password Storage
Never store passwords in plain text or with weak hashing algorithms:
- Use bcrypt, Argon2, or PBKDF2 for password hashing
- Use a sufficient work factor (cost) to make brute force attacks computationally expensive
- Never use MD5 or SHA-1 for password hashing
- Include a unique salt for each password (built into bcrypt and Argon2)
3. Enforce Strong Password Policies
Require users to create strong passwords:
- Minimum length of 8 characters
- Combination of uppercase and lowercase letters
- Inclusion of numbers and special characters
- Check against common password lists
- Consider implementing a password strength meter
4. Implement Brute Force Protection
Prevent automated attacks on authentication endpoints:
- Implement rate limiting for login attempts
- Add account lockout after multiple failed attempts
- Use exponential backoff for repeated failures
- Consider CAPTCHA for suspicious activity
- Log and alert on unusual authentication patterns
5. Secure Session Management
Properly manage user sessions after authentication:
- Use httpOnly cookies for storing session tokens
- Set the secure flag in production environments
- Use the SameSite attribute to prevent CSRF
- Implement reasonable session timeouts
- Regenerate session IDs after authentication state changes
- Properly invalidate sessions on logout
6. Implement Multi-Factor Authentication
Add an additional layer of security with MFA:
- Offer TOTP (Time-based One-Time Password) authentication
- Consider SMS, email, or push notification options
- Implement secure recovery mechanisms
- Allow users to manage their MFA settings
- Require MFA for sensitive operations
7. Validate and Sanitize Inputs
Prevent injection attacks and other vulnerabilities:
- Validate email format and other user inputs
- Sanitize inputs to prevent XSS and injection attacks
- Use parameterized queries for database operations
- Implement proper error handling that doesn’t leak information
8. Implement Secure Password Reset
Create a secure password reset flow:
- Use time-limited, single-use tokens
- Send reset links to verified email addresses
- Require verification before changing passwords
- Log password changes and notify users
- Don’t reveal account existence through reset flows
Real-World Scenarios and Solutions
Let’s examine some real-world authentication scenarios in vibe-coded applications and their solutions.
Scenario 1: Weak Password Storage
Problem: A developer used vibe coding to create an authentication system that stores passwords with MD5 hashing, which is cryptographically broken.
Solution:
-
Migrate to a secure password hashing algorithm:
const bcrypt = require('bcrypt'); // Function to migrate user passwords async function migrateUserPassword(user) { // Check if password needs migration (stored with MD5) if (user.passwordVersion !== 'bcrypt') { // When user logs in with correct MD5 password // Hash their password with bcrypt const hashedPassword = await bcrypt.hash(plainTextPassword, 12); // Update user record user.password = hashedPassword; user.passwordVersion = 'bcrypt'; await user.save(); } } // Update login logic to handle migration async function login(email, password) { const user = await User.findOne({ email }); if (!user) { return { success: false, message: 'Invalid credentials' }; } let isValid = false; // Check if password needs migration if (user.passwordVersion !== 'bcrypt') { // Verify with old MD5 method const md5Hash = crypto .createHash('md5') .update(password) .digest('hex'); isValid = user.password === md5Hash; // If valid, migrate to bcrypt if (isValid) { user.password = await bcrypt.hash(password, 12); user.passwordVersion = 'bcrypt'; await user.save(); } } else { // Verify with bcrypt isValid = await bcrypt.compare(password, user.password); } if (!isValid) { return { success: false, message: 'Invalid credentials' }; } // Generate token and complete login // ... return { success: true, token }; }
-
Notify users about the security upgrade and encourage password changes.
Scenario 2: Missing Brute Force Protection
Problem: A vibe-coded authentication system allows unlimited login attempts, making it vulnerable to brute force attacks.
Solution:
-
Implement rate limiting middleware:
const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); const redisClient = new Redis({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT, password: process.env.REDIS_PASSWORD }); // Global rate limiter const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window standardHeaders: true, legacyHeaders: false, message: 'Too many requests, please try again later', store: new RedisStore({ sendCommand: (...args) => redisClient.call(...args) }) }); // Specific login rate limiter const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 login attempts per window standardHeaders: true, legacyHeaders: false, message: 'Too many login attempts, please try again after 15 minutes', store: new RedisStore({ sendCommand: (...args) => redisClient.call(...args), prefix: 'rl:login:' }) }); // Apply limiters to routes app.use(globalLimiter); // Apply to all routes app.post('/login', loginLimiter, loginController); // Apply to login route
-
Implement account-specific lockout:
// In login controller async function login(req, res) { const { email, password } = req.body; // Find user const user = await User.findOne({ email }); // Check if account is locked if (user && user.accountLocked && user.lockUntil > Date.now()) { const remainingTime = Math.ceil((user.lockUntil - Date.now()) / 1000 / 60); return res.status(429).json({ message: `Account is locked. Try again in ${remainingTime} minutes` }); } // Verify credentials if (!user || !await bcrypt.compare(password, user.password)) { // If user exists, increment failed attempts if (user) { user.failedLoginAttempts = (user.failedLoginAttempts || 0) + 1; // Lock account after 5 failed attempts if (user.failedLoginAttempts >= 5) { user.accountLocked = true; user.lockUntil = Date.now() + 30 * 60 * 1000; // 30 minutes } await user.save(); } return res.status(401).json({ message: 'Invalid credentials' }); } // Reset failed attempts on successful login if (user.failedLoginAttempts > 0) { user.failedLoginAttempts = 0; user.accountLocked = false; user.lockUntil = null; await user.save(); } // Complete login process // ... }
Scenario 3: Insecure Session Management
Problem: A vibe-coded application stores authentication tokens in localStorage, making them vulnerable to XSS attacks.
Solution:
-
Switch to httpOnly cookies for token storage:
// Server-side code app.post('/login', async (req, res) => { // Verify credentials // Generate JWT const token = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' } ); // Set httpOnly cookie instead of returning token in response res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 60 * 60 * 1000 // 1 hour }); // Return user data without token res.json({ user: { id: user._id, name: user.name, email: user.email } }); }); // Client-side code async function login(email, password) { try { const response = await fetch('/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), credentials: 'include' // Important for cookies }); const data = await response.json(); if (response.ok) { // Store user data in state management // No need to store token as it's in the cookie return { success: true, user: data.user }; } else { return { success: false, error: data.message }; } } catch (error) { return { success: false, error: 'Login failed' }; } }
-
Update authentication middleware to check for cookies:
function authenticate(req, res, next) { const token = req.cookies.token; if (!token) { return res.status(401).json({ message: 'Authentication required' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (error) { res.clearCookie('token'); return res.status(401).json({ message: 'Invalid or expired token' }); } }
Scenario 4: Missing Multi-Factor Authentication
Problem: A vibe-coded authentication system lacks multi-factor authentication, making it vulnerable to credential stuffing attacks.
Solution:
-
Implement TOTP-based MFA:
const speakeasy = require('speakeasy'); const QRCode = require('qrcode'); // Setup MFA for a user app.post('/mfa/setup', authenticate, async (req, res) => { const userId = req.user.userId; // Generate a secret const secret = speakeasy.generateSecret({ name: `YourApp:${req.user.email}` }); // Store the secret in the user's record await User.findByIdAndUpdate(userId, { mfaSecret: secret.base32, mfaEnabled: false // Not enabled until verified }); // Generate QR code QRCode.toDataURL(secret.otpauth_url, (err, dataUrl) => { if (err) { return res.status(500).json({ error: 'Could not generate QR code' }); } res.json({ secret: secret.base32, qrCode: dataUrl }); }); }); // Verify and enable MFA app.post('/mfa/verify', authenticate, async (req, res) => { const { token } = req.body; const userId = req.user.userId; // Get user with MFA secret const user = await User.findById(userId); // Verify the token const verified = speakeasy.totp.verify({ secret: user.mfaSecret, encoding: 'base32', token, window: 1 // Allow 1 period before and after for clock skew }); if (!verified) { return res.status(400).json({ error: 'Invalid verification code' }); } // Enable MFA for the user await User.findByIdAndUpdate(userId, { mfaEnabled: true }); res.json({ success: true }); }); // Login with MFA app.post('/login', async (req, res) => { const { email, password, mfaToken } = req.body; // Find user const user = await User.findOne({ email }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } // Verify password const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Invalid credentials' }); } // Check if MFA is enabled if (user.mfaEnabled) { // If no MFA token provided in initial request if (!mfaToken) { return res.status(200).json({ requireMfa: true, message: 'Please provide MFA token' }); } // Verify MFA token const verified = speakeasy.totp.verify({ secret: user.mfaSecret, encoding: 'base32', token: mfaToken, window: 1 }); if (!verified) { return res.status(401).json({ error: 'Invalid MFA token' }); } } // Generate JWT and complete login // ... res.json({ success: true, token }); });
-
Implement recovery options:
// Generate recovery codes app.post('/mfa/recovery-codes', authenticate, async (req, res) => { const userId = req.user.userId; // Generate 10 random recovery codes const recoveryCodes = Array.from({ length: 10 }, () => crypto.randomBytes(4).toString('hex').toUpperCase() ); // Hash the recovery codes before storing const hashedCodes = await Promise.all( recoveryCodes.map(code => bcrypt.hash(code, 10)) ); // Store hashed codes in user record await User.findByIdAndUpdate(userId, { mfaRecoveryCodes: hashedCodes }); // Return plain text codes to user (only shown once) res.json({ recoveryCodes }); }); // Login with recovery code app.post('/login/recovery', async (req, res) => { const { email, password, recoveryCode } = req.body; // Find user const user = await User.findOne({ email }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } // Verify password const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Invalid credentials' }); } // Verify recovery code let validRecoveryCode = false; let validCodeIndex = -1; for (let i = 0; i < user.mfaRecoveryCodes.length; i++) { if (await bcrypt.compare(recoveryCode, user.mfaRecoveryCodes[i])) { validRecoveryCode = true; validCodeIndex = i; break; } } if (!validRecoveryCode) { return res.status(401).json({ error: 'Invalid recovery code' }); } // Remove used recovery code user.mfaRecoveryCodes.splice(validCodeIndex, 1); await user.save(); // Generate JWT and complete login // ... res.json({ success: true, token, message: 'Recovery code used. Please set up MFA again.' }); });
Conclusion
Authentication security is a critical aspect of application development that requires careful attention, especially in the context of vibe coding where AI tools may prioritize functionality over security. By understanding common authentication vulnerabilities in AI-generated code and implementing secure alternatives, you can build robust authentication systems that protect your users and their data.
The key principles of secure authentication in vibe-coded applications include:
- Proper password storage using modern hashing algorithms like bcrypt or Argon2
- Brute force protection through rate limiting and account lockout mechanisms
- Secure session management with httpOnly cookies and appropriate security flags
- Input validation and sanitization to prevent injection attacks
- Multi-factor authentication to add an additional layer of security
- Secure password reset flows that don’t leak information
Each full-stack app builder—Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit—has its own authentication patterns and potential vulnerabilities. By applying platform-specific security enhancements and following best practices, you can ensure that your vibe-coded applications maintain robust authentication security while still benefiting from the rapid development that AI tools enable.
Remember that authentication is your application’s first line of defense. Investing time in securing this critical component will protect your users, your data, and your reputation from the devastating consequences of security breaches.
Additional Resources
- OWASP Authentication Cheat Sheet
- NIST Digital Identity Guidelines
- Have I Been Pwned API (for checking compromised passwords)
- Auth0 MFA Documentation
- Supabase Auth Documentation
- JWT.io (for understanding JWT structure and security)
References
- OWASP. (2023). “Top 10 Web Application Security Risks.” Retrieved from https://owasp.org/Top10/
- Security Boulevard. (2025). “AI-Generated Code Security Report.” Retrieved from https://securityboulevard.com/2025/02/ai-generated-code-security-report
- Checkmarx. (2025). “Security in Vibe Coding: Innovation Meets Risk.” Retrieved from https://checkmarx.com/blog/security-in-vibe-coding/
- Segura, T. (2025). “A Vibe Coding Security Playbook.” Infisical. Retrieved from https://infisical.com/blog/vibe-coding-security-playbook
- Trotta, F. (2025). “5 Vibe Coding Risks and Ways to Avoid Them in 2025.” Zencoder. Retrieved from https://zencoder.ai/blog/vibe-coding-risks