QuestionsNetworking/Security

OAuth 2.0 and OIDC: the authorization-code flow with PKCE

AuthenticationHardNetworking/Security

Explain the OAuth 2.0 authorization-code flow with PKCE and how OpenID Connect fits in. Why is PKCE required for SPAs and why is the implicit flow dead?

What it tests

Understanding delegated authorization end-to-end, the role of PKCE, and OIDC's identity layer — without hand-waving.

Approach & answer

OAuth 2.0 is a DELEGATED AUTHORIZATION protocol: it lets a user grant your app limited access to their resources on another service (or lets you 'Sign in with X') WITHOUT the user handing your app their password. OpenID Connect (OIDC) is a thin identity layer ON TOP of OAuth 2.0: OAuth alone gives you an ACCESS TOKEN (authorization to call APIs); OIDC adds an ID TOKEN (a signed JWT asserting WHO the user is — authentication), which is what 'Sign in with Google' actually uses. The AUTHORIZATION-CODE flow is the recommended flow, and PKCE (Proof Key for Code Exchange) hardens it for public clients. Steps: (1) the app generates a random CODE VERIFIER and its SHA-256 hash, the CODE CHALLENGE. (2) It redirects the user to the authorization server's /authorize with client_id, redirect_uri, scope, a state value (CSRF protection for the redirect), and the code_challenge. (3) The user authenticates and consents AT THE AUTH SERVER (your app never sees the password). (4) The auth server redirects back to your redirect_uri with a short-lived AUTHORIZATION CODE (and echoes state, which you verify). (5) The app exchanges that code at the /token endpoint, sending the original code_verifier. (6) The server hashes the verifier, checks it matches the challenge from step 2, and only then returns the access token (+ refresh token, + ID token for OIDC). Why PKCE is essential for SPAs and mobile (public clients that can't keep a client secret): the authorization code travels through the browser/redirect and could be intercepted (a malicious app registering the redirect scheme, logs, referrer leakage). Without PKCE, a stolen code could be redeemed by the attacker. With PKCE, redeeming the code REQUIRES the code_verifier, which never left the original app — so an intercepted code is useless. Why the IMPLICIT flow is dead: it returned the access token DIRECTLY in the redirect URL fragment (no code exchange), which exposed the token in browser history, referrer headers, and logs, and offered no PKCE-style binding — the current OAuth 2.0 Security BCP and OAuth 2.1 deprecate it and mandate authorization-code + PKCE for everyone (SPAs included). Practical notes: validate state (and OIDC nonce) to prevent CSRF/replay on the callback, verify the ID token's signature/issuer/audience/expiry, request least-privilege scopes, and prefer having the auth server set tokens in HttpOnly cookies (or a BFF — backend-for-frontend — pattern) rather than exposing them to SPA JavaScript.

Use this technique when

Integrating 'Sign in with X'; explaining PKCE; choosing an OAuth flow for an SPA.

Code

Authorization-Code flow with PKCE (OIDC adds an ID token):

  App: verifier = random(); challenge = SHA256(verifier)

  1. redirect -> /authorize?client_id&redirect_uri&scope&state&code_challenge
  2. user logs in + consents AT THE AUTH SERVER (app never sees password)
  3. redirect back -> redirect_uri?code=AUTH_CODE&state   (verify state)
  4. POST /token  { code: AUTH_CODE, code_verifier: verifier }
  5. server: SHA256(verifier) == challenge ?  -> yes
  6. <- access_token (+ refresh_token) (+ id_token for OIDC = who the user is)

PKCE: an intercepted AUTH_CODE is useless without the verifier (never left the app)
      -> required for SPAs/mobile (public clients, no client secret).

Implicit flow (DEAD): returned the token in the URL fragment -> leaked via
      history/referrer/logs, no PKCE binding. OAuth 2.1 mandates code+PKCE.
Validate state + OIDC nonce; verify id_token sig/iss/aud/exp; least-privilege scopes.

References