Authentication & Authorization
Sessions vs JWTs. OAuth flows. RBAC done right. Why bcrypt is non-negotiable.
A Short History of Auth
Authentication has evolved over decades, in response to scale and the changing shape of the web. Knowing this history helps you understand why each scheme exists and what problem it was actually solving.
1. Stateful sessions (1990s) — The earliest model. User logs in, server creates a session record in memory or a database, hands the user a cookie with the session ID. Every subsequent request, the server looks up the session. Worked beautifully for single-server apps. Broke the moment you added a second server (which session store does it use?).
2. Stateless tokens / JWT (2010s) — The cloud-era answer. Instead of looking up state on the server, encode the user's identity into a signed token and hand it to the client. Any server with the secret can verify it. Scales horizontally, works across services. Trade-off: you can't easily revoke a token before its expiry.
3. SAML (early 2000s) — XML-based standard for enterprise SSO. "Let employees use one corporate login across all internal apps." Heavy, verbose, but still everywhere in B2B and government. If you've seen "Sign in with Okta" at work, that's SAML or OIDC under the hood.
4. OAuth 1.0 → OAuth 2.0 (2007 → 2012) — Solves a different problem: "Let app A access my data on service B without giving app A my password for B." OAuth 1 used cryptographic signing; OAuth 2 simplified by relying on TLS and added different flows for different app types.
5. OpenID Connect / OIDC (2014) — OAuth 2.0 was about authorization (what an app can do), not authentication (who the user is). OIDC layered identity on top: now "Sign in with Google" works the same way everywhere.
Modern apps usually combine: OIDC for identity, OAuth 2 for delegated authorization, JWT for the session token, and RBAC at the application layer for fine-grained permissions.
Authentication vs Authorization
Authentication (AuthN) — WHO are you?
Proving identity. "I am Alice."
Done via: passwords, tokens, certificates, biometrics.
Authorization (AuthZ) — WHAT can you do?
Checking permissions after identity is established.
"Alice is allowed to read this document, but not delete it."
Common mistake: conflating them. Authentication failing → 401 Unauthorized. Authorization failing → 403 Forbidden. These are different HTTP status codes for a reason.
Sessions vs Tokens
Sessions (server-side state):
1. User logs in → server creates session, stores in DB/Redis
2. Server gives client a session ID (cookie)
3. Every request: client sends cookie, server looks up session in DB
Pros: Easy to invalidate. Server controls state.
Cons: Doesn't scale horizontally without shared session store.
JWTs (stateless tokens):
1. User logs in → server creates signed token containing claims
2. Client stores token (localStorage or cookie)
3. Every request: client sends token, server validates signature
Pros: Stateless, scales infinitely, works across services.
Cons: Can't be revoked without a blocklist. Tokens valid until expiry.
Production pattern: Use short-lived JWTs (15 min) + long-lived refresh tokens (7 days) stored in httpOnly cookies.
JWT Deep Dive
A JWT has three base64url-encoded parts: Header.Payload.Signature
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user_123", "role": "admin", "iat": 1699000000, "exp": 1699000900}
Signature: HMAC-SHA256(base64(header) + "." + base64(payload), secret)
The server validates by re-computing the signature and checking it matches. Tampering with the payload breaks the signature.
NEVER store sensitive data in the payload — it's base64 encoded, not encrypted. Anyone can decode and read it.
Important claims:
• sub — subject (user ID)
• iat — issued at
• exp — expiration
• iss — issuer
• aud — audience
Where to Store the JWT
Once you have a JWT, the question is: where does the client keep it? This decision matters a lot for security.
Three places clients commonly store tokens:
localStorage / sessionStorage
JavaScript can read it freely → any XSS vulnerability on your site means token theft. Convenient, but the worst from a security standpoint.
In-memory (a JS variable)
Safer than localStorage — disappears on page refresh. But that means the user has to log in again every time they close the tab. Annoying for them.
httpOnly Cookie ← the right answer for most apps
The cookie is set by the server, automatically sent on every request, and JavaScript cannot read it. XSS attacks can't steal it.
If you choose cookies, set the right flags:
• HttpOnly — JavaScript can't read it (blocks XSS theft)
• Secure — only sent over HTTPS
• SameSite=Strict (or Lax) — blocks CSRF attacks by preventing cross-site cookie sending
• Path=/ and a sensible Max-Age
res.cookie("auth", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 15 * 60 * 1000 // 15 minutes
});
Common production pattern: short-lived access JWT (15 min) in an httpOnly cookie + a long-lived refresh token (7 days) also in an httpOnly cookie. When the access token expires, the client hits a /refresh endpoint and gets a new one silently.
Password Hashing
NEVER store plain-text passwords. NEVER use MD5 or SHA-1 for passwords.
Use bcrypt, Argon2, or scrypt — these are slow by design (hardened against GPU brute-force).
bcrypt flow:
1. User sets password "hunter2"
2. bcrypt generates a random salt
3. bcrypt(password + salt) → hash (takes ~100ms intentionally)
4. Store the hash, discard the password
On login:
1. User sends "hunter2"
2. Retrieve hash from DB
3. bcrypt.compare("hunter2", storedHash) → true/false
bcrypt stores the salt in the hash itself — no separate column needed.
Cost factor: higher = slower = more secure. 10-12 is standard. Test on your hardware.
SAML — The Enterprise Veteran
SAML (Security Assertion Markup Language) is the older standard, born in the early 2000s for enterprise SSO. If your company uses Okta, Ping, or ADFS to give employees one login across many internal apps, that's almost certainly SAML.
How it works at a high level:
1. User tries to access an app (the Service Provider, or SP)
2. The app redirects them to the company's Identity Provider (IdP) — Okta, ADFS, etc.
3. User logs into the IdP (just once)
4. IdP issues a signed XML document called a SAML Assertion saying "this is Alice, here are her groups"
5. Browser POSTs the assertion to the app
6. App verifies the signature, trusts the assertion, logs Alice in
When to encounter SAML:
• You're building enterprise B2B software
• Your customer is a large company that wants employees to log in via their corporate identity
• Government, healthcare, finance — SAML is everywhere
When to NOT use SAML:
• You're building a consumer app — use OIDC (Sign in with Google) instead
• Your audience is developers — OAuth 2 + OIDC is friendlier
SAML is heavy (verbose XML, complex tooling) but battle-tested. Most SaaS B2B products implement it because enterprise buyers demand it.
OAuth 2.0 & SSO
OAuth 2.0 is not authentication — it's authorization delegation. "Allow this app to access my Google data."
Key roles:
• Resource Owner — the user
• Client — your app
• Authorization Server — Google/GitHub/etc.
• Resource Server — the API with the data
Authorization Code Flow (for web apps):
1. User clicks "Login with Google"
2. Redirect to Google with your client_id + redirect_uri
3. User grants permission
4. Google redirects back with ?code=xyz
5. Your server exchanges code for access_token + id_token (server-to-server)
6. Verify id_token, extract user info, create session
OpenID Connect (OIDC) adds identity on top of OAuth 2.0. The id_token is a JWT with user info. This IS authentication.
Never use the Implicit Flow (deprecated). Use PKCE for mobile/SPA apps.
Role-Based Access Control (RBAC)
RBAC assigns permissions to roles, then assigns roles to users.
Roles: user, moderator, admin, super_admin
Permissions: read:posts, create:posts, delete:any_post, manage:users
User has roles → roles have permissions → check if permission is granted.
Implementation:
function requirePermission(permission) {
return (req, res, next) => {
const userPermissions = getPermissions(req.user.roles);
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
More granular: ABAC (Attribute-Based) — check attributes like "user owns this resource". Combine RBAC + ABAC for production systems.
OpenID Connect — When You Actually Want Login
OAuth 2.0 was a triumph for authorization but a confusing answer for authentication. Apps started misusing the OAuth access token as a substitute for "who is this user?" — which was technically wrong and often insecure.
OpenID Connect (OIDC) was the fix. It's a thin layer on top of OAuth 2.0 that adds a proper identity token.
The key difference:
• access_token → "what can this app do on the user's behalf" (OAuth's job)
• id_token → "who is this user" (OIDC's job)
The id_token is a signed JWT with user identity claims:
• sub — stable user ID at the IdP (e.g., Google's internal user number)
• email, name, picture — identity profile fields
• iat / exp — issued and expiration times
• aud — the client_id this token was issued for
• iss — the issuer (e.g., "https://accounts.google.com")
Because it's a JWT, your app verifies the signature locally — no extra network call to validate.
"Sign in with Google", "Sign in with Apple", "Sign in with Microsoft" — all OIDC. Your auth flow:
1. Redirect user to provider with your client_id + scopes
2. Provider authenticates the user
3. Provider redirects back with an authorization code
4. Your server exchanges the code for access_token + id_token
5. Verify the id_token's signature, extract sub/email/name
6. Create a session (or issue your own JWT) using that identity
For consumer apps, OIDC is almost always what you want when someone says "let users log in with Google". You don't need OAuth 2.0 alone.
The Full Auth Stack
Here's how the pieces fit together in a typical production system:
┌─────────────────────────────┐
│ Identity Provider (IdP) │ ← Google, Okta, Auth0
│ - OIDC for user identity │
│ - Issues id_token (JWT) │
└─────────────────────────────┘
│
┌─────────────────────────────┐
│ Your Application Server │
│ - Verifies id_token │
│ - Issues session JWT │
│ - Enforces RBAC / ABAC │ ← your authZ logic lives here
└─────────────────────────────┘
│
┌─────────────────────────────┐
│ Client (browser / app) │
│ - Stores token in cookie │
│ - Sends on every request │
└─────────────────────────────┘
Mental model:
• Authentication tells you who. Get this from OIDC (or your own login).
• Authorization tells you what they're allowed to do. RBAC + ABAC live in your app.
• The token is just the carrier — it's the contents (claims, roles) that matter.
Get authentication right by delegating to a battle-tested IdP. Get authorization right by keeping it inside your app — that's where your business rules live.
The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.