Advanced25 min read

Auth Middleware & Protected Routes

Create authentication middleware that protects API routes by verifying JWT tokens on every request.

The Authorization Header

When a user logs in and receives a JWT, the question becomes: how does the client send that token back to the server on every subsequent request? The answer is the HTTP Authorization header.

The standard format uses the Bearer scheme:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSJ9.abc123signature

The word "Bearer" indicates that the token grants access to whoever "bears" (carries) it — similar to a concert ticket. You do not need to prove your identity beyond presenting the token. This is why keeping tokens secure is critical.

On the server side, you extract the token from the Authorization header, verify it, and use the decoded payload to identify the user. This extraction and verification happens in middleware so you do not have to repeat it in every single route handler.

Other places to send tokens:

  • Cookies (specifically httpOnly cookies) — More secure against XSS attacks because JavaScript cannot read httpOnly cookies. The browser automatically sends them with every request. Preferred for browser-based apps.
  • Query parameters?token=eyJhb... — Generally not recommended because URLs are logged in server access logs, browser history, and can be shared accidentally. Only used for special cases like one-time password reset links.
  • Custom headers — Some APIs use custom headers like X-Auth-Token. This works but is non-standard and may cause issues with certain proxies and CDNs.

The Authorization: Bearer header is the standard for REST APIs and is what most frontend libraries (Axios, Fetch) expect to use. Cookies are preferred when your frontend and backend share the same domain.

Building Auth Middleware

Authentication middleware is a function that sits between the incoming request and your route handler. Its job is to verify the JWT and attach the user's data to the request object before the route handler runs. If the token is missing or invalid, the middleware short-circuits the request and returns an error response.

Here is the step-by-step logic for auth middleware:

Step 1: Get the Authorization header. Access it via req.headers.authorization. Headers in Express are automatically lowercased, so Authorization becomes authorization.

Step 2: Check the Bearer prefix. The header should start with "Bearer " (note the space). If it does not, the format is wrong — return 401.

Step 3: Extract the token. Split the header string by space and take the second part: authHeader.split(' ')[1].

Step 4: Verify the token. Use jwt.verify(token, secret) inside a try/catch block. If the token is expired, tampered with, or signed with a different secret, jwt.verify throws an error.

Step 5: Attach user data to the request. Set req.user = decoded where decoded is the JWT payload containing the user's id, email, and role. This makes user data available to all downstream middleware and route handlers.

Step 6: Call next(). Pass control to the next middleware or route handler. The request continues normally.

If any step fails — no header, wrong format, invalid token, expired token — the middleware returns a 401 Unauthorized response and does not call next(). This stops the request from reaching the route handler.

The 401 status code specifically means "you need to authenticate" (missing or bad credentials). Do not confuse it with 403 Forbidden, which means "you are authenticated but not allowed" (wrong permissions).

Complete Auth Middleware Implementation

javascript
const jwt = require('jsonwebtoken');

// ---- Main auth middleware ----
const authMiddleware = async (req, res, next) => {
  try {
    // Step 1: Get the Authorization header
    const authHeader = req.headers.authorization;

    // Step 2: Check if header exists and has Bearer prefix
    if (!authHeader?.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'No token provided' });
    }

    // Step 3: Extract the token (everything after "Bearer ")
    const token = authHeader.split(' ')[1];

    // Step 4: Verify the token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // Step 5: Attach user data to the request
    req.user = decoded;
    // Now req.user = { id: '...', email: '...', role: '...', iat: ..., exp: ... }

    // Step 6: Pass control to the next middleware/route
    next();
  } catch (error) {
    // Token is invalid or expired
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
};

// ---- Usage in routes ----
const express = require('express');
const router = express.Router();

// Public routes — no auth needed
router.post('/login', loginHandler);
router.post('/signup', signupHandler);
router.get('/products', getAllProducts);

// Protected routes — auth required
router.get('/profile', authMiddleware, (req, res) => {
  // req.user is available here because authMiddleware verified the token
  res.json({ user: req.user });
});

router.put('/profile', authMiddleware, (req, res) => {
  // Update user profile using req.user.id to find the right user
  updateProfile(req.user.id, req.body);
});

// Apply to all routes in a group
const protectedRouter = express.Router();
protectedRouter.use(authMiddleware); // All routes below require auth
protectedRouter.get('/dashboard', getDashboard);
protectedRouter.get('/settings', getSettings);
protectedRouter.get('/notifications', getNotifications);
app.use('/api', protectedRouter);

Protecting Routes

Not all routes in your API need authentication. Public endpoints like login, signup, product listings, and health checks should be accessible to everyone. Private endpoints like user dashboards, account settings, and admin panels must be protected.

Three strategies for applying auth middleware:

1. Per-route middleware — Apply to individual routes that need protection. This gives you the most granular control:

javascript
router.get('/profile', authMiddleware, getProfile);
router.delete('/account', authMiddleware, deleteAccount);

2. Router-level middleware — Apply to all routes on a specific router. Use this when you have a group of routes that all require authentication:

javascript
const protectedRouter = express.Router();
protectedRouter.use(authMiddleware);
protectedRouter.get('/dashboard', getDashboard);
protectedRouter.get('/settings', getSettings);
app.use('/api/protected', protectedRouter);

3. Global middleware with exclusions — Apply middleware globally and skip it for specific routes. Useful when most routes are protected:

javascript
const publicPaths = ['/api/login', '/api/signup', '/api/products'];
app.use((req, res, next) => {
  if (publicPaths.includes(req.path)) return next();
  authMiddleware(req, res, next);
});

After authentication, access user data via req.user. The auth middleware attached the decoded JWT payload, so you can get the user's ID with req.user.id, their email with req.user.email, and their role with req.user.role. This is how your route handlers know WHO is making the request without querying the database on every request. If you need more user data than what is in the JWT (like their full profile), use req.user.id to fetch it from the database within the route handler.

Optional Authentication

Some routes should work for both authenticated and unauthenticated users, but behave differently depending on who is making the request. For example, an e-commerce product listing might show the same products to everyone, but display personalized recommendations or saved-item indicators to logged-in users. A blog post page might show the article to everyone, but display edit/delete buttons only to the author.

For these cases, create optional auth middleware that tries to verify the token but does not reject the request if verification fails:

javascript
const optionalAuth = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    req.user = null; // No token — user is anonymous
    return next();   // Continue anyway!
  }
  const token = authHeader.split(' ')[1];
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
  } catch (error) {
    req.user = null; // Invalid token — treat as anonymous
  }
  next(); // Always continue, never return 401
};

In your route handler, check if req.user exists:

javascript
router.get('/products', optionalAuth, (req, res) => {
  const products = getProducts();
  if (req.user) {
    // Logged in — add personalized data
    products.forEach(p => p.isSaved = user.savedItems.includes(p.id));
  }
  res.json(products);
});

The key difference: standard auth middleware returns 401 and stops the request. Optional auth middleware always calls next() — it just sets req.user to the decoded payload or null. This pattern is extremely common in production APIs where you want to progressively enhance the experience for authenticated users without blocking anonymous access entirely.

What HTTP status code should you return for an invalid or missing auth token?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.