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.