Skip to content

JWT Authentication in Express

JSON Web Tokens (JWTs) are the most common way to implement stateless authentication in Express REST APIs. This lesson builds a complete login and protected-route flow using the jsonwebtoken package.

How JWT Authentication Works

A JWT is a compact, signed string encoding a payload (like a user ID) and an expiration time. The server signs it with a secret key at login time and hands it to the client; the client sends it back, usually in an Authorization: Bearer <token> header, on every subsequent request.

Because the signature can be verified without any database lookup, the server doesn't need to store session state, any server instance holding the same secret can verify any valid token.

const jwt = require('jsonwebtoken');

app.post('/login', async (req, res) => {
  const user = await verifyCredentials(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
});

expiresIn: '1h' limits how long the token is valid, reducing the impact if a token is ever leaked.

Verifying Tokens in Middleware

function authenticate(req, res, next) {
  const header = req.headers.authorization;
  const token = header?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Missing token' });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}
  • The Authorization header conventionally follows the Bearer <token> format.
  • jwt.verify() throws if the signature is invalid or the token has expired.
  • Attach the decoded payload to req.user so downstream handlers can access the current user's identity.
  • Keep the JWT secret in an environment variable, never hardcoded in source code.

JWT Authentication Cheatsheet

The jsonwebtoken API you will use for most JWT flows.

Task Code
Install npm install jsonwebtoken
Sign a token jwt.sign(payload, secret, { expiresIn: '1h' })
Verify a token jwt.verify(token, secret)
Decode without verifying jwt.decode(token)
Protect a route app.get('/me', authenticate, handler)
Read Authorization header req.headers.authorization

Protecting Routes With the Middleware

Once the authenticate middleware is defined, protecting any route is a one-line addition to its middleware chain.

app.get('/me', authenticate, (req, res) => {
  res.json({ userId: req.user.sub });
});

app.use('/api/admin', authenticate, requireAdmin, adminRouter);

Access Tokens and Refresh Tokens

Short-lived access tokens (minutes to an hour) limit exposure if leaked, but require a way to renew them without forcing the user to log in again. A common pattern issues a longer-lived, more carefully guarded refresh token (often in an httpOnly cookie) that can be exchanged for a new access token.

app.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  try {
    const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    const accessToken = jwt.sign({ sub: payload.sub }, process.env.JWT_SECRET, { expiresIn: '15m' });
    res.json({ accessToken });
  } catch {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Common Mistakes

  • Storing JWTs in localStorage for browser apps, exposing them to XSS attacks, prefer httpOnly cookies when possible.
  • Using a long-lived access token with no refresh flow, forcing an awkward tradeoff between security and user experience.
  • Hardcoding the JWT secret in source code instead of an environment variable.
  • Not handling jwt.verify() throwing on expired tokens, crashing the request instead of returning a clean 401.

Key Takeaways

  • JWTs let servers verify identity without a database lookup or server-side session storage.
  • The Authorization: Bearer <token> header is the standard way to send a JWT on each request.
  • Short-lived access tokens paired with a refresh token balance security and user experience.
  • Always keep JWT secrets in environment variables, never in source code.

Pro Tip

Set access token expiry short (15 minutes to 1 hour) and rely on a refresh token flow for longer sessions, this limits the damage window if an access token is ever intercepted.