# 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)

![](https://cdn.hashnode.com/uploads/covers/69722030e94f080c107ed87f/4ed13fa3-13eb-4a1c-ae93-98d9e4988739.png align="center")

* * *

## 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):

```text
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`:

```text
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`:

```http
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

```json
{
  "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

```http
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.

```java
// 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)

```http
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:

```text
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`
