Skip to content
Express.js ex auth 4 min read

Authentication Overview

Authentication is how your Express app answers a single deceptively simple question: who is making this request? Express ships with no built-in identity layer, which is a feature, not a gap — it leaves you free to bolt on exactly the strategy your application needs, from server-rendered session cookies to stateless JSON Web Tokens to delegated OAuth logins. This page maps the landscape so you can pick the right approach before writing a line of credential-handling code.

Authentication vs. authorization

These two words are constantly confused, but they solve different problems and usually run as separate middleware in the request pipeline.

  • Authentication establishes identity. It verifies a credential — a password, a token, a session cookie — and decides who the caller is.
  • Authorization establishes permission. Given a known identity, it decides what that caller is allowed to do.

In practice authentication runs first and attaches the resolved user to the request; authorization then inspects that user.

// Authentication: who are you?
function authenticate(req, res, next) {
  const user = resolveUserFromCredential(req); // session, token, etc.
  if (!user) return res.status(401).json({ error: "Unauthenticated" });
  req.user = user;
  next();
}

// Authorization: are you allowed?
function requireAdmin(req, res, next) {
  if (req.user.role !== "admin") {
    return res.status(403).json({ error: "Forbidden" });
  }
  next();
}

app.delete("/posts/:id", authenticate, requireAdmin, async (req, res) => {
  await db.posts.delete(req.params.id);
  res.status(204).end();
});

Tip: Return 401 Unauthorized when the caller is unknown (missing or invalid credential) and 403 Forbidden when the caller is known but lacks rights. Swapping these status codes is a classic and confusing bug.

Stateful vs. stateless

The deepest architectural fork in authentication is where the truth about a session lives.

Stateful authentication keeps session data on the server (in memory, Redis, or a database) and hands the client only an opaque session ID — typically inside an HTTP-only cookie. Every request looks the session up server-side.

Stateless authentication puts the identity claims inside the credential itself, signed so they cannot be tampered with. A JWT is the canonical example: the server verifies the signature and trusts the embedded claims without any lookup.

TraitStateful (sessions)Stateless (JWT)
Where state livesServer store (Redis/DB)Inside the token
Server lookup per requestYesNo (signature check only)
Instant revocationEasy — delete the sessionHard — needs a denylist
Horizontal scalingNeeds shared storeTrivial, no shared state
Token size on the wireTiny (session ID)Larger (encoded claims)
Natural fitRendered web appsAPIs, microservices, mobile

The trade-off is fundamentally revocation versus scalability. Sessions let you kill access immediately but require a shared store across your servers. JWTs scale effortlessly but you cannot un-issue one before it expires without extra machinery.

Common strategies

A quick reference for the approaches covered in this section:

StrategyBest forCredential carried as
Session cookieRendered web apps, same-originHTTP-only cookie + server store
JWT (access token)Stateless APIs, SPAs, mobileAuthorization: Bearer <token>
JWT + refresh tokenLong-lived sessions over an APIShort access token + rotating refresh token
OAuth / social login”Sign in with Google/GitHub”Delegated provider token
API keyService-to-service, machine clientsX-API-Key header or secret

Choosing for APIs vs. rendered apps

The decisive question is what kind of client you serve.

If you render HTML on the server (EJS, Pug) and the browser is your only client, session cookies are the natural choice. The browser stores and resends the cookie automatically, HTTP-only cookies are invisible to JavaScript (mitigating XSS token theft), and revocation is one store deletion away. Reach for express-session backed by Redis.

If you expose a JSON API consumed by single-page apps, mobile apps, or other services, statelessness shines. A JWT sent as a Bearer token avoids per-request session lookups and frees you from a shared session store — the foundation of horizontally scalable, multi-service backends. Pair short-lived access tokens with refresh tokens to balance security against re-login friction.

Here is a minimal Bearer-token guard for an API route:

const jwt = require("jsonwebtoken");

function requireToken(req, res, next) {
  const header = req.headers.authorization || "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) return res.status(401).json({ error: "Missing token" });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ error: "Invalid or expired token" });
  }
}

app.get("/api/me", requireToken, (req, res) => {
  res.json({ id: req.user.sub, email: req.user.email });
});

Output:

$ curl -H "Authorization: Bearer eyJhbGci..." http://localhost:3000/api/me
{"id":"u_123","email":"[email protected]"}

$ curl http://localhost:3000/api/me
{"error":"Missing token"}

Warning: Never store raw passwords or sign tokens with a hardcoded secret. Hash passwords with bcrypt and load signing secrets from environment variables — a leaked secret lets anyone forge valid tokens for every user.

Best Practices

  • Always serve authentication endpoints over HTTPS so credentials and tokens are never sent in cleartext.
  • Hash passwords with a slow, salted algorithm like bcrypt — never store or compare them in plaintext.
  • Store session IDs and refresh tokens in HttpOnly, Secure, SameSite cookies to blunt XSS and CSRF.
  • Keep JWT access tokens short-lived (minutes) and use rotating refresh tokens for longevity.
  • Distinguish 401 (who are you?) from 403 (you may not) consistently across the API.
  • Centralize auth in reusable middleware and apply it at the router level rather than repeating it per route.
  • Load all secrets from environment variables and rotate them if a leak is suspected.
Last updated June 14, 2026
Was this helpful?