PKCE explained¶
Proof Key for Code Exchange (RFC 7636) is an OAuth 2.0 extension that prevents authorization codes from being intercepted and exchanged by an attacker. PKCE is required for every public client (no client_secret) on Little X Little. It's recommended for confidential clients too.
Why¶
Without PKCE, the authorization code returned to your redirect_uri is a bearer secret. If an attacker intercepts it (malicious browser extension, OS-level URL handler, log file leak), they can exchange it for tokens.
With PKCE, the attacker also needs the original code_verifier, which never leaves your client.
How¶
sequenceDiagram
participant C as Client
participant L as id.littlexlittle.org
C->>C: verifier = random 43-128 chars
C->>C: challenge = base64url(SHA256(verifier))
C->>L: /authorize?code_challenge=...&code_challenge_method=S256
L-->>C: 302 ?code=...
C->>L: /token (code + verifier)
L->>L: assert SHA256(verifier) == stored challenge
L-->>C: id_token + access_token + refresh_token Generating the values¶
js function base64url(bytes) { return btoa(String.fromCharCode(...bytes)) .replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); } const verifier = base64url(crypto.getRandomValues(new Uint8Array(32))); const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)); const challenge = base64url(new Uint8Array(digest));
php $verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); $challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
python import base64, hashlib, secrets verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=').decode() challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=').decode()
Storing the verifier¶
The verifier is short-lived (until the callback fires) but must survive across the redirect. Use whichever fits:
| Client type | Where to store |
|---|---|
| Server-side web app | Server session ($_SESSION['code_verifier']). |
| SPA | sessionStorage (cleared on tab close). |
| Mobile | In-memory, scoped to the auth controller. |
code_challenge_method¶
Only S256 is accepted. The legacy plain method is rejected with 400 invalid_request.
Common mistakes¶
- Using the same verifier for multiple flows. Always generate a fresh one per
/authorizerequest. - Logging the verifier. It's a secret — treat it like a password.
- Storing the verifier in
localStorage. That persists across tabs and is reachable from XSS. UsesessionStorage. - Forgetting the verifier in the token request. The exchange will return
400 invalid_grant.