Skip to content

Authentication Middleware for Express — Building Reusable Auth Components in Node.js

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Authentication Middleware for Express. We cover key concepts, practical examples, and best practices to help you master this topic.

Express authentication middleware intercepts incoming requests before route handlers, validating credentials and attaching user context to the request object for downstream use.

What You'll Learn

Building Express middleware for JWT verification, API key validation, role-based access control, composing middleware chains, and handling authentication errors gracefully.

Why It Matters

Express middleware is the standard pattern for authentication in Node.js APIs. Centralizing auth logic in middleware prevents repetition, ensures consistency, and simplifies testing.

Real-World Use

Express.js powers APIs for companies like PayPal, Uber, and MySpace. Durga Antivirus Pro uses Express middleware for its dashboard API, combining JWT, API key, and session-based auth strategies.

Code Example: JWT Authentication Middleware

const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret';

function authenticateJWT(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'UNAUTHORIZED',
      message: 'Missing or invalid Authorization header'
    });
  }

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

  try {
    const decoded = jwt.verify(token, JWT_SECRET, {
      algorithms: ['HS256'],
      issuer: 'auth.durga-antivirus.com'
    });

    // Attach user to request
    req.user = {
      id: decoded.sub,
      roles: decoded.roles || [],
      scopes: (decoded.scope || '').split(' '),
      sessionId: decoded.jti
    };

    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({
        error: 'TOKEN_EXPIRED',
        message: 'Access token has expired'
      });
    }

    return res.status(401).json({
      error: 'INVALID_TOKEN',
      message: 'Token validation failed'
    });
  }
}

Code Example: Role-Based Authorization Middleware

// Role-based access control middleware
function requireRole(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        error: 'UNAUTHORIZED',
        message: 'Authentication required'
      });
    }

    const userRoles = req.user.roles;
    const hasRole = allowedRoles.some(role => userRoles.includes(role));

    if (!hasRole) {
      return res.status(403).json({
        error: 'FORBIDDEN',
        message: `Requires one of roles: ${allowedRoles.join(', ')}`,
        userRoles: userRoles
      });
    }

    next();
  };
}

// Scope-based authorization middleware
function requireScope(requiredScope) {
  return (req, res, next) => {
    if (!req.user || !req.user.scopes) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    if (!req.user.scopes.includes(requiredScope)) {
      return res.status(403).json({
        error: 'INSUFFICIENT_SCOPE',
        required: requiredScope,
        granted: req.user.scopes
      });
    }

    next();
  };
}

// Route usage
router.get('/api/v1/threats',
  authenticateJWT,
  requireRole('analyst', 'admin'),
  requireScope('threat:read'),
  threatController.list
);

router.post('/api/v1/threats',
  authenticateJWT,
  requireRole('admin'),
  requireScope('threat:write'),
  threatController.create
);

Code Example: Composable Auth Middleware Chain

const compose = (...middlewares) => {
  return (req, res, next) => {
    let index = 0;

    const execute = (err) => {
      if (err) return next(err);

      const middleware = middlewares[index++];
      if (!middleware) return next();

      try {
        middleware(req, res, execute);
      } catch (error) {
        next(error);
      }
    };

    execute();
  };
};

// Auth strategy factory — supports multiple auth methods
function createAuthMiddleware(strategies = []) {
  return async (req, res, next) => {
    for (const strategy of strategies) {
      const result = await strategy.authenticate(req);

      if (result.authenticated) {
        req.user = result.user;
        req.authStrategy = strategy.name;
        return next();
      }

      if (result.challenge) {
        // Strategy can respond with WWW-Authenticate header
        res.set('WWW-Authenticate', result.challenge);
      }
    }

    return res.status(401).json({
      error: 'UNAUTHORIZED',
      message: 'No valid authentication provided'
    });
  };
}

// Define strategies
const jwtStrategy = {
  name: 'jwt',
  authenticate: async (req) => {
    const header = req.headers.authorization;
    if (!header || !header.startsWith('Bearer ')) {
      return { authenticated: false };
    }
    try {
      const decoded = jwt.verify(header.split(' ')[1], JWT_SECRET);
      return { authenticated: true, user: { id: decoded.sub, roles: decoded.roles } };
    } catch {
      return { authenticated: false };
    }
  }
};

const apiKeyStrategy = {
  name: 'api_key',
  authenticate: async (req) => {
    const key = req.headers['x-api-key'];
    if (!key) return { authenticated: false };
    const keyData = await validateApiKey(key);
    if (keyData) {
      return { authenticated: true, user: { id: keyData.service, roles: ['service'] } };
    }
    return { authenticated: false };
  }
};

// Use composable auth
app.use('/api', createAuthMiddleware([jwtStrategy, apiKeyStrategy]));

Common Mistakes

1. Not Using try-catch with jwt.verify

jwt.verify throws on invalid tokens. Without try-catch, the error propagates to Express error handlers, potentially leaking stack traces.

2. Ignoring Token Algorithm

Always specify the allowed algorithms (algorithms: ['HS256']). Otherwise, an attacker can craft a token with algorithm 'none' and bypass verification.

3. Modifying req.body in Middleware

Middleware should attach to req.user, not modify req.body. Modifying the body can break downstream Parsing and validation.

4. Not Returning After res.status().json()

Without the return statement, Express continues to route handlers after sending an error response, causing multiple response attempts.

5. Applying Middleware Globally When Not Needed

Global middleware applies to every route. Use route-level middleware for auth to allow public endpoints (health, login) to bypass authentication.

Practice Questions

  1. How does Express middleware pass user data to route handlers?
  2. What is the difference between role-based and scope-based authorization?
  3. How does middleware composition work for multiple auth strategies?
  4. Why should jwt.verify specify allowed algorithms?
  5. What happens without the return statement after res.status().json()?

Answers:

  1. Middleware attaches data to the req object (req.user = decoded). Route handlers access this data via the same req parameter.
  2. Role-based checks the user's role (admin, analyst). Scope-based checks granular permissions (threat:read). Roles are broad categories; scopes are specific permissions.
  3. Each Strategy in the array attempts authentication. If one succeeds, the user is authenticated. If all fail, a 401 is returned. This allows multiple auth methods.
  4. If algorithms are not specified, an attacker can change the algorithm in the JWT header to 'none' and send a token with no signature, which jwt.verify accepts without verification.
  5. Express continues executing subsequent middleware and route handlers. The response may be attempted multiple times, causing "Cannot set headers after they are sent" errors.

Challenge: Build an Express auth middleware suite with JWT, API key, and Basic Auth strategies, composable chains, role and scope enforcement, and proper error responses.

FAQ

Should JWT middleware decode the token on every request?

Yes. JWT verification (signature check) takes microseconds. Stateless verification allows horizontal scaling without shared session state.

How do I test Express auth middleware?

Use supertest with valid tokens, expired tokens, missing headers, and malformed tokens. Assert the correct status code and error message for each case.

Can middleware handle refresh tokens?

No. Middleware validates access tokens. Refresh logic belongs in a separate route handler that accepts the refresh token and returns new access tokens.

How do I handle CORS with auth middleware?

CORS middleware must run before auth middleware. Preflight OPTIONS requests should not require authentication.

Should middleware be async or sync?

Use async middleware for database lookups (API key validation). Use sync middleware for JWT verification which is purely computational.

Mini Project

Build an Express API with composable auth middleware supporting JWT, API keys, and Basic Auth, with role and scope enforcement, proper error responses, and a test suite using supertest.

What's Next

Now learn about Authentication Middleware for FastAPI for building reusable auth components in Python APIs.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro