Authorization Code + PKCE
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:
Verify
statematches what you storedReject missing or tampered
stateExchange
codepromptly (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:
Client generates random
code_verifier(43–128 chars)Sends
code_challenge = BASE64URL(SHA256(code_verifier))in step 1Proves possession by sending
code_verifierin 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_uriJWT 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_endpointFront-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
stateand PKCE matter[ ] You never put refresh tokens in browser
localStorage