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 Unauthorizedwhen the caller is unknown (missing or invalid credential) and403 Forbiddenwhen 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.
| Trait | Stateful (sessions) | Stateless (JWT) |
|---|---|---|
| Where state lives | Server store (Redis/DB) | Inside the token |
| Server lookup per request | Yes | No (signature check only) |
| Instant revocation | Easy — delete the session | Hard — needs a denylist |
| Horizontal scaling | Needs shared store | Trivial, no shared state |
| Token size on the wire | Tiny (session ID) | Larger (encoded claims) |
| Natural fit | Rendered web apps | APIs, 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:
| Strategy | Best for | Credential carried as |
|---|---|---|
| Session cookie | Rendered web apps, same-origin | HTTP-only cookie + server store |
| JWT (access token) | Stateless APIs, SPAs, mobile | Authorization: Bearer <token> |
| JWT + refresh token | Long-lived sessions over an API | Short access token + rotating refresh token |
| OAuth / social login | ”Sign in with Google/GitHub” | Delegated provider token |
| API key | Service-to-service, machine clients | X-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,SameSitecookies to blunt XSS and CSRF. - Keep JWT access tokens short-lived (minutes) and use rotating refresh tokens for longevity.
- Distinguish
401(who are you?) from403(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.