Authentication β Cookies, Sessions, JWT & OAuth
Understand how web applications authenticate users and maintain secure sessions using cookies, sessions, JWT, and OAuth, along with common authentication flows, security practices, and third-party login.

π Authentication β Cookies, Sessions, JWT & OAuth
A complete, interview-ready guide. Read top to bottom once, then use the Interview Q&A and Cheat Sheet sections for revision.
#Table of Contents#
- Fundamentals: Authentication vs Authorization
- HTTP is Stateless β Why Auth Needs State
- Password Handling (Hashing, Salting)
- Cookies
- Web Storage vs Cookies
- Sessions (Stateful Auth)
- JWT (Stateless Auth)
- Access Token + Refresh Token Pattern
- Session vs JWT β Comparison
- OAuth 2.0
- OpenID Connect (OIDC)
- SSO, SAML, MFA, Passkeys
- Common Attacks & Defenses
- Authorization Models (RBAC, ABAC)
- Auth in React / Next.js / Node.js
- Best Practices Checklist
- Interview Q&A
- Cheat Sheet
1. Fundamentals
| Term | Meaning | Question it answers |
|---|---|---|
| Identification | Claiming an identity (username/email) | "Who do you say you are?" |
| Authentication (AuthN) | Verifying that identity (password, OTP, biometrics) | "Are you really who you claim?" |
| Authorization (AuthZ) | Deciding what an authenticated identity may do | "What are you allowed to do?" |
- Authentication happens first, authorization after.
- HTTP status codes:
401 Unauthorizedβ actually means unauthenticated (not logged in / invalid credentials).403 Forbiddenβ authenticated, but not permitted.
Authentication factors
- Something you know β password, PIN
- Something you have β phone (OTP), hardware key, authenticator app
- Something you are β fingerprint, face
- MFA / 2FA = combining two or more different factors.
2. HTTP is Stateless
- Every HTTP request is independent; the server does not remember previous requests.
- After login, the server must recognize the user on every next request. Solutions:
- Session ID in a cookie (server remembers state) β stateful
- Token (JWT) sent with each request (token carries state) β stateless
Login βββΊ Server verifies credentials βββΊ issues "proof" (session ID or JWT)
Next requests βββΊ client sends the proof βββΊ server verifies the proof
3. Password Handling
Never store plain-text passwords. Never use reversible encryption. Always hash.
Hashing vs Encryption vs Encoding
| Reversible? | Purpose | Example | |
|---|---|---|---|
| Encoding | Yes (no key) | Data format | Base64, URL encoding |
| Encryption | Yes (with key) | Confidentiality | AES, RSA |
| Hashing | No (one-way) | Integrity / password storage | SHA-256, bcrypt, Argon2 |
Good password hashing algorithms (slow on purpose)
- bcrypt, scrypt, Argon2 (recommended), PBKDF2
- β Not MD5 / SHA-1 / plain SHA-256 (too fast β brute-forceable)
Salt & Pepper
- Salt: random unique value per password, stored alongside the hash β defeats rainbow tables and makes identical passwords hash differently.
- Pepper: secret value kept outside the database (env/HSM) added to all passwords.
- Work factor / cost: tunable slowness (bcrypt rounds β 10β12+).
import bcrypt from "bcrypt";
// Register
const hash = await bcrypt.hash(plainPassword, 12); // salt is generated & embedded in hash
await db.users.create({ email, passwordHash: hash });
// Login
const user = await db.users.findOne({ email });
const ok = user && await bcrypt.compare(plainPassword, user.passwordHash);
if (!ok) return res.status(401).json({ message: "Invalid credentials" }); // generic message!
Tips
- Return a generic error ("Invalid email or password") β don't reveal which one was wrong (prevents user enumeration).
- Add rate limiting, account lockout / backoff, and CAPTCHA to slow brute force.
4. Cookies
A cookie is a small piece of data (β€ ~4 KB) the server asks the browser to store and automatically send back on every matching request.
How it works
Server response: Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
Browser request: Cookie: sid=abc123
Cookie attributes (very important)
| Attribute | Purpose |
|---|---|
Expires / Max-Age | Lifetime. Without it β session cookie (deleted when browser closes) |
Domain | Which domain(s) receive the cookie (includes subdomains if set) |
Path | URL path scope |
HttpOnly | JS cannot read it (document.cookie) β protects against XSS token theft |
Secure | Sent only over HTTPS |
SameSite | Controls cross-site sending β protects against CSRF |
Partitioned (CHIPS) | Per-top-level-site cookie jar for third-party cookies |
__Host- / __Secure- prefixes | Enforce Secure / Path=/ / no Domain for extra safety |
SameSite values
Strictβ sent only for same-site requests. Safest, but links from other sites arrive logged-out.Lax(browser default now) β also sent on top-level GET navigations (clicking a link), not on cross-site POST/iframes/fetch.Noneβ sent everywhere; requiresSecure. Needed for cross-site embeds / third-party use.
Same-site β same-origin. Site = scheme + registrable domain (eTLD+1).
app.example.comandapi.example.comare the same site but different origins.
Types of cookies
- Session cookie (no expiry) vs Persistent cookie (has expiry)
- First-party vs Third-party (set by another domain; increasingly blocked)
- Secure / HttpOnly / SameSite cookies (flag-based)
- Signed cookies (tamper-evident via HMAC; e.g.,
cookie-parserwith secret)
Express example
res.cookie("token", value, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 1000, // 1 hour
path: "/",
});
res.clearCookie("token"); // logout
Cookies + CORS (frequent gotcha)
// Server
app.use(cors({ origin: "https://app.example.com", credentials: true }));
// origin cannot be "*" when credentials are used
// Client
fetch("https://api.example.com/me", { credentials: "include" });
// axios: axios.get(url, { withCredentials: true })
Pros / Cons
β
Automatic sending, HttpOnly protects from XSS, built-in expiry
β Vulnerable to CSRF (because sent automatically), 4 KB limit, sent on every request (overhead), tricky across domains
5. Web Storage vs Cookies
| Feature | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| Size | ~4 KB | ~5β10 MB | ~5 MB |
| Sent to server automatically | Yes | No | No |
| Accessible by JS | No if HttpOnly | Yes | Yes |
| Expiry | Configurable | Never (until cleared) | Tab close |
| XSS risk | Token safe if HttpOnly | Token stealable | Token stealable |
| CSRF risk | Yes | No | No |
Rule of thumb: For auth tokens prefer HttpOnly Secure SameSite cookies. Storing JWTs in localStorage exposes them to any XSS.
6. Sessions
Session-based authentication = the server stores user state; the client holds only a session ID.
Flow
1. User submits credentials βββββββββββββββΊ Server
2. Server verifies, creates session {userId, role, ...} in a session store
3. Server responds: Set-Cookie: sid=<random-id> (HttpOnly; Secure)
4. Browser sends Cookie: sid=<id> on each request
5. Server looks up sid in store β identifies user
6. Logout β server deletes session from store + clears cookie
Session ID requirements
- Long, random, unpredictable (β₯ 128 bits entropy, from a CSPRNG)
- Meaningless by itself (just a pointer / lookup key)
- Rotated on login/privilege change
Session stores
| Store | Notes |
|---|---|
| Memory (default) | Dev only β lost on restart, doesn't scale, leaks memory |
| Redis / Memcached | Fast, TTL support, shared across servers β production standard |
| Database (SQL/Mongo) | Persistent, slower |
Express + express-session + Redis
import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";
const redisClient = createClient();
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET, // signs the session cookie
resave: false, // don't save unchanged sessions
saveUninitialized: false, // don't create session until something stored
cookie: { httpOnly: true, secure: true, sameSite: "lax", maxAge: 1000 * 60 * 60 },
}));
app.post("/login", async (req, res) => {
const user = await verifyCredentials(req.body);
if (!user) return res.sendStatus(401);
req.session.regenerate(err => { // prevent session fixation
req.session.userId = user.id;
res.json({ ok: true });
});
});
app.post("/logout", (req, res) => {
req.session.destroy(() => {
res.clearCookie("connect.sid");
res.sendStatus(204);
});
});
Session timeouts
- Idle (sliding) timeout β expires after inactivity
- Absolute timeout β expires after fixed time regardless of activity
Scaling sessions
- Multiple servers need a shared store (Redis) or sticky sessions (load balancer pins user to a server β less resilient).
Pros / Cons
β Easy revocation (delete the session), small cookie, server has full control, simple to reason about β Server state & storage lookup per request, needs shared store to scale, CSRF protection needed, less natural for mobile/cross-domain APIs
7. JWT (JSON Web Token)
A JWT (RFC 7519) is a compact, URL-safe, signed token that carries claims. Servers can verify it without a database lookup β stateless.
Structure: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. β Header (Base64URL)
eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ. β Payload (Base64URL)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c β Signature
| Part | Contains | Example |
|---|---|---|
| Header | Algorithm & type | { "alg": "HS256", "typ": "JWT" } |
| Payload | Claims | { "sub": "123", "role": "admin", "iat": 1700000000, "exp": 1700000900 } |
| Signature | HMAC/RSA(base64(header) + "." + base64(payload), key) | Proves integrity & authenticity |
β οΈ JWT payload is only Base64URL-encoded, NOT encrypted. Anyone can decode it. Never put secrets/passwords/PII inside. (If you need confidentiality β JWE.)
Standard (registered) claims
| Claim | Meaning |
|---|---|
iss | Issuer |
sub | Subject (user ID) |
aud | Audience (intended recipient) |
exp | Expiration time |
nbf | Not before |
iat | Issued at |
jti | Unique token ID (useful for revocation/replay prevention) |
Signing algorithms
| Type | Algorithm | Key | Use case |
|---|---|---|---|
| Symmetric | HS256 | One shared secret signs & verifies | Single service / monolith |
| Asymmetric | RS256, ES256 | Private key signs, public key verifies | Microservices, third parties verifying tokens, JWKS |
- Asymmetric lets many services verify without being able to issue tokens.
- Public keys are often published at a JWKS endpoint (
/.well-known/jwks.json), identified bykidheader.
JWT flow
1. Login βββΊ Server validates credentials βββΊ signs JWT
2. Client stores token (cookie / memory)
3. Client sends: Authorization: Bearer <token> (or cookie)
4. Server verifies signature + exp + iss + aud βββΊ trusts claims
Node.js example
import jwt from "jsonwebtoken";
// Sign
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "15m", issuer: "my-app", audience: "my-app-users", algorithm: "HS256" }
);
// Verify middleware
function auth(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) return res.sendStatus(401);
try {
req.user = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ["HS256"], // ALWAYS pin the algorithm
issuer: "my-app",
audience: "my-app-users",
});
next();
} catch (e) {
return res.status(401).json({ message: e.name }); // TokenExpiredError, JsonWebTokenError
}
}
Where to store JWT on the client?
| Location | XSS | CSRF | Verdict |
|---|---|---|---|
localStorage / sessionStorage | β Vulnerable | β Safe | Avoid for sensitive apps |
| HttpOnly cookie | β Safe | β Needs mitigation (SameSite/CSRF token) | Recommended |
| In-memory (JS variable) | β Mostly safe | β Safe | Good for access tokens; lost on refresh (restore via refresh cookie) |
JWT security pitfalls
alg: noneattack β attacker removes signature. Fix: whitelist algorithms.- Algorithm confusion (RS256 β HS256) β attacker signs with the public key as HMAC secret. Fix: pin the algorithm per key.
- Weak HMAC secret β brute-forceable. Use β₯ 256-bit random secret.
- Not validating
exp,iss,aud. - Long-lived access tokens β big window if stolen.
- Sensitive data in payload.
- Hard to revoke (see below).
Revoking JWTs (the core weakness)
Because JWTs are self-contained, you can't "delete" them server-side. Mitigations:
- Short expiry (5β15 min) + refresh tokens
- Denylist / blocklist of
jtiin Redis until natural expiry - Token versioning β store
tokenVersionon user; bump it to invalidate all old tokens - Rotate signing keys (nuclear option)
Adding a denylist/DB check on every request makes JWTs effectively stateful again β tradeoff.
8. Access + Refresh Token Pattern
| Access Token | Refresh Token | |
|---|---|---|
| Purpose | Call APIs | Get new access tokens |
| Lifetime | Short (5β15 min) | Long (daysβweeks) |
| Sent to | Every API request | Only the /refresh endpoint |
| Storage | Memory / HttpOnly cookie | HttpOnly Secure cookie (path-restricted) |
| Stored on server? | No | Yes (hashed in DB) β revocable |
Flow
1. Login β server returns accessToken (15m) + refreshToken (7d, HttpOnly cookie)
2. Client calls API with accessToken
3. accessToken expires β API returns 401
4. Client calls POST /auth/refresh (cookie auto-sent) β new accessToken
5. Refresh expired / revoked β user must log in again
Refresh Token Rotation (best practice)
- Each refresh issues a new refresh token and invalidates the old one.
- Reuse detection: if an old (already-used) refresh token is presented β assume theft β revoke the whole token family and force re-login.
app.post("/auth/refresh", async (req, res) => {
const rt = req.cookies.refreshToken;
if (!rt) return res.sendStatus(401);
const stored = await db.refreshTokens.findOne({ hash: sha256(rt) });
if (!stored || stored.revoked || stored.expiresAt < Date.now()) {
if (stored?.revoked) await revokeFamily(stored.familyId); // reuse detected
return res.sendStatus(401);
}
await db.refreshTokens.update(stored.id, { revoked: true }); // rotate
const newRt = await issueRefreshToken(stored.userId, stored.familyId);
const newAt = signAccessToken(stored.userId);
res.cookie("refreshToken", newRt, { httpOnly: true, secure: true, sameSite: "strict", path: "/auth/refresh" });
res.json({ accessToken: newAt });
});
Frontend: auto-refresh with Axios interceptor
api.interceptors.response.use(
r => r,
async error => {
const original = error.config;
if (error.response?.status === 401 && !original._retry) {
original._retry = true;
const { data } = await api.post("/auth/refresh");
setAccessToken(data.accessToken);
original.headers.Authorization = `Bearer ${data.accessToken}`;
return api(original);
}
return Promise.reject(error);
}
);
9. Session vs JWT
| Aspect | Session (cookie + server store) | JWT (stateless token) |
|---|---|---|
| State | Server-side | Client-side (self-contained) |
| Client holds | Opaque session ID | Full signed token with claims |
| Per-request cost | Store lookup (Redis/DB) | Signature verification (CPU only) |
| Revocation | Easy (delete session) | Hard (needs denylist/short expiry) |
| Scalability | Needs shared store/sticky sessions | Scales horizontally easily |
| Size | Tiny cookie | Larger (grows with claims) |
| Cross-domain / mobile / microservices | Harder | Easier |
| Typical storage | Cookie | Cookie / memory / (storage) |
| CSRF | Vulnerable (cookie) | Vulnerable only if in cookie; not if in Authorization header |
| XSS | Safe with HttpOnly | Depends on storage |
| Best for | Traditional web apps, server-rendered, need instant logout | APIs, SPAs + mobile, microservices, service-to-service |
Reality check: Many production systems use both β e.g., a JWT carried inside an HttpOnly cookie, or opaque tokens introspected by an auth server. "JWT vs session" isn't strictly either/or.
Opaque token vs JWT: Opaque = random string, meaning only known by the auth server (needs introspection endpoint); JWT = self-describing and locally verifiable.
10. OAuth 2.0
OAuth 2.0 (RFC 6749) is an authorization framework that lets an app access a user's resources on another service without sharing the user's password.
OAuth 2.0 is for AUTHORIZATION (delegated access), NOT authentication. Authentication ("Login with Google") is added by OpenID Connect on top of OAuth.
Real-world analogy
A hotel key card: you (resource owner) ask reception (authorization server) for a card (access token) that opens only your room (scope) for a limited time β you don't hand over the master key (password).
Roles
| Role | Description | Example |
|---|---|---|
| Resource Owner | The user who owns the data | You |
| Client | App requesting access | Your web app / "Trello" |
| Authorization Server | Authenticates user, issues tokens | Google Accounts / Auth0 / Keycloak |
| Resource Server | API holding the data | Google Drive API |
Key terms
- Access Token β credential to call the resource server (short-lived)
- Refresh Token β obtain new access tokens
- Scope β permission set requested (
read:email,profile,drive.readonly) - Client ID / Client Secret β identify the app (secret only for confidential clients)
- Redirect URI β where the auth server sends the user back (must be pre-registered & exactly matched)
- Consent screen β user approves requested scopes
- State β random value to prevent CSRF in the OAuth flow
Client types
- Confidential β can keep a secret (server-side apps)
- Public β can't keep secrets (SPAs, mobile, desktop) β must use PKCE
Grant types (flows)
| Grant | Use case | Status |
|---|---|---|
| Authorization Code (+ PKCE) | Web apps, SPAs, mobile β user login | β Recommended |
| Client Credentials | Machine-to-machine (no user) | β |
| Device Code | TVs, CLIs, IoT with limited input | β |
| Refresh Token | Renew access tokens | β |
| Implicit | Old SPAs (token in URL fragment) | β Deprecated |
| Resource Owner Password (ROPC) | App collects username/password | β Deprecated |
π Authorization Code Flow (most important)
User Client App Authorization Server Resource Server
β click "Login with Google" β β
ββββββββββββββΊβ β β
β β 1. Redirect to /authorize? β
β β response_type=code&client_id=...& β
β β redirect_uri=...&scope=...&state=xyz β
βββββββββββββββΌββββββββββββββββββββΊβ β
β 2. User logs in & grants consent β β
ββββββββββββββββββββββββββββββββββΊ β β
β 3. Redirect back: redirect_uri?code=AUTH_CODE&state=xyz β
ββββββββββββββΊβ β β
β β 4. POST /token (code + client_secret/verifier) β
β βββββββββββββββββββββΊβ β
β β 5. access_token (+ refresh_token, id_token) β
β ββββββββββββββββββββββ β
β β 6. GET /api Authorization: Bearer <access> β
β βββββββββββββββββββββββββββββββββββββββββββββββββΊβ
β β 7. Protected data β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ
Why a code first, then a token? The browser front-channel only sees a short-lived, single-use code. The real token is exchanged over a secure back-channel (server-to-server), so the access token never passes through the browser URL/history.
PKCE (Proof Key for Code Exchange, "pixie")
Protects public clients from authorization code interception.
- Client generates random
code_verifier. - Computes
code_challenge = BASE64URL(SHA256(code_verifier)). - Sends
code_challenge(+code_challenge_method=S256) in/authorize. - Sends the original
code_verifierin/token. - Server hashes the verifier & compares β only the legit client can redeem the code.
Now recommended for all clients (OAuth 2.1 makes PKCE mandatory).
Authorization request example
GET https://accounts.google.com/o/oauth2/v2/auth
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=https://myapp.com/callback
&scope=openid%20email%20profile
&state=RANDOM_STATE
&code_challenge=CHALLENGE
&code_challenge_method=S256
Token request example
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://myapp.com/callback
&client_id=CLIENT_ID
&client_secret=SECRET (confidential clients)
&code_verifier=VERIFIER (PKCE)
Express callback sketch
app.get("/auth/google/callback", async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.oauthState) return res.status(400).send("Invalid state");
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
grant_type: "authorization_code",
}),
});
const tokens = await tokenRes.json(); // access_token, id_token, refresh_token
// Verify id_token, find/create local user, then create YOUR OWN session/JWT
});
Common library: Passport.js (
passport-google-oauth20), NextAuth/Auth.js, Auth0, Clerk, Keycloak.
Client Credentials Flow (machine-to-machine)
Service A ββPOST /token (grant_type=client_credentials, client_id, client_secret, scope)βββΊ Auth Server
Service A βββ access_token ββ ; then calls Service B with Bearer token
OAuth security points
- Always validate
state(CSRF) and exact redirect URI (prevents open-redirect/code theft). - Use PKCE for public clients; never embed client secret in frontend/mobile code.
- Request least-privilege scopes.
- Use short-lived access tokens, rotate refresh tokens.
- Store tokens securely; use HTTPS everywhere.
11. OpenID Connect (OIDC)
OIDC = identity (authentication) layer built on top of OAuth 2.0.
| OAuth 2.0 | OIDC | |
|---|---|---|
| Purpose | Authorization (access to APIs) | Authentication (who is the user) |
| Main token | Access token | ID Token (a JWT) + access token |
| Scope | custom (read:files) | must include openid (plus profile, email) |
| Extra | β | /userinfo endpoint, discovery document |
ID Token (JWT) claims
iss, sub (unique user ID), aud (client ID), exp, iat, nonce, email, name, pictureβ¦
Validating an ID token
- Verify signature using the provider's JWKS
- Check
iss= expected provider - Check
aud= your client ID - Check
expnot expired - Check
noncematches the one you sent (prevents replay)
Discovery
https://provider/.well-known/openid-configuration β lists endpoints, supported scopes, JWKS URI.
Access token β ID token. ID token is for the client (proves login); access token is for the API. Don't send the ID token to APIs.
12. SSO, SAML, MFA, Passkeys
SSO (Single Sign-On)
Log in once, access many apps (e.g., Google Workspace). Implemented with SAML, OIDC, or CAS.
SAML 2.0
- XML-based, enterprise SSO standard. Roles: IdP (identity provider) and SP (service provider).
- Sends signed assertions. Older, heavier; common in corporate/legacy.
- OIDC is JSON/REST/mobile-friendly; SAML is XML/enterprise.
MFA / 2FA
- TOTP (Time-based OTP) β Google Authenticator; code = HMAC(secret, time-step ~30s)
- SMS/Email OTP β convenient, weaker (SIM-swap)
- Push notifications, hardware keys (FIDO2/YubiKey) β strongest
Passkeys / WebAuthn (FIDO2)
- Public-key crypto: private key stays on device; server stores public key.
- Phishing-resistant, passwordless; user verifies via biometric/PIN.
Magic links & Passwordless
Single-use, short-lived emailed link/token; treat like a password reset flow.
API Keys vs Tokens
- API key: static identifier for an app/project; weak identity guarantee, no user context.
- Bearer token: short-lived, scoped, tied to user/client.
Basic Auth
Authorization: Basic base64(user:pass) β credentials on every request; only over HTTPS; avoid for user-facing apps.
13. Common Attacks & Defenses
| Attack | What happens | Defense |
|---|---|---|
| XSS (Cross-Site Scripting) | Injected JS steals tokens / acts as user | Escape/sanitize output, CSP, HttpOnly cookies, avoid dangerouslySetInnerHTML |
| CSRF (Cross-Site Request Forgery) | Malicious site makes browser send authenticated cookie request | SameSite cookies, CSRF tokens (synchronizer / double-submit), check Origin/Referer, custom headers, don't change state on GET |
| Session Hijacking | Attacker steals session ID (sniffing, XSS) | HTTPS, Secure+HttpOnly, short TTL, bind to context cautiously |
| Session Fixation | Attacker sets victim's session ID before login | Regenerate session ID on login |
| Brute force / Credential stuffing | Guessing / reusing leaked passwords | Rate limit, lockout/backoff, MFA, breach-password checks, CAPTCHA |
| Man-in-the-Middle | Intercepting traffic | HTTPS + HSTS |
JWT tampering / alg:none | Forged tokens | Verify signature, pin algorithms, strong keys |
| Token replay | Reusing stolen token | Short expiry, jti, HTTPS, refresh rotation, sender-constrained tokens (DPoP/mTLS) |
| Open redirect (OAuth) | Steal auth code via bad redirect | Exact redirect URI matching |
| Authorization code interception | Steal code on public clients | PKCE |
| Clickjacking | UI redress in iframe | X-Frame-Options / CSP frame-ancestors |
| User enumeration | Error messages reveal valid emails | Generic errors, uniform timing |
| Timing attacks | Compare strings leaking info | Constant-time comparison (crypto.timingSafeEqual) |
| SQL/NoSQL Injection | Bypass login query | Parameterized queries, input validation |
CSRF in depth
- Works because browsers auto-attach cookies.
- JWT in
Authorizationheader is not auto-attached β naturally CSRF-safe (but XSS-exposed if in localStorage). - Double-submit cookie: send random token both as cookie and as header/body; server compares.
- Synchronizer token: server stores token in session and validates against form/header value.
XSS vs CSRF (quick contrast)
- XSS exploits user's trust in a site (runs attacker script inside your origin).
- CSRF exploits site's trust in the user's browser (forges requests from another origin).
- XSS can defeat CSRF protection (script can read the CSRF token).
Security headers worth naming
Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy.
14. Authorization Models
- RBAC (Role-Based): users β roles β permissions (
admin,editor,viewer). Simple, common. - ABAC (Attribute-Based): decisions from attributes (user dept, resource owner, time, IP).
- ACL (Access Control List): per-resource list of who can do what.
- ReBAC (Relationship-Based): e.g., "owner/editor of this document" (Google Zanzibar style).
- Principle of Least Privilege β grant only the minimum needed.
const requireRole = (...roles) => (req, res, next) =>
roles.includes(req.user.role) ? next() : res.sendStatus(403);
app.delete("/users/:id", auth, requireRole("admin"), deleteUser);
- Never trust the client for authorization β always enforce on the server.
- Beware IDOR / BOLA (Insecure Direct Object Reference): check that the resource belongs to the user, not just that the user is logged in.
15. Auth in React / Next.js / Node.js
Typical architectures
- Server-rendered / Next.js + session cookie β simplest and safest.
- SPA + API: access token in memory + refresh token in HttpOnly cookie.
- BFF (Backend-for-Frontend): SPA talks to its own server which holds tokens and sets a session cookie β keeps tokens away from JS (recommended for OAuth SPAs).
React: Protected route + auth context
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get("/auth/me") // cookie sent automatically
.then(r => setUser(r.data))
.catch(() => setUser(null))
.finally(() => setLoading(false));
}, []);
return <AuthContext.Provider value={{ user, setUser, loading }}>{children}</AuthContext.Provider>;
}
function ProtectedRoute({ children }) {
const { user, loading } = useContext(AuthContext);
if (loading) return <Spinner />;
return user ? children : <Navigate to="/login" replace />;
}
Client-side route guards are UX only; real protection is on the API.
Next.js: Middleware protection
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { jwtVerify } from "jose"; // works on the Edge runtime
export async function middleware(req: NextRequest) {
const token = req.cookies.get("token")?.value;
try {
if (!token) throw new Error("no token");
await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET!));
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL("/login", req.url));
}
}
export const config = { matcher: ["/dashboard/:path*"] };
- Use
jose(notjsonwebtoken) in Edge middleware. - In Server Components / Route Handlers use
cookies()fromnext/headers. - Libraries: Auth.js (NextAuth), Clerk, Auth0, Supabase Auth, Lucia.
- Re-check authorization in Server Actions / Route Handlers too β middleware alone isn't enough.
Node/Express login with JWT in HttpOnly cookie
app.post("/auth/login", async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user || !(await bcrypt.compare(req.body.password, user.passwordHash)))
return res.status(401).json({ message: "Invalid credentials" });
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "15m" });
res.cookie("token", token, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 15 * 60 * 1000 })
.json({ id: user.id, email: user.email });
});
16. Best Practices Checklist
- β HTTPS everywhere + HSTS
- β Hash passwords with Argon2/bcrypt (+ salt)
- β Generic login errors; rate-limit + lockout; offer MFA
- β
Cookies:
HttpOnly,Secure,SameSite, sensibleMax-Age - β Regenerate session ID on login/privilege change; destroy on logout
- β Short-lived access tokens + rotating refresh tokens with reuse detection
- β
Pin JWT algorithms; validate
exp,iss,aud; strong secrets/keys; rotate keys (kid) - β Don't store sensitive data in JWT payload
- β
Use Authorization Code + PKCE; validate
state&nonce; exact redirect URIs - β Least-privilege scopes/roles; server-side authorization; guard against IDOR
- β CSRF protection when using cookies; CSP to mitigate XSS
- β Secrets in env vars / secret manager, never in code or frontend
- β Log auth events, alert on anomalies; support session/device revocation
- β Prefer proven libraries/providers over rolling your own crypto
17. Interview Q&A
Basics
Q1. Authentication vs Authorization?
Authentication verifies who you are; authorization decides what you can do. AuthN first, then AuthZ. 401 = unauthenticated, 403 = forbidden.
Q2. Why is HTTP stateless and how do we handle login persistence?
Each request is independent. We persist identity using a session ID (server-side state) or a token like JWT (client-carried state), usually sent via cookie or Authorization header.
Q3. Hashing vs encryption vs encoding? Hashing: one-way (passwords). Encryption: two-way with a key (confidentiality). Encoding: reversible format change with no secrecy (Base64).
Q4. Why salt passwords? So identical passwords produce different hashes and rainbow tables/precomputation become useless.
Q5. Why bcrypt instead of SHA-256 for passwords? bcrypt is intentionally slow with adjustable cost, making brute force expensive; SHA-256 is fast and GPU-friendly.
Cookies
Q6. What are cookies and how do they work?
Small key-value data set by server via Set-Cookie, stored by the browser and automatically sent in the Cookie header on matching requests.
Q7. Explain HttpOnly, Secure, SameSite.
HttpOnly: not readable by JS (XSS mitigation). Secure: HTTPS only. SameSite: restricts cross-site sending (CSRF mitigation) β Strict, Lax, None.
Q8. Session cookie vs persistent cookie?
Session cookie has no expiry and disappears when the browser closes; persistent has Expires/Max-Age.
Q9. Cookie vs localStorage for tokens? Cookies (HttpOnly) are safe from XSS reading but need CSRF protection; localStorage is immune to CSRF but any XSS can steal the token. HttpOnly cookie is generally preferred.
Q10. Why does fetch not send cookies cross-origin by default?
Need credentials: "include" on client and Access-Control-Allow-Credentials: true with a specific (non-*) origin on server.
Q11. Same-origin vs same-site?
Origin = scheme + host + port. Site = scheme + registrable domain. a.example.com and b.example.com are same-site, cross-origin.
Sessions
Q12. How does session-based auth work? Server creates session data in a store, gives the client a random session ID cookie, looks it up on each request, deletes it on logout.
Q13. Where should sessions be stored in production? In a shared fast store such as Redis (with TTL) β not in server memory, so it survives restarts and scales across instances.
Q14. What is session fixation and how do you stop it? Attacker forces a known session ID on the victim, then reuses it after the victim logs in. Prevent by regenerating the session ID on login.
Q15. How do you scale session auth across multiple servers? Shared session store (Redis) or sticky sessions at the load balancer; shared store is more robust.
JWT
Q16. What is a JWT and what are its parts?
A signed token header.payload.signature. Header: alg/type; payload: claims; signature: integrity proof. Base64URL-encoded, not encrypted.
Q17. Is JWT encrypted? No β signed (JWS), not encrypted. Anyone can read the payload. Use JWE if confidentiality is needed.
Q18. HS256 vs RS256? HS256 uses a shared secret for sign+verify. RS256 uses a private key to sign and a public key to verify β better for distributed systems since verifiers can't mint tokens.
Q19. How do you log out / revoke a JWT?
Short expiries, delete refresh token server-side, denylist jti, token version counter, rotate keys. Clear client-side cookie/memory too.
Q20. Why use access + refresh tokens? Short-lived access tokens limit damage if stolen; long-lived refresh tokens (stored server-side, revocable) give good UX without frequent logins.
Q21. What is refresh token rotation? Every refresh issues a new refresh token and invalidates the old; reuse of an old one signals theft β revoke the family.
Q22. What are common JWT vulnerabilities?
alg:none, algorithm confusion, weak secrets, missing claim validation, long expiry, sensitive data in payload, insecure storage.
Q23. Where should you store a JWT in a browser app? Prefer HttpOnly Secure SameSite cookie (or access token in memory + refresh in HttpOnly cookie). Avoid localStorage for high-security apps.
Q24. What happens when a JWT expires mid-request flow? API returns 401; client uses refresh token to get a new access token and retries (interceptor pattern); if refresh fails β redirect to login.
Session vs JWT
Q25. Session vs JWT β which is better? Neither universally. Sessions: easy revocation, server control, ideal for traditional web apps. JWT: stateless, scalable, good for APIs/microservices/mobile but harder to revoke. Choose by requirements.
Q26. Is JWT truly stateless if you keep a denylist? No β a denylist/DB check reintroduces state. It's a pragmatic trade-off for revocation.
OAuth / OIDC
Q27. What is OAuth 2.0? A delegation framework allowing an app to access resources on a user's behalf without learning their password, via scoped, time-limited tokens.
Q28. OAuth vs OIDC?
OAuth = authorization (access tokens). OIDC = authentication layer on top, adding ID token (JWT), openid scope, /userinfo.
Q29. Explain the Authorization Code flow.
Redirect user to auth server β user logs in & consents β server redirects back with a code β client exchanges code (plus secret or PKCE verifier) at /token for tokens β client calls API with access token.
Q30. Why a code instead of returning the token directly? Keeps tokens out of the browser URL/history; token exchange happens on a secure back-channel.
Q31. What is PKCE and why is it needed? Proves the party redeeming the code is the one that started the flow (code_verifier/challenge). Protects public clients (SPAs/mobile) from code interception.
Q32. What is the state parameter?
A random, unguessable value bound to the user's session, echoed back by the auth server; prevents CSRF/login-CSRF in the OAuth flow.
Q33. What is the nonce in OIDC?
Random value included in the ID token to bind it to the auth request and prevent replay.
Q34. What are scopes?
Permissions the client requests (e.g., email, read:repo); enforce least privilege and show on the consent screen.
Q35. Which OAuth grant for a backend service calling another service? Client Credentials.
Q36. Why are Implicit and Password grants deprecated? Implicit exposes tokens in URL/browser; Password grant exposes user credentials to the client and bypasses MFA/consent. Use Auth Code + PKCE.
Q37. Access token vs ID token? ID token (OIDC, JWT) tells the client who logged in. Access token authorizes calls to the API. Don't use an ID token to call APIs.
Q38. How would you implement "Login with Google"? Use OIDC Authorization Code + PKCE: redirect to Google, receive code, exchange for tokens, verify ID token (signature, iss, aud, exp, nonce), find/create local user, then issue own session or JWT.
Security
Q39. XSS vs CSRF and defenses? XSS injects script into your site (fix: output escaping, CSP, HttpOnly). CSRF tricks browser into sending authenticated requests (fix: SameSite, CSRF tokens, Origin checks).
Q40. Does using JWT in the Authorization header prevent CSRF? Yes, because browsers don't attach it automatically β but if stored in localStorage it's exposed to XSS.
Q41. What is IDOR/BOLA? Accessing another user's resource by changing an ID because the server only checks login, not ownership. Fix with per-resource authorization checks.
Q42. How do you secure a login endpoint? HTTPS, hashing, rate limiting, generic errors, lockout/backoff, MFA, CAPTCHA, logging/monitoring, parameterized queries.
Q43. What is MFA and TOTP? Multiple distinct factors; TOTP generates a time-based one-time code from a shared secret (30-second windows).
Q44. What are passkeys? WebAuthn/FIDO2 credentials using public-key cryptography; phishing-resistant, passwordless.
Q45. What is SSO and how does SAML differ from OIDC? SSO = one login for many apps. SAML: XML, enterprise, assertions. OIDC: JSON/REST, modern, mobile/SPA-friendly.
Scenario questions
S1. Design auth for a SPA + REST API + mobile app. OIDC Authorization Code + PKCE via an identity provider; short-lived access tokens; rotating refresh tokens (HttpOnly cookie for web, secure storage for mobile); RBAC on API; rate limiting; MFA option.
S2. User reports they can't be logged out of a stolen device. Sessions: delete session records. JWT: revoke refresh tokens/bump token version/denylist; keep access TTL short; show "active devices" with remote logout.
S3. Microservices need to trust each user's identity. Auth service issues RS256 JWTs; services verify via JWKS public keys; propagate token; use client credentials or mTLS for service-to-service.
S4. Token theft suspected. Revoke refresh token family, rotate signing keys if needed, force re-auth, audit logs, patch XSS vector, shorten TTLs.
18. Cheat Sheet
COOKIE Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax; Max-Age=...
SESSION Server stores state; client has random ID cookie. Easy revoke. Needs store (Redis).
JWT header.payload.signature. Signed, NOT encrypted. Stateless. Hard to revoke.
ACCESS TKN short (5β15m) REFRESH TKN long (days), server-stored, rotate + reuse detect
OAUTH2 Authorization (delegated access). Auth Code + PKCE. Scopes. state param.
OIDC Authentication on top of OAuth2. ID token (JWT). openid scope. nonce.
SAML XML-based enterprise SSO.
CSRF SameSite + CSRF token + Origin check. XSS CSP + escape + HttpOnly.
FIXATION Regenerate session ID on login.
PASSWORDS Argon2/bcrypt + salt. Generic errors. Rate limit. MFA.
401 vs 403 Unauthenticated vs Forbidden.
One-line memory hooks
- Cookie = the carrier. Session = the server's memory. JWT = the self-contained passport. OAuth = the valet key. OIDC = the ID card.
- Sessions β "state on server"; JWT β "state in token".
- OAuth answers "what can this app access?"; OIDC answers "who is this user?".
β If you can explain Sections 4β11 in your own words, draw the Authorization Code + PKCE flow, and answer the scenarios in Section 17, you're interview-ready on this topic.
π§ Continue Learning
Related technical blueprints and practical guides in Web Development.

Web Application Architecture: Client-Server, MVC & Layered Architecture
Understand how web applications are structured using client-server architecture, MVC, layered architecture, and related patterns, and learn when each approach is useful.

Web Application Deployment: Vercel, Render, Docker & Cloud
Learn how to take web applications from local development to production using platforms like Vercel and Render, containerization with Docker, and modern cloud deployment platforms.

Version Control with Git & GitHub
Learn how developers track code changes, manage branches, collaborate, resolve conflicts, and maintain projects using Git and GitHub.

JavaScript Package Management: npm, pnpm & Yarn
Understand how JavaScript package managers handle dependencies, scripts, lockfiles, versions, and project environments using npm, pnpm, and Yarn.