Express
05 / 07

Authentication & Security

Express Authentication & Security

Security is not optional. Every production Express API needs authentication, input validation, rate limiting, and proper security headers. This page covers the essential patterns and packages for building secure Express applications.

JWT Authentication

JSON Web Tokens (JWT) are a stateless authentication mechanism. The server signs a token containing user claims; the client sends it with every request. No session store required.

npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const JWT_SECRET = process.env.JWT_SECRET; // Must be a long random string
const JWT_EXPIRES_IN = '7d';

// Register endpoint
app.post('/api/auth/register', async (req, res, next) => {
  try {
    const { email, password, name } = req.body;
    const hashedPassword = await bcrypt.hash(password, 12);
    const user = await User.create({ email, password: hashedPassword, name });
    const token = jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
    res.status(201).json({ token, user: { id: user.id, email, name } });
  } catch (err) { next(err); }
});

// Login endpoint
app.post('/api/auth/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    const user = await User.findByEmail(email);
    if (!user || !(await bcrypt.compare(password, user.password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    const token = jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
    res.json({ token });
  } catch (err) { next(err); }
});

// JWT middleware
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
};

// Role-based authorization
const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }
  next();
};

app.get('/api/admin/stats', authenticate, authorize('admin'), (req, res) => {
  res.json({ stats: 'admin only data' });
});

Input Validation

Never trust client input. Validate and sanitize every request body, query parameter, and route param. Zod and express-validator are the most popular choices.

// Using Zod for validation
const { z } = require('zod');

const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
  age: z.number().int().min(18).max(120).optional(),
});

// Validation middleware factory
const validate = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.errors.map(e => ({
        field: e.path.join('.'),
        message: e.message,
      })),
    });
  }
  req.validated = result.data;  // Attach validated + typed data
  next();
};

app.post('/api/users', validate(createUserSchema), async (req, res, next) => {
  try {
    const user = await User.create(req.validated);
    res.status(201).json(user);
  } catch (err) { next(err); }
});

// Query param validation
const listUsersSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  search: z.string().optional(),
});

const validateQuery = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.query);
  if (!result.success) return res.status(400).json({ error: 'Invalid query params' });
  req.query = result.data;
  next();
};

Rate Limiting & Security Headers

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

// Security headers (XSS, clickjacking, MIME sniffing, etc.)
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", 'https://fonts.googleapis.com'],
      imgSrc: ["'self'", 'data:', 'https:'],
    },
  },
}));

// General API rate limit
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 min window
  max: 100,
  message: { error: 'Too many requests, retry after 15 minutes' },
});
app.use('/api/', apiLimiter);

// Stricter limit for auth endpoints (prevent brute force)
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour window
  max: 10,                   // 10 attempts per hour
  skipSuccessfulRequests: true,  // Only count failures
  message: { error: 'Too many failed attempts, try again in an hour' },
});
app.use('/api/auth/', authLimiter);

// Prevent large payload attacks
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// HTTPS redirect in production
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect(301, 'https://' + req.headers.host + req.url);
  }
  next();
});

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free