The Engineering Codex/Application Security Engineering
DAY 2 · PM
04 / 09

Authentication & Authorization

schedule6 minsignal_cellular_altIntermediate1,323 words
Sessions vs JWTs, OAuth 2.0 and OIDC without the hand-waving, MFA in practice, and the difference between RBAC and ABAC. The two halves engineers most often confuse — and what to do about it.

What you will learn

01Authentication — Proving Identity
02OAuth 2.0 — Delegated Access
03Authorization — The Hard Half
04The Most Common AuthN/AuthZ Mistakes

Authentication and authorization are easy to say in one breath. Treating them as one thing is the most common security bug on the internet. Both OWASP A01 Broken Access Control and A07 Identification & Authentication Failures exist because someone wrote a check that conflated logged in with allowed.

Authentication — Proving Identity

Authentication asks who are you? and demands evidence. Evidence comes in factors:

  • Something you know — password, PIN.
  • Something you have — phone, hardware key (YubiKey), trusted device.
  • Something you are — fingerprint, face, voice.

True multi-factor authentication mixes different factors — password + hardware key, not password + security question. SMS one-time codes count as "something you have," but are weak: SIM-swap attacks have stolen tens of millions of dollars in the past five years. NIST 800-63B has effectively deprecated SMS for high-assurance use.

MethodPhishing-resistant?StrengthNotes
Password aloneWeakReuse + breach lists make most accounts vulnerable
Password + SMS OTPBetterVulnerable to SIM swap, real-time phishing
Password + TOTP (authenticator app)❌ (real-time phishable)GoodStandard for most apps
Password + WebAuthn / passkeyStrongOrigin-bound; phishing-proof
Passkey only (passwordless)StrongWebAuthn level 3; the modern target

Sessions — The Workhorse

The simplest scheme: after login, the server issues a random opaque token, sets a cookie, and stores the session record. Every request, the cookie comes back; the server looks it up. Logging out invalidates the session row — instantly.

Session cookie opaque · server-stored · revocable browser server + DB sid=8a4f…3c2 (random 256-bit) Logout = DELETE session row. Effect is immediate. JWT (bearer token) self-contained · stateless · not easy to revoke client resource server eyJhbGciOi… · payload · signature Logout = client deletes token. Server can't revoke before expiry.
Sessions vs JWTs. The trade-off is statefulness vs distributability — both are correct in different contexts.

Cookie attributes are non-negotiable in 2025:

HTTP
Set-Cookie: sid=...; Secure; HttpOnly; SameSite=Lax;
            Path=/; Max-Age=86400
  • Secure — only over TLS.
  • HttpOnly — invisible to JavaScript (mitigates XSS theft).
  • SameSite=Lax (or Strict) — blocks cross-site requests from carrying it (CSRF defense).
  • __Host- prefix for sensitive cookies — forces Secure + Path=/ + no Domain.

JWTs — Useful, Routinely Misused

A JWT is three base64 parts: header, payload, signature. The signature is over the first two with either HMAC (HS256, shared secret) or a public-key signature (RS256, ES256, EdDSA). The payload is readable — JWTs encode, they do not encrypt.

JWT
eyJhbGciOiJFUzI1NiIsImtpZCI6IjE0In0.eyJzdWIiOiJ1XzQyIiwic2NvcGUiOiJyZWFkOm9yZGVycyIsImV4cCI6MTcwOTI4MDAwMH0.MEUCIQDk…
# header              payload                                                signature
🚫
JWT pitfalls
1) Trusting the alg header — historic libraries accepted alg: none or downgraded RS256 to HS256 using the public key as a shared secret. Always pin the algorithm server-side. 2) No revocation — until the token expires, it works. Keep access-token TTL short (5–15 min) and use refresh tokens. 3) Storing in localStorage — readable by any XSS payload. Prefer HttpOnly cookies. 4) Algorithm confusion — verify with the public key, never the public key as if it were a secret.

When to Pick Which

Pick sessions when
  • Single-domain web app
  • You need instant logout / revocation
  • You're fine with a session store (Redis, Postgres)
  • You want the smallest attack surface
Pick JWTs when
  • Service-to-service across orgs / clouds
  • Mobile or third-party API access
  • Stateless verification matters (CDN, edge)
  • You can keep TTLs short and use a refresh-token rotation

OAuth 2.0 — Delegated Access

OAuth answers a different question: how does an app act on a user's behalf at another service, without handling the user's password? Authorization Code with PKCE is the only flow you should reach for in 2025 (yes, even for SPAs).

User Client App Auth Server 1. clicks "Sign in with X" 2. redirect with code_challenge (PKCE) 3. user logs in & consents 4. browser redirected back with ?code=... 5. POST code + code_verifier 6. access_token + refresh_token + id_token PKCE binds steps 2 and 5: only the client that started the flow can complete it. Defeats code-interception attacks.
OAuth 2.0 Authorization Code with PKCE — six steps, three actors, one phishing-resistant code exchange.

OIDC — Authentication on Top of OAuth

OAuth 2.0 grants an access token; it doesn't tell you who the user is. OpenID Connect (OIDC) layers on top: the token endpoint also returns an id_token — a signed JWT with the user's identity (sub, email, name). Always validate iss, aud, exp, and the signature against the IdP's JWKS.

Authorization — The Hard Half

Now the user is identified. What may they actually do? Two dominant models, plus a third for cross-cutting policy.

RBAC — Role-Based

Users get one or more roles; roles are bundles of permissions. Simple to reason about. Right answer for most apps.

policy
# roles → permissions
role viewer  → orders:read
role manager → orders:read, orders:refund
role admin   → orders:*, users:*

ABAC — Attribute-Based

Permissions are computed from attributes of the user, the resource, and the context. Necessary when policy depends on data values, not just on who you are.

policy (Rego-style)
allow if user.dept == order.dept
   and order.amount < 50000
   and request.time.hour < 22

ReBAC — Relationship-Based

Permissions follow graph edges ("X is in folder Y, which is shared with team Z"). Google's Zanzibar paper popularised it; engines like SpiceDB, OpenFGA, and AuthZed implement it. Right answer for collaborative apps (Drive, GitHub, Notion).

Active recall
A SaaS app's URL is /api/orders/{id}. Auth middleware verifies the JWT and lets the request through. The handler queries SELECT * FROM orders WHERE id = $1. Why is this broken?
Show answer
No object-level authorization. The middleware proved the user is logged in (AuthN). The handler never checked that order $1 belongs to the caller (AuthZ). Every authenticated user can read every order — IDOR. Fix: WHERE id = $1 AND tenant_id = $2 AND owner_id = $3, with all three parameters derived from the verified JWT, never from the request body.

The Most Common AuthN/AuthZ Mistakes

  • No object-level authorization. The cause of nearly every IDOR.
  • Trusting client-side checks. "Hide the admin button if not admin" — useful UX, useless security. Always re-check on the server.
  • Mass assignment. Updating UserUpdate{...} with req.body directly. Attacker adds "role": "admin" in the JSON. Use allow-lists.
  • JWTs without revocation. A leaked token is valid until expiry. Keep TTLs short; track a denylist for forced logout.
  • Open redirect on callback. Allowing arbitrary redirect_uri values turns OAuth into a phishing primitive. Strict allow-list of registered URIs.
Flashcard
What is PKCE protecting against in the OAuth flow, and why is it now required even for confidential clients?
Click to flip ↻
Answer
PKCE binds the authorization request and the token exchange with a per-flow secret (code_verifier). It defeats authorization-code interception on mobile/SPAs (and on confidential clients via redirect-URI bugs). OAuth 2.1 makes PKCE mandatory for all flows. Always implement it; it's free.
Mnemonic — every authorized request
"Who, What, Which, When."
  • Who — verify the token (signature, exp, iss, aud)
  • What — the action's required role/scope
  • Which — the specific resource: tenant, owner, status
  • When — context: IP, time, MFA-fresh-within-N-min
🔑
Key takeaways
1) AuthN proves identity; AuthZ grants access. They are separate checks at separate layers. 2) Sessions are revocable; JWTs are stateless — pick the trade-off you actually need. 3) Cookies must be Secure, HttpOnly, SameSite. JWT alg must be pinned server-side. 4) OAuth 2.0 + PKCE + OIDC is the default for delegated identity. 5) Every protected request must answer Who, What, Which, When — never assume the previous middleware did your authorization for you.

Finished reading?