Skip to main content

Command Palette

Search for a command to run...

Authorization Code + PKCE

Updated
4 min readView as Markdown

The one flow to learn first

If you implement only one OAuth flow, make it Authorization Code with PKCE (Proof Key for Code Exchange).

It works for:

  • Server-side web apps (Spring Boot, Django, Rails)

  • Single-page applications (React, Vue)

  • Mobile apps (iOS, Android)

The older implicit flow put access tokens in the URL fragment. That is deprecated. Password grant bypasses consent. Auth Code + PKCE is the 2026 default.


Step-by-step (with real parameters)


What each step does

Step 1 — Authorization request

The client redirects the user to the authorization server. Critical query parameters:

Parameter Purpose
response_type=code "I want an authorization code"
client_id Identifies your registered app
redirect_uri Must exactly match a registered URI
scope Requested permissions (openid triggers OIDC)
state CSRF protection — random, stored server-side or in session
code_challenge PKCE: hash of a secret the client will prove later
code_challenge_method=S256 SHA-256 (not plain)

Example URL (formatted for reading):

https://auth.example.com/realms/myrealm/protocol/openid-connect/auth
  ?response_type=code
  &client_id=orders-web
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Flogin%2Foauth2%2Fcode%2Fkeycloak
  &scope=openid%20profile%20email
  &state=8f3c2a1b
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

Step 2 — Authorization code in redirect

On success, the user agent is redirected to your redirect_uri:

https://app.example.com/login/oauth2/code/keycloak
  ?code=0.AX4A...truncated...
  &state=8f3c2a1b

Your code must:

  1. Verify state matches what you stored

  2. Reject missing or tampered state

  3. Exchange code promptly (codes are short-lived, often ~60 seconds)

The code is useless without the PKCE verifier (and, for confidential clients, without the client secret).

Step 3 — Token request

Backend (or SPA with caution) POSTs to /token:

POST /realms/myrealm/protocol/openid-connect/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=0.AX4A...
&redirect_uri=https://app.example.com/login/oauth2/code/keycloak
&client_id=orders-web
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

For confidential clients (server-side Spring app), also send client_secret or use client authentication via JWT assertion.

Step 4 — Token response

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzUxMiIs...",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "scope": "openid profile email"
}

Step 5–6 — Call the API

GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

The resource server validates signature, expiry, issuer, audience, and scopes.


PKCE in plain language

Problem: Public clients (SPA, mobile) cannot hide a client_secret. An attacker who steals an authorization code could exchange it for tokens.

PKCE fix:

  1. Client generates random code_verifier (43–128 chars)

  2. Sends code_challenge = BASE64URL(SHA256(code_verifier)) in step 1

  3. Proves possession by sending code_verifier in step 3

Only the client that started the flow can finish it.

// Generating PKCE values (conceptual)
String codeVerifier = secureRandomBase64Url(64);
String codeChallenge = base64Url(sha256(codeVerifier));

Even confidential server apps should send PKCE today (OAuth 2.1 direction).


Confidential vs public clients

Confidential Public
Examples Spring Boot server, backend API SPA, mobile app
Secret Has client_secret No secret
Token exchange Backend channel Backend-for-frontend recommended
PKCE Recommended Required

For SPAs: do not put client_secret in JavaScript. Use a backend-for-frontend (BFF) or the SPA pattern with strict origin + short-lived tokens + refresh via httpOnly cookie.


Refresh flow (when access token expires)

POST /token
grant_type=refresh_token
&refresh_token=eyJhbGciOiJIUzUxMiIs...
&client_id=orders-web

Returns a new access token (and sometimes a rotated refresh token). Handle refresh server-side when possible.


Go deeper

PAR and JAR (optional hardening)

  • Pushed Authorization Requests (PAR): send authorize parameters via back-channel POST first, get request_uri

  • JWT Secured Authorization Request (JAR): signed request objects

Useful in high-security environments; Keycloak supports PAR in recent versions.

Logout

OAuth does not standardize logout completely. OIDC provides:

  • RP-initiated logout — redirect to end_session_endpoint

  • Front-channel / back-channel logout — SSO session termination

Plan logout explicitly; closing your app cookie alone may leave an IdP SSO session alive.

Discovery document

Fetch once at startup:

GET https://auth.example.com/realms/myrealm/.well-known/openid-configuration

Contains endpoints for authorize, token, jwks, userinfo, end_session. Spring and Keycloak clients use this automatically.


What you should know after this post

  • [ ] You can draw the six steps from memory

  • [ ] You know why state and PKCE matter

  • [ ] You never put refresh tokens in browser localStorage