Notifications

No notifications

/Phase 3

Authentication & JWT

Authentication — Who Are You?

Authentication verifies identity. Authorization checks permissions.

Auth Methods

MethodHow It WorksBest For
Session/CookieServer stores session, client gets cookieTraditional web apps
JWT (Token)Client stores signed tokenAPIs, SPAs, mobile apps
OAuth 2.0Delegate auth to Google/GitHubSocial login
API KeysStatic key in headerServer-to-server

JWT Flow

1. Client → POST /login { email, password }
2. Server → Verify credentials
3. Server → Create JWT with user info
4. Server → Send JWT to client
5. Client → Store JWT (localStorage/cookie)
6. Client → Send JWT in Authorization header
7. Server → Verify JWT on each request

JWT Structure

header.payload.signature

eyJhbGciOiJIUzI1NiJ9. ← Header (algorithm) eyJ1c2VySWQiOjEsInJvbGUi. ← Payload (user data) SflKxwRJSMeKKF2QT4fwp... ← Signature (verification)

Password Security

❌ Never✅ Always
Store plain text passwordsHash with bcrypt
Use MD5/SHA1 for passwordsUse bcrypt (salt + hash)
Send passwords in URLsSend in request body over HTTPS

On this page

Detailed Theory

Authentication answers "who are you?". Authorization answers "what are you allowed to do?". They are different jobs and they fail in different ways. Mixing them up is the most common cause of security incidents in junior code.

What Auth Actually Is

When a request hits your API, the server needs to:

1. Identify the caller (read a token / cookie / API key). 2. Verify that the credential is genuine (signature valid, not expired). 3. Authorize the action (does this user own this post? do they have role "admin"?).

Missing any of the three = a vulnerability. "401 vs 403" maps cleanly: 401 = step 1 or 2 failed, 403 = step 3 failed.

Two Big Schools: Sessions vs Tokens

Sessions (cookie + server store)Tokens (JWT)
Where state livesServer (Redis, DB)Inside the token
Revoke a userDelete their session rowHard — token is valid until it expires
Cross-domain APIsAwkward (CORS + cookies)Easy (Authorization header)
Mobile / 3rd-party clientsPainfulDesigned for it
Default for browser-only apps✅ Often the right choiceOverkill

Real apps often mix: short-lived JWT access tokens for the API + a session-ish refresh token in an httpOnly cookie.

Step 1 — Storing Passwords (Hash, Don't Encrypt)

Never store raw passwords. Never use MD5 or SHA-256 directly (too fast — attackers brute-force billions/sec on a GPU). Use a slow, salted hash:

  • bcrypt — battle-tested, simple, default for most Node apps.
  • argon2 — newer winner of the Password Hashing Competition, what new projects should pick.
  • scrypt — built into Node, also fine.
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(plainPassword, 12); // cost factor 12
const ok   = await bcrypt.compare(plainPassword, hash);

The cost factor (rounds) controls slowness. Higher = safer but slower login. Pick the highest value that keeps login under ~250ms on your server.

Cost~TimeUse case
10~65msOld default
12~250msRecommended today
14~1sParanoid / very sensitive

Step 2 — JWT Anatomy

A JWT is three Base64-encoded parts joined by dots:

xxxxx.yyyyy.zzzzz
header.payload.signature

  • Header{ "alg": "HS256", "typ": "JWT" }
  • Payload — your claims (sub, exp, role, etc.) — readable by anyone, just Base64.
  • SignatureHMAC(secret, header + payload) — proves the payload has not been tampered with.
const jwt = require('jsonwebtoken');
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, {
  expiresIn: '15m',
  issuer: 'campuscrate',
  audience: 'web',
});

const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if bad

Key rule: JWT is signed, not encrypted. Do not put passwords, full names, addresses, or anything sensitive in the payload — anyone with the token can decode it.

Beginner Mistakes to Skip

1. Storing JWTs in localStorage — any XSS bug = stolen token. Use httpOnly + Secure + SameSite=Strict cookies for browsers. 2. No expirationexpiresIn is mandatory. Long-lived tokens you cannot revoke = nightmare. 3. Using a weak / hard-coded secret — must be ≥ 32 random bytes, from process.env, never committed. 4. Same secret across environments — dev, staging, prod each get their own. 5. Comparing tokens or hashes with === — use crypto.timingSafeEqual to avoid timing attacks. 6. Telling users "wrong password" vs "unknown email" — leaks which emails are registered. Always say "invalid credentials".

Intermediate: Refresh Token Pattern

Short access tokens are great for security but annoying for users (re-login every 15min). The fix:

Login → access token (15m, JSON)  +  refresh token (7d, httpOnly cookie)
API call → send access token in Authorization header
401 expired → call /auth/refresh → server reads cookie, issues new access token
Logout → server deletes the refresh token from its store

Rotate refresh tokens on every use (one-time-use). If the *same* refresh token is replayed, assume it was stolen and revoke the whole family — this is called *refresh token reuse detection*.

Intermediate: A Clean auth() Middleware

function auth(requiredRole) {
  return (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: 'NO_TOKEN' });
    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET);
      req.user = payload;
      if (requiredRole && payload.role !== requiredRole) {
        return res.status(403).json({ error: 'FORBIDDEN' });
      }
      next();
    } catch {
      res.status(401).json({ error: 'INVALID_TOKEN' });
    }
  };
}

app.get('/me', auth(), (req, res) => res.json(req.user)); app.get('/admin/stats', auth('admin'), adminController);

Intermediate: Login Hardening

The login endpoint is the #1 brute-force target. Always layer:

  • Rate limit per IP (express-rate-limit, e.g. 10 attempts / 15min).
  • Per-account lockout after N failures (with exponential cooldown).
  • CAPTCHA after suspicious behaviour.
  • Generic errors so you do not leak which emails exist.
  • Log every failed attempt with IP + email hash for SIEM.

Intermediate: Authorization Patterns

  • RBAC (Role-Based) — role: 'admin'
    'editor'
    'viewer'
    . Easy, covers 80% of apps.
  • ABAC (Attribute-Based) — rules over user + resource attributes (user.team === post.team && action === 'edit').
  • Ownership checks — even with RBAC, every resource handler should re-check resource.userId === req.user.sub. This is the most-missed check in real bugs.

Advanced: OAuth 2.0 & OIDC in One Page

  • OAuth 2.0 = a framework for *delegated authorization* ("let this app post tweets for me").
  • OIDC = a thin identity layer on top of OAuth 2.0 — adds an ID token (JWT) describing the user. This is what "Login with Google" actually uses.
  • Authorization Code flow + PKCE is the only flow you should use for SPAs and mobile apps today. Implicit flow is dead.
  • The redirect dance: app → provider login → callback URL with one-time code → server exchanges code for tokens.
Use libraries (passport, openid-client, NextAuth, Auth0, Clerk) — never roll your own.

Advanced: Multi-Factor & Modern Auth

  • TOTP (Google Authenticator) — server stores a secret, validates rotating 6-digit codes.
  • WebAuthn / Passkeys — phishing-resistant, no shared secret, increasingly the gold standard.
  • Magic links / OTP via email — UX-friendly for low-risk apps.
  • Step-up auth — require MFA only for sensitive actions (changing password, large transfer).

Advanced: Token Revocation Strategies

JWTs are *bearer tokens*: whoever holds it is treated as the user. To support "log out everywhere":

  • Keep a denylist of revoked token IDs (jti) in Redis with TTL = token's remaining lifetime.
  • Or store a tokenVersion on the user — verify it matches the JWT's tokenVersion claim. Logout = increment, all old tokens fail.

Practice Path

1. Implement POST /signup and POST /login with bcrypt cost 12 and JWT (15m). 2. Add refresh tokens stored as httpOnly cookies + a POST /auth/refresh endpoint. 3. Add auth(role) middleware and protect GET /admin routes; double-check ownership in resource handlers. 4. Add express-rate-limit to login and confirm a brute-force script gets blocked after 10 tries.