TechLead
Lesson 5 of 9
5 min read
Web Security

Authentication Security

Implement secure authentication with password hashing, session management, JWTs, and multi-factor authentication.

Secure Authentication

Authentication verifies user identity. Poor authentication implementation is a leading cause of security breaches. Let's explore best practices.

Password Security

1. Password Hashing with bcrypt

const bcrypt = require('bcrypt');

// NEVER store plain text passwords!
// BAD:
const user = { password: req.body.password }; // NEVER DO THIS

// GOOD: Hash passwords before storing
async function hashPassword(password) {
  const saltRounds = 12; // Higher = more secure but slower
  return bcrypt.hash(password, saltRounds);
}

async function createUser(email, password) {
  const passwordHash = await hashPassword(password);

  return db.user.create({
    data: {
      email,
      passwordHash, // Store the hash, never the plain password
    },
  });
}

// Verify password
async function verifyPassword(password, hash) {
  return bcrypt.compare(password, hash);
}

async function login(email, password) {
  const user = await db.user.findUnique({ where: { email } });

  if (!user) {
    // Use same error to prevent user enumeration
    throw new Error('Invalid credentials');
  }

  const valid = await verifyPassword(password, user.passwordHash);

  if (!valid) {
    throw new Error('Invalid credentials');
  }

  return user;
}

2. Password Requirements

import { z } from 'zod';

const passwordSchema = z.string()
  .min(12, 'Password must be at least 12 characters')
  .regex(/[A-Z]/, 'Password must contain uppercase letter')
  .regex(/[a-z]/, 'Password must contain lowercase letter')
  .regex(/[0-9]/, 'Password must contain number')
  .regex(/[^A-Za-z0-9]/, 'Password must contain special character')
  .refine(
    (pwd) => !commonPasswords.includes(pwd.toLowerCase()),
    'Password is too common'
  );

// Check against breached passwords using Have I Been Pwned API
async function isPasswordBreached(password) {
  const crypto = require('crypto');
  const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
  const prefix = hash.slice(0, 5);
  const suffix = hash.slice(5);

  const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
  const text = await response.text();

  return text.includes(suffix);
}

Session Management

const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const redisClient = redis.createClient();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  name: 'sessionId', // Don't use default 'connect.sid'
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,       // Only HTTPS
    httpOnly: true,     // No JavaScript access
    sameSite: 'strict', // CSRF protection
    maxAge: 3600000,    // 1 hour
  },
}));

// Regenerate session on login to prevent fixation
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body);

  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: 'Session error' });

    req.session.userId = user.id;
    req.session.loginTime = Date.now();
    res.json({ success: true });
  });
});

// Destroy session on logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    res.clearCookie('sessionId');
    res.json({ success: true });
  });
});

JWT Authentication

const jwt = require('jsonwebtoken');

// Generate tokens
function generateTokens(userId) {
  const accessToken = jwt.sign(
    { userId, type: 'access' },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  return { accessToken, refreshToken };
}

// Verify access token
function verifyAccessToken(token) {
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    if (payload.type !== 'access') throw new Error('Invalid token type');
    return payload;
  } catch (error) {
    throw new Error('Invalid token');
  }
}

// Middleware
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];

  try {
    req.user = verifyAccessToken(token);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

// Refresh token endpoint
app.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  try {
    const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);

    // Check if token is revoked (store in Redis/DB)
    const isRevoked = await isTokenRevoked(refreshToken);
    if (isRevoked) throw new Error('Token revoked');

    const tokens = generateTokens(payload.userId);
    res.json(tokens);
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Multi-Factor Authentication (MFA)

const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// Generate TOTP secret
async function enableMFA(userId) {
  const secret = speakeasy.generateSecret({
    name: `MyApp (${user.email})`,
    issuer: 'MyApp',
  });

  // Store secret securely (encrypted)
  await db.user.update({
    where: { id: userId },
    data: { mfaSecret: encrypt(secret.base32) },
  });

  // Generate QR code for authenticator app
  const qrCode = await QRCode.toDataURL(secret.otpauth_url);

  return { secret: secret.base32, qrCode };
}

// Verify TOTP code
function verifyMFACode(secret, code) {
  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token: code,
    window: 1, // Allow 1 step before/after
  });
}

// Login with MFA
async function loginWithMFA(email, password, mfaCode) {
  const user = await authenticate(email, password);

  if (user.mfaEnabled) {
    const secret = decrypt(user.mfaSecret);
    const valid = verifyMFACode(secret, mfaCode);

    if (!valid) {
      throw new Error('Invalid MFA code');
    }
  }

  return generateTokens(user.id);
}

Rate Limiting

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// Strict rate limiting for auth endpoints
const authLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: { error: 'Too many login attempts, try again later' },
  standardHeaders: true,
  legacyHeaders: false,
});

app.post('/login', authLimiter, loginHandler);
app.post('/register', authLimiter, registerHandler);
app.post('/forgot-password', authLimiter, forgotPasswordHandler);

Security Checklist

  • Hash passwords with bcrypt (cost factor 12+)
  • Enforce strong password requirements
  • Check passwords against breach databases
  • Use secure session cookies (httpOnly, secure, sameSite)
  • Implement short-lived access tokens + refresh tokens
  • Offer and encourage MFA
  • Rate limit authentication endpoints
  • Log authentication events
  • Use account lockout after failed attempts

Continue Learning