Web Authentication and Security Best Practices in 2025
Authentication is one of the most critical parts of any web application—and one of the easiest to get wrong. Here's a comprehensive guide to implementing secure authentication in 2025.
Never Roll Your Own Crypto
This cannot be stressed enough: Do not implement your own authentication system from scratch. Use proven libraries and services:
If you must implement it yourself, use battle-tested libraries.
Password Security
Hashing
Always hash passwords with a slow, adaptive algorithm:
// Use bcrypt, scrypt, or Argon2
const bcrypt = require('bcrypt');
// Hash password
const saltRounds = 12; // Increase over time as hardware improves
const hash = await bcrypt.hash(password, saltRounds);
// Verify password
const isValid = await bcrypt.compare(password, hash);
Never use MD5, SHA-1, or SHA-256 for passwords. They're too fast and vulnerable to brute-force attacks.
Password Requirements
Balance security with usability:
function isPasswordSecure(password) {
return (
password.length >= 12 && // Minimum length
/[a-z]/.test(password) && // Lowercase
/[A-Z]/.test(password) && // Uppercase
/\d/.test(password) && // Number
/[!@#$%^&*]/.test(password) // Special char
);
}
But consider:
- Allowing passphrases ("correct horse battery staple")
- Using Have I Been Pwned API to check for compromised passwords
- Implementing account lockout after failed attempts
JWT Best Practices
JSON Web Tokens are popular but often misused:
Store Securely
// ❌ Bad: localStorage is vulnerable to XSS
localStorage.setItem('token', token);
// ✅ Good: httpOnly cookies prevent XSS
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
Keep Tokens Short-Lived
// Short-lived access token
const accessToken = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// Long-lived refresh token
const refreshToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
Implement Token Refresh
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.cookies;
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
const user = await User.findById(payload.userId);
// Check token version (allows invalidating all tokens)
if (user.tokenVersion !== payload.tokenVersion) {
throw new Error('Invalid token');
}
const newAccessToken = generateAccessToken(user);
res.cookie('token', newAccessToken, cookieOptions);
res.json({ success: true });
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
OAuth 2.0 / Social Login
Let users sign in with existing accounts:
// Using Passport.js
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName
});
}
done(null, user);
}));
Multi-Factor Authentication (MFA)
Add an extra layer of security:
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate secret
const secret = speakeasy.generateSecret({
name: 'MyApp ([email protected])'
});
// Generate QR code
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
// Verify code
const isValid = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: userEnteredCode,
window: 2 // Allow 2 time steps before/after
});
Session Management
Secure Session Configuration
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No client-side access
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
Implement Session Timeout
app.use((req, res, next) => {
if (req.session.lastActivity) {
const inactiveTime = Date.now() - req.session.lastActivity;
if (inactiveTime > 30 * 60 * 1000) { // 30 minutes
req.session.destroy();
return res.redirect('/login');
}
}
req.session.lastActivity = Date.now();
next();
});
CSRF Protection
Protect against Cross-Site Request Forgery:
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/form', csrfProtection, (req, res) => {
// Process form
});
Rate Limiting
Prevent brute-force attacks:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/login', loginLimiter, async (req, res) => {
// Handle login
});
Security Headers
Use Helmet for security headers:
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Input Validation
Never trust user input:
const { body, validationResult } = require('express-validator');
app.post('/register',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }),
body('username').trim().isAlphanumeric(),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process registration
}
);
Logging and Monitoring
Log authentication events:
function logAuthEvent(type, userId, success, metadata = {}) {
logger.info({
type, // 'login', 'logout', 'register', 'password_reset'
userId,
success,
timestamp: new Date(),
ip: metadata.ip,
userAgent: metadata.userAgent
});
}
// Alert on suspicious activity
if (failedLoginAttempts > 10) {
alertSecurity('Multiple failed login attempts', { userId, ip });
}
Security Checklist
- [ ] Use HTTPS everywhere
- [ ] Hash passwords with bcrypt/Argon2
- [ ] Implement rate limiting
- [ ] Use httpOnly cookies
- [ ] Add CSRF protection
- [ ] Set secure headers
- [ ] Validate all inputs
- [ ] Implement MFA
- [ ] Use short-lived tokens
- [ ] Log security events
- [ ] Regular security audits
- [ ] Keep dependencies updated
Stay Vigilant
Security is not a one-time task. Stay informed:
- Subscribe to security mailing lists
- Use tools like Snyk to scan dependencies
- Run regular penetration tests
- Monitor for unusual activity
Need help securing your application? Contact us for a security audit.