Authentication & Security

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.

9 min read Level 3/5 #system-design#security#auth
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 livesServer (session store)Inside the token itself
Server lookup per requestYes (fetch session)No (verify signature)
RevocationEasy — delete the sessionHard — valid until it expires
ScalingNeeds shared session storeStateless — any node verifies
Size on the wireTiny session IDLarger 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.
Authentication & Security — architecture diagram

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.

Hash a password with bcrypt, verify on login script.js
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;
}
▶ Preview: console

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:

Sign and verify a JWT with a pinned algorithm script.js
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' });
  }
}
▶ Preview: console

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.