Web Authentication and Security Best Practices in 2025

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.