Auth & security concepts
Authentication vs authorization, sessions+cookies vs JWT, and OAuth2 vs OIDC — distinct jobs that interviewers love to conflate.
Four terms get conflated constantly — authentication, authorization, OAuth2, and OIDC — and interviewers love to watch you untangle them. They answer different questions: who are you, what may you do, can app A act for me on B, and who logged in. Keep those four questions straight and the whole area stops being a fog of acronyms.
Authentication vs authorization
Authentication runs first and establishes identity; authorization runs second and gates actions against that identity. A 401 means authentication failed (no/invalid credentials — retry with auth); a 403 means authentication succeeded but authorization denied the action (you’re known, just not allowed). The number one tell of a junior answer is swapping these two.
Keep them in separate layers. The middleware that authenticates a request (verify the token, attach req.user) should not also decide what that user may touch — that’s the job of an authorization check at the resource, where the policy lives (“does this user own order 42?”). Mixing them produces the classic bug where a valid login leaks access to other users’ data because nobody checked ownership.
Sessions vs JWT
| Approach | Use when | Avoid when | Gotcha |
|---|---|---|---|
| Sessions + cookies | server-rendered apps, you need instant revoke / logout-everywhere | stateless multi-service APIs where a shared session store is a bottleneck | needs a session store; CSRF risk — set SameSite + CSRF token |
| JWT (stateless) | APIs / microservices verifying a token without a central lookup | you need to revoke immediately or store much per-user state | can't easily revoke before expiry — keep TTL short + use refresh tokens |
The fundamental trade is where the truth lives. A session keeps the truth on the server — the cookie is just a meaningless random ID, and deleting the server row instantly logs the user out everywhere. A JWT moves the truth into the token — the server can verify it with just a public key and no database call, which is gold for microservices, but there’s nothing to delete, so the token stays valid until it expires.
SESSION (stateful)
Login → server creates session, stores it, sets cookie:
sid=opaqueRandom; HttpOnly; Secure; SameSite=Lax
Each request → server looks up sid in the store → identity.
Logout → delete the row. Instant, everywhere.
JWT (stateless)
Login → server signs a token:
header.payload.signature
payload = { sub: 42, role: "admin", exp: 1700000000 }
Each request → Authorization: Bearer <jwt>
server verifies the signature with its key — NO DB lookup.
Logout → ??? the token is valid until exp.
Mitigate: short TTL + refresh token (or a revocation list).The JWT carries its claims inside it, so any service holding the public key can validate it independently — that’s the appeal for microservices, and also exactly why you can’t simply “delete” it to log someone out. The standard answer to revocation is a short-lived access token (minutes) paired with a refresh token: revoke the refresh token, and the access token dies on its own within minutes.
OAuth2 vs OIDC
OAuth2 was built to delegate authorization: you let a third-party app obtain a scoped access token to call an API on your behalf, without handing over your password. “Let this app post to my calendar” is OAuth2 — the app gets a token good for calendar.write and nothing else. OIDC (OpenID Connect) layers authentication on top — it reuses the same OAuth2 flow but also returns an id_token (a JWT describing who logged in). “Login with Google” is OIDC.
The one-line distinction interviewers want: OAuth2 answers what may this app do for me; OIDC answers who is this user. OAuth2 alone was never designed to prove identity — apps that misused its access token as a login signal created real security holes, which is precisely the gap OIDC’s id_token was created to fill.
The flow above is the authorization-code grant — the secure default for web apps. The key design choice is that the app never sees the user’s password and receives a short-lived code in the browser redirect, then swaps that code for tokens in a back-channel server-to-server call (step 5). That swap requires the app’s client secret, so even if an attacker intercepts the code in the URL they can’t redeem it. (Public clients like SPAs and mobile apps add PKCE to plug the same hole without a secret.)
01 Learning objectives
0 / 4 done02 Curated reading
03 Knowledge check
- 01easy
Authentication answers which question?
- 02medium
A key downside of stateless JWTs versus server sessions is:
- 03medium
“Login with Google” on top of OAuth2 is provided by:
04 Interview questions
browse all ↗What gets asked on this topic — tap a card for how to approach it, the follow-ups, and the trap. Company tags are best-effort & sourced.
-
Authentication vs authorization — state the difference crisply with an example.
Authentication answers 'who are you?' — verifying identity (password, token, passkey). Authorization answers 'what are you allowed to do?' — checking permissions after identity is established.
Example: logging in with your password is authentication; the check that decides you can read but not delete the document is authorization. Authn always precedes authz. The corresponding status codes: 401 Unauthorized = not authenticated; 403 Forbidden = authenticated but not permitted.
Follow-ups they push on- Which HTTP status maps to each failure?
- Where does each typically live in a request pipeline?
Red flag Swapping 401 and 403, or saying 'authorization checks your password'. Authorization assumes identity is already known.
source: Auth0 — Authentication vs Authorization ↗ -
Session cookies vs JWTs for API auth — compare the tradeoffs. How do you revoke each?
Sessions: server stores session state, the client holds an opaque session id in an
HttpOnlycookie. Stateful, but revocation is trivial — delete the server-side session. Needs shared session storage to scale horizontally.JWTs: a signed, self-contained token the server verifies without a lookup — stateless and scales easily. The catch is revocation: a valid JWT is honored until it expires, so logout/ban requires a denylist or short expiry + refresh tokens, which reintroduces state. Use short-lived access tokens (minutes) plus a refresh token to limit the blast radius.
Follow-ups they push on- How do you revoke a JWT before it expires?
- Where should the browser store a JWT — localStorage or a cookie?
Red flag Calling stateless JWTs strictly better. Their headline weakness is revocation; any real logout/ban story drags state back in.
source: Auth0 — Token-based vs session-based authentication ↗ -
OAuth2 vs OIDC — what is each actually for? Don't conflate them.
OAuth 2.0 is delegated authorization: 'let app A access my data on service B' without sharing my password — it issues access tokens scoped to resources. It says nothing about who the user is.
OIDC (OpenID Connect) is an authentication layer built on top of OAuth2. It adds an ID token (a JWT) and a standard
/userinfoendpoint, so the app learns *who* logged in — this is what powers 'Log in with Google'. So: OAuth2 = access to resources; OIDC = proof of identity.Follow-ups they push on- What does the ID token contain that the access token doesn't?
- Why is using a raw OAuth2 access token as proof of login a mistake?
Red flag Using a bare OAuth2 access token to authenticate a user. Access tokens are for resource access; identity comes from the OIDC ID token.
source: OpenID Connect — How it works ↗ -
Walk through the OAuth2 authorization code flow. Why was PKCE added?
Authorization code flow: the app redirects the user to the auth server; the user authenticates and consents; the auth server redirects back with a short-lived authorization code; the app's backend exchanges that code (plus its client secret) for an access token over a back channel. Keeping tokens off the front channel is the point.
PKCE (Proof Key for Code Exchange) hardens this for public clients (SPAs, mobile) that can't keep a secret. The client sends a hashed
code_challengeup front and the originalcode_verifierat exchange time, so a stolen authorization code is useless without the verifier. PKCE is now recommended for all clients.Follow-ups they push on- Why is the implicit flow discouraged now?
- What attack does PKCE specifically stop?
Red flag Using the deprecated implicit flow (tokens in the URL fragment) for SPAs. The modern guidance is auth-code + PKCE.
source: oauth.com — Authorization Code with PKCE ↗ -
Where should a browser store an access token, and how do the choices map to XSS vs CSRF?
localStorageis readable by any JavaScript on the page, so a single XSS flaw leaks the token. AnHttpOnlycookie is invisible to JS (XSS can't read it) but is sent automatically, which opens CSRF.The pragmatic answer: store tokens in
HttpOnly,Secure,SameSite=Lax/Strictcookies and add anti-CSRF defenses (SameSite already blocks most cross-site sends; add a CSRF token for the rest). Keep access tokens short-lived. There's no storage location immune to a compromised front end — defense in depth plus a tight CSP matters more than the slot.Follow-ups they push on- How does SameSite=Strict mitigate CSRF?
- Why doesn't HttpOnly help against CSRF?
Red flag Claiming HttpOnly cookies are 'XSS-proof and safe'. They stop token theft via JS but are auto-sent, so you still need CSRF protection.
source: OWASP — JWT / token storage cheat sheet ↗ -
How should passwords be stored, and why is a fast hash like SHA-256 the wrong choice?
Never store plaintext or reversible encryption. Use a slow, salted, adaptive password hash — bcrypt, scrypt, or Argon2 (the current OWASP-preferred). The salt (unique per user) defeats rainbow tables; the deliberate slowness/work factor caps how many guesses an attacker can make per second after a breach.
Fast general-purpose hashes (SHA-256, MD5) are wrong precisely because they're fast — a GPU computes billions per second, making offline brute force cheap. Choose a memory-hard function and raise the cost factor as hardware improves.
Follow-ups they push on- What does the salt protect against specifically?
- Why is Argon2 preferred over bcrypt today?
Red flag Using SHA-256/MD5 (even salted) for passwords. They're built to be fast, which is the opposite of what password hashing needs.
source: OWASP — Password Storage Cheat Sheet ↗ -
Why use short-lived access tokens with refresh tokens instead of one long-lived token?
A stateless access token can't be revoked before it expires, so you want it to live only minutes — that bounds the damage if it leaks. To avoid forcing the user to log in every few minutes, a longer-lived refresh token (stored more securely, server-trackable) is exchanged for fresh access tokens.
This splits concerns: access tokens are stateless and fast to verify; refresh tokens are the revocable, stateful part. Add refresh token rotation (issue a new refresh token each use and invalidate the old one) so a stolen refresh token is detected on reuse.
Follow-ups they push on- What is refresh token rotation and what attack does it catch?
- Where do you store the refresh token vs the access token?
Red flag Issuing a long-lived access token 'for convenience'. If it leaks you have no way to revoke it until expiry.
source: Auth0 — Refresh tokens ↗ -
Common pattern: use OAuth/OIDC to log in, then issue your own session or JWT. Why do that instead of using the provider's token directly?
After OIDC verifies identity, you typically mint your own session/JWT rather than passing Google's token around. Reasons: you control expiry and revocation; you attach your app's roles/permissions and user id; you don't couple every internal service to the external provider's token format or availability; and you avoid leaking a powerful provider token across your backend.
The provider token is used once at login to establish identity; from then on your own credential governs the session.
Follow-ups they push on- What goes in your token that the provider's doesn't?
- How does this help if you later add a second identity provider?
Red flag Forwarding the raw Google/Apple token to every internal service. It couples you to the provider and complicates revocation and authorization.
source: OAuth.com — OAuth 2.0 Simplified ↗ -
What is CSRF, and why does a CSRF attack work even though the attacker never sees the victim's cookie?
CSRF (Cross-Site Request Forgery) tricks a logged-in victim's browser into making a state-changing request to your site. The attacker hosts a page that auto-submits a form (or fires a request) to
yourbank.com/transfer; because the browser automatically attaches the victim's cookies to any request to that origin, the request arrives authenticated — even though the attacker never read the cookie.The core enabler is ambient authority: cookies ride along by default. Defenses:
SameSitecookies (block cross-site sends), anti-CSRF tokens (a secret the attacker's page can't know), and checking Origin/Referer.What a strong answer coversThe browser auto-sends cookies to the target origin — the attacker exploits that, not the cookie value.
Only state-changing requests matter; CSRF can't read the response (same-origin policy).
SameSite=Lax/Strictcookies are the first-line modern defense.Anti-CSRF tokens add a secret the attacker's page cannot supply.
Quick self-checkWhy does a CSRF attack succeed without the attacker ever reading the session cookie?
-
Wrong — no decryption happens; the attacker never accesses the cookie.
-
Correct — ambient cookie authority sends it on the forged request.
-
Wrong — that's XSS; CSRF doesn't read the cookie.
-
Wrong — SOP is browser-enforced and still applies.
Follow-ups they push on- Why are JWTs in the Authorization header less exposed to CSRF than cookie sessions?
- Does CSRF let the attacker read the response? (No — SOP blocks that.)
Red flag Thinking HTTPS or HttpOnly stops CSRF. They don't — the browser still auto-attaches the cookie. SameSite and CSRF tokens are the defenses.
source: OWASP — Cross-Site Request Forgery Prevention Cheat Sheet ↗ -
Walk through the three parts of a JWT. What does the signature guarantee — and what does it NOT?
A JWT is
header.payload.signature, each base64url-encoded and joined by dots. The header names the algorithm; the payload holds the claims (sub,exp, roles); the signature is computed over header+payload with a secret (HMAC) or private key (RSA/ECDSA).The signature guarantees integrity and authenticity — the server detects any tampering and confirms the token was issued by a holder of the key. It does not provide confidentiality: the payload is merely encoded, not encrypted, so anyone can base64-decode and read it. Never put secrets in a JWT payload, and always verify the signature server-side.
What a strong answer coversThree parts: header, payload (claims), signature — base64url, dot-separated.
Signature → integrity + authenticity (tamper-evident, proves the issuer).
Payload is encoded, not encrypted — readable by anyone; no secrets in it.
Standard claims:
sub,exp,iat,iss,aud.
Quick self-checkWhat does a valid JWT signature prove?
-
Wrong — the payload is base64-encoded plaintext, not encrypted.
-
Correct — that's integrity and authenticity.
-
Wrong — replay needs short expiry/jti, not the signature.
-
Wrong — claims drive authorization, and they must still be checked.
Follow-ups they push on- Why must the server verify the signature on every request?
- What's the difference between a signed (JWS) and an encrypted (JWE) token?
Red flag Storing sensitive data in the JWT payload assuming it's hidden. It's base64-decodable plaintext — signing protects integrity, not confidentiality.
source: jwt.io — Introduction to JSON Web Tokens ↗ -
Debugging: a JWT library accepts a token with alg: none and lets a forged admin token through. What happened?
This is the classic
alg: none/ algorithm-confusion vulnerability. The JWT header declares its own algorithm; if the verifier trusts that field, an attacker setsalg: none(or strips the signature) and the library skips verification, accepting a payload they forged (role: admin). A related attack swapsRS256forHS256, signing with the public key as if it were an HMAC secret.Fix: never let the token dictate the algorithm. Configure the verifier with an allowlist of expected algorithms, reject
none, and validateexp/aud/iss. Treat the header'salgas untrusted input.What a strong answer coversThe bug: the verifier trusts the attacker-controlled
algheader.alg: nonetells naive libraries to skip signature verification entirely.RS256→HS256 confusion lets the public key be abused as an HMAC secret.
Fix: pin the expected algorithm(s) server-side; reject
none; verify standard claims.
Quick self-checkWhat's the root cause of the alg:none JWT bypass?
-
No — with alg:none there's no signature check at all.
-
Correct — it should pin expected algorithms, not read them from the token.
-
No — expiry is unrelated to the signature-skip bug.
-
No — transport security doesn't affect signature verification logic.
Follow-ups they push on- Why is the RS256-to-HS256 swap dangerous when the public key is, well, public?
- Which standard claims should you always validate?
Red flag Calling a generic `verify()` that honors the token's own `alg`. Always pass an explicit algorithm allowlist; never accept `none`.
source: Auth0 — Critical vulnerabilities in JSON Web Token libraries ↗ -
RBAC vs ABAC — what's the difference, and when do you outgrow roles?
RBAC (Role-Based Access Control) grants permissions through roles: a user is an
editor, theeditorrole canupdate:article. Simple, auditable, and enough for most apps. It strains when access depends on context beyond a role — ownership, department, time of day, resource attributes — leading to a 'role explosion' (editor_team_a_readonly_weekends).ABAC (Attribute-Based Access Control) decides via policies over attributes of the user, resource, action, and environment (e.g. 'allow if
user.dept == resource.deptandtimeis business hours'). It's far more expressive but harder to reason about and audit. Start with RBAC; reach for ABAC when contextual, fine-grained rules cause role explosion.What a strong answer coversRBAC: permissions via roles — simple, auditable, sufficient for most apps.
ABAC: policies over user/resource/action/environment attributes — expressive, context-aware.
Role explosion signals you've outgrown pure RBAC.
ABAC trades simplicity/auditability for fine-grained flexibility.
Quick self-checkRequirement: 'a user may edit a document only if they are in the same department as the document.' Which model fits naturally?
-
Leads to role explosion and still can't express the per-resource match cleanly.
-
Correct — it's an attribute relationship, exactly ABAC's strength.
-
Wrong — too broad; grants everyone everything.
-
Wrong — the requirement is explicitly an access rule.
Follow-ups they push on- What's 'role explosion' and what causes it?
- How does ownership-based access (only edit your own posts) fit RBAC vs ABAC?
Red flag Encoding contextual rules as ever-more-specific roles. When permissions depend on resource attributes or context, that's an ABAC need, not more roles.
source: Auth0 — RBAC vs ABAC ↗ -
Why compare password hashes (and tokens) with a constant-time comparison instead of ==?
A normal string
==short-circuits at the first mismatching byte, so it returns faster the earlier the difference. An attacker measuring response timing can exploit this timing side channel to recover a secret (an API token or HMAC) byte by byte — try values until the comparison takes slightly longer, meaning one more byte matched.A constant-time comparison always examines the full length regardless of where bytes differ, leaking no timing information. Use the platform's
crypto.timingSafeEqual/hmac.compare_digestfor tokens, HMAC tags, and similar secrets. (Note: bcrypt/Argon2 verification already handles this for passwords.)What a strong answer covers==short-circuits, so its runtime depends on how many leading bytes match.An attacker can recover a secret byte-by-byte from timing differences.
Constant-time compare scans the full input regardless of mismatches.
Use
crypto.timingSafeEqual/hmac.compare_digestfor token/HMAC checks.
Follow-ups they push on- Why doesn't this timing concern apply to comparing two bcrypt hashes the same way?
- Where else do timing side channels show up?
Red flag Comparing secret tokens or HMAC signatures with ordinary string equality. The early-exit timing leak can let an attacker brute-force the secret one byte at a time.
source: OWASP — Cryptographic Storage Cheat Sheet ↗ -
What is a pepper, and how does it differ from a salt in password hashing?
A salt is a unique random value stored *alongside* each password hash; it ensures identical passwords produce different hashes and defeats precomputed rainbow tables. It's not secret — it lives in the database with the hash.
A pepper is a single secret value mixed into every password before hashing, but kept outside the database (in app config, a secret manager, or an HSM). The point of defense-in-depth: if an attacker steals only the database, the salts don't help them, and without the pepper they still can't crack the hashes offline. Salt = per-user, public, in DB; pepper = global, secret, outside DB.
What a strong answer coversSalt: per-user, random, stored with the hash — kills rainbow tables.
Pepper: global secret, kept out of the DB — defends against DB-only theft.
They're complementary, not alternatives.
Pepper rotation is harder, so it's stored in config/secret manager/HSM.
Quick self-checkWhat distinguishes a pepper from a salt?
-
Backwards — salt is per-user, pepper is global.
-
Correct — that's the defining difference.
-
Wrong — neither describes salt vs pepper.
-
Wrong — they serve different, complementary roles.
Follow-ups they push on- If both leak, does the pepper still help?
- Where should the pepper be stored, and why not in the DB?
Red flag Storing the pepper in the same database as the hashes — that defeats its entire purpose. The pepper's value comes from living somewhere a DB dump won't expose.
source: OWASP — Password Storage Cheat Sheet (Peppering) ↗