Prove Who They Are, Then What They Can Do
Authentication & Security
Sessions vs JWT, OAuth2/OIDC at a high level, TLS, and password hashing with bcrypt/argon2 — plus the pitfalls that turn auth into a breach.
What you'll learn
- Distinguish authentication from authorization
- Compare session cookies and JWTs and pick correctly
- Hash passwords and verify JWTs safely in Node
Two words get used interchangeably and absolutely should not be: authentication (authn) is proving who you are; authorization (authz) is deciding what you’re allowed to do. You log in once (authn); then every request checks whether this identity may perform this action (authz). Conflate them and you build systems where being logged in is mistaken for being allowed — a classic source of breaches.
This lesson is the security backbone of the section: how identity is established, carried, and protected.
Sessions vs JWT: how you carry identity
After login, the server needs to recognize the user on subsequent requests without re-asking for the password. Two dominant approaches:
| Session (cookie) | JWT (token) | |
|---|---|---|
| Where state lives | Server (session store) | Inside the token itself |
| Server lookup per request | Yes (fetch session) | No (verify signature) |
| Revocation | Easy — delete the session | Hard — valid until it expires |
| Scaling | Needs shared session store | Stateless — any node verifies |
| Size on the wire | Tiny session ID | Larger signed payload |
Session-based: the server stores session state (in Redis or a DB) and gives the client an opaque session ID in a cookie. Each request, the server looks up the session. Revoking access is trivial — delete the session — but every request costs a lookup, and the store has to be shared across your stateless app servers.
JWT (JSON Web Token): a signed token containing the claims (user id, roles, expiry) inside it. The server verifies the signature and trusts the contents — no lookup needed, so it scales beautifully across a stateless fleet. The catch is the mirror image: because there’s no server-side state, you can’t easily revoke a JWT before it expires. The standard mitigation is short-lived access tokens (minutes) paired with a longer-lived refresh token you can revoke.
OAuth2 and OIDC, briefly
When you click “Sign in with Google,” you’re using OAuth2 plus OIDC, and it’s worth knowing what each does:
- OAuth2 is an authorization framework — it lets your app get delegated access (a scoped access token) to a user’s resources on another service, without the user handing you their password.
- OIDC (OpenID Connect) is a thin authentication layer on top of OAuth2 that adds an ID token (a JWT) proving who the user is. OAuth2 alone gives permission; OIDC adds identity.
The win is that you never see the user’s Google password — you delegate authentication to an identity provider that’s better at it than you are.
TLS: the table stakes
None of the above matters if credentials travel in the clear. TLS (the S in
HTTPS) encrypts data in transit, so tokens, cookies, and passwords can’t be read
or tampered with on the wire. It’s non-negotiable, everywhere, always — and from
the gateway lesson, you typically terminate TLS at the edge. Modern TLS 1.3
is fast enough that “HTTPS is slow” hasn’t been a real excuse for years.
The JavaScript angle: hashing and verifying in Node
Two operations you’ll write in nearly every Node backend. First, the cardinal rule of passwords: never store them in plaintext, and never with a fast hash.
import bcrypt from 'bcrypt';
// On signup: hash with a work factor (cost). bcrypt salts automatically.
async function register(email, password) {
const hash = await bcrypt.hash(password, 12); // 12 = cost; tune for ~250ms
await db.users.insert({ email, passwordHash: hash });
}
// On login: compare — constant-time, so timing can't leak the answer.
async function login(email, password) {
const user = await db.users.findByEmail(email);
if (!user) return null;
const ok = await bcrypt.compare(password, user.passwordHash);
return ok ? user : null;
} The work factor is the point: bcrypt is deliberately slow (CPU-cost-hard), and argon2 — the modern preference — is also memory-hard, so even if your hash database leaks, brute-forcing it is infeasible. A fast hash like SHA-256 is the wrong tool here — GPUs crack billions of those per second. Slowness is the feature.
Second, verifying a JWT — note the explicit algorithm:
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET; // never hardcode
function issueToken(user) {
return jwt.sign(
{ sub: user.id, role: user.role },
SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }, // short-lived access token
);
}
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
try {
// Pin algorithms — this is what blocks the `alg: none` forgery.
req.auth = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
next();
} catch {
res.status(401).json({ error: 'invalid token' });
}
} That completes the Reliability, Scale & Operations toolkit — rate limiting, gateways, resilience, backpressure, observability, fault tolerance, consensus, locks, IDs, search, and security. With every building block in hand, the track turns to putting them together: real architectures, starting with the classic warm-up — designing a URL shortener.