OAuth 2.0 / OIDC for SPAs — Auth Code + PKCE
TL;DR
For modern single-page apps, the standard auth flow is OAuth 2.0 Authorization Code with PKCE (Proof Key for Code Exchange). It replaces the deprecated Implicit flow (tokens in URL hash → localStorage). For identity (who is the user?), add OpenID Connect (OIDC) on top — adds an ID token (JWT with user claims). The senior knowledge: why Implicit is dead, how PKCE prevents code interception, where tokens should end up (httpOnly cookies via a BFF pattern, not localStorage), and PKCE’s role in mobile/native too.
Interview Q&A
Q: OAuth 2.0 quick mental model.
A: OAuth is delegated authorization — a user lets your app access resources on a third party (their Google calendar, GitHub repos) without sharing their password.
Roles:
- Resource Owner — the user.
- Client — your app.
- Authorization Server — issues tokens (e.g.,
accounts.google.com). - Resource Server — the API the token grants access to (e.g.,
googleapis.com/calendar).
Flow types: Authorization Code (server-side apps, SPAs with PKCE, mobile), Client Credentials (machine-to-machine), Device Code (TVs, CLIs). Implicit and Resource Owner Password are deprecated.
Q: OAuth vs OIDC — different things?
A: OAuth = authorization (“can this app access this resource?”). OIDC = identity (“who is the user?”) layered on top.
OAuth alone gives you an access token — opaque to you; it lets you call an API. OIDC adds:
- An ID Token (JWT) with claims about the user (
sub,email,name). - A userinfo endpoint for more profile data.
- Standardized scopes (
openid,profile,email).
If you want to log a user in via Google: use OIDC. If you only want to call an API on their behalf: OAuth suffices.
Q: Why is Implicit flow dead?
A: The old SPA flow: redirect to https://auth.example.com/authorize?response_type=token, user logs in, redirected back to https://yourapp.com/#access_token=.... Your JS parses the fragment and stores the token.
Problems:
- Token in URL fragment — logged in browser history, referer headers, server logs (if URL leaks).
- No refresh tokens — short-lived access tokens require silent-refresh hacks (iframe with
prompt=none). - No code interception protection — anything that intercepts the redirect gets the token directly.
OAuth 2.1 (draft consolidating best practices) deprecates Implicit. Modern alternative: Auth Code + PKCE.
Q: Authorization Code with PKCE — the modern flow.
A:
-
Client generates a code verifier (random ~64-char string) and code challenge =
BASE64URL(SHA256(verifier)). -
Redirect to auth server with
response_type=codeandcode_challenge:https://auth.example.com/authorize? response_type=code& client_id=spa-client& redirect_uri=https://yourapp.com/callback& scope=openid profile email& state=<csrf>& code_challenge=<challenge>& code_challenge_method=S256 -
User logs in, consents. Auth server redirects back with a one-time authorization code:
https://yourapp.com/callback?code=<code>&state=<csrf> -
Client POSTs the code + verifier to exchange for tokens:
POST /token grant_type=authorization_code code=<code> redirect_uri=https://yourapp.com/callback client_id=spa-client code_verifier=<verifier> -
Auth server verifies SHA256(verifier) === stored challenge. If yes, returns:
{ "access_token": "...", "id_token": "...", "refresh_token": "...", "expires_in": 3600 }
PKCE binds the redirect (where the code is delivered) to the token exchange (where the verifier is sent). Even if an attacker intercepts the code, without the verifier they can’t exchange it.
Q: Why PKCE? Wasn’t OAuth secure without it?
A: Public clients (SPAs, mobile apps) can’t keep a client_secret. Without PKCE:
- Attacker intercepts the authorization code (malicious app on the same device, redirect interception, history leak).
- Attacker exchanges the code for tokens (no secret needed for public clients).
PKCE adds a per-flow secret (the verifier). Code without verifier is useless.
Originally for mobile (where redirect URIs can be hijacked by other apps), PKCE is now required for all public clients per OAuth 2.1.
Q: Where do the tokens end up?
A: The hard question. Two patterns:
Token in browser (SPA-only):
- Access token: in-memory variable (best) or localStorage (worse).
- Refresh token: localStorage (bad — XSS reads forever) or, awkwardly, in memory (lost on refresh).
- Pros: simple, no backend needed.
- Cons: XSS = token theft.
BFF (Backend-For-Frontend) pattern:
- Your SPA’s backend (a thin server you control) handles the OAuth dance.
- Tokens are stored server-side (in a session store, Redis).
- Browser gets an httpOnly session cookie referencing the server session.
- API calls go SPA → BFF → upstream API (BFF attaches the access token).
- Pros: XSS can’t reach tokens. CSRF mitigated by SameSite cookies.
- Cons: needs a server (no longer “static SPA”).
Modern best practice for SPAs: BFF pattern. Auth0, Okta, NextAuth, Lucia all support this. Tokens never touch the browser.
Q: PKCE flow with a BFF.
A:
- User clicks “Login with Google.”
- SPA redirects to
/api/auth/login(your BFF). - BFF generates state + PKCE verifier, stores in session, redirects to Google with challenge.
- User authenticates with Google.
- Google redirects to
/api/auth/callback?code=...(BFF endpoint). - BFF exchanges code + verifier for tokens — Google returns access, ID, refresh.
- BFF stores tokens in its session store; sets httpOnly session cookie on the browser.
- SPA does
fetch("/api/users")— cookie attached. BFF authenticates request, attaches Google access token, calls Google API, returns result.
The browser never sees the tokens. Frameworks (NextAuth, Auth.js, Lucia, Better Auth) handle this end-to-end.
Q: What’s state for in OAuth?
A: CSRF protection during the redirect. Client generates a random state, stores it (cookie or session), passes in the authorize URL. Auth server echoes it back in the callback. Client verifies match.
Without it, an attacker can complete an OAuth flow with their account, redirect the victim to the callback with the attacker’s code, victim’s app links the attacker’s account to the victim’s session.
Always required for security. PKCE doesn’t replace state — they protect different things.
Q: ID Token validation.
A: The ID token is a JWT. Validate it:
- Signature against the IdP’s JWKs (
https://provider/.well-known/jwks.json). iss(issuer) matches the expected provider.aud(audience) is yourclient_id.exp(expiry) in the future.nonce(sent in the authorize request) matches.
Use a library (jose, oidc-client-ts); don’t hand-roll JWT verification.
Q: Refresh tokens — how do they fit?
A: Short-lived access tokens (~1h) + long-lived refresh tokens (~30 days). When access expires, exchange refresh for a new access.
Storage:
- Refresh token: httpOnly cookie (BFF pattern) or secure server storage. NEVER localStorage.
- Access token: short-lived, in-memory or per-request.
Refresh token rotation: each refresh issues a new refresh token, invalidating the old. If an old refresh is replayed (attacker stole it), the system detects the replay and revokes the session.
Modern OAuth providers (Auth0, Okta) implement refresh rotation by default. Use it.
Q: Single Sign-On (SSO).
A: OIDC enables SSO across apps using the same Identity Provider:
- User logs into App A via Google → has session with Google.
- User visits App B; App B redirects to Google; Google sees existing session → no prompt; redirects back with code.
- App B has the user logged in without a fresh password entry.
Same flow, “remembered session at the IdP.” Why workplace SSO (Okta, Azure AD, Auth0) is convenient — one login, many apps.
Q: Common OAuth mistakes.
A:
- Implicit flow for new code — deprecated.
- Storing refresh tokens in localStorage — XSS reads forever.
- Skipping
state— CSRF on the redirect. - Not validating ID token signature — accepting any JWT.
redirect_urinot strict — open redirect lets attacker steal codes.client_secretin SPA code — there are no secrets in a SPA; whatever you ship, attackers see.- Token validation only on
exp— must check signature + issuer + audience.
Gotchas / edge cases
- PKCE on a confidential client (server) is allowed but not required — defense in depth.
response_mode=form_post— code POSTed instead of in URL — useful for some SPA setups to keep it out of browser history.- Implicit flow may still be used by some legacy IdPs; if you encounter one, push for Auth Code support.
- OAuth scopes are opaque to the protocol — what
read:usermeans is per-provider. Check docs. prompt=nonefor silent re-auth in iframes — deprecated by browser privacy (third-party cookies). Replaced by refresh tokens + BFF.- Logout — OIDC has a
end_session_endpoint. Logging out of your app doesn’t always log out of the IdP — handle deliberately. access_tokenlifespan vsrefresh_token— typical 1h access, 30d refresh. Configurable.
What a senior is expected to say
- “Auth Code + PKCE is the modern OAuth flow for SPAs and mobile. Implicit is deprecated — tokens-in-URL fragment is a leak vector.”
- “OIDC is identity on top of OAuth — adds an ID Token JWT with user claims. Validate signature + iss + aud + exp + nonce.”
- “Best practice: BFF pattern. Tokens never reach the browser; httpOnly session cookie references server-stored tokens. Removes XSS-exfiltration vector.”
- “Refresh tokens in localStorage = catastrophic. Refresh always in httpOnly cookie or server-side. Refresh rotation detects replay attacks.”
- “
statefor CSRF on the redirect, PKCE for code interception protection — different threats, both required.” - “Strict
redirect_uriallowlist on the IdP — wildcard is an open redirect, lets attackers grab codes.”
Cross-references
- Token storage trade-offs: 06_token_storage.md
- JWT pitfalls (backend): ../../backend/11_authentication/jwt/
- Backend OAuth: ../../backend/11_authentication/sso/
- XSS (the threat to token-in-browser): 01_xss.md
Further reading
- OAuth 2.0 — RFC 6749: https://datatracker.ietf.org/doc/html/rfc6749
- PKCE — RFC 7636: https://datatracker.ietf.org/doc/html/rfc7636
- OAuth 2.0 Security BCP: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
- OIDC spec: https://openid.net/specs/openid-connect-core-1_0.html
- Auth0 — PKCE explained: https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-proof-key-for-code-exchange-pkce