PKCE
PKCE (Proof Key for Code Exchange) is a one-time secret that protects the sign-in code: the client generates a random secret (the code verifier) and sends a hash of it when starting the authorization flow, then the real secret when exchanging the code. An attacker who intercepts the authorization code cannot redeem it without the verifier.
LumoAuth supports the S256 challenge method. The plain method is intentionally not supported because it offers no protection against an attacker who intercepts the authorization request. Discovery advertises code_challenge_methods_supported: ["S256"].
PKCE is required for all public clients (SPAs, mobile apps, CLI tools). Confidential clients should also use PKCE for defense in depth.
How PKCE Works
- The client generates a random
code_verifier(43–128 characters, URL-safe). - It hashes the verifier with SHA-256 and base64url-encodes the result to produce
code_challenge. - It sends
code_challengeandcode_challenge_method=S256in the authorization request. - It sends the original
code_verifierwhen exchanging the code at the token endpoint.
LumoAuth validates that SHA256(code_verifier) == code_challenge.
Generating PKCE Values
async function generatePKCE() {
// 1. Generate a random code verifier
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const codeVerifier = base64UrlEncode(array);
// 2. Hash to produce code challenge
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = base64UrlEncode(new Uint8Array(digest));
return { codeVerifier, codeChallenge };
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
import secrets
import hashlib
import base64
def generate_pkce():
code_verifier = secrets.token_urlsafe(32)
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
return code_verifier, code_challenge
Authorization Request with PKCE
GET /orgs/{orgId}/api/v1/oauth/authorize?
response_type=code&
client_id=your_client_id&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
scope=openid+profile&
state=abc123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Token Request with code_verifier
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=SplxlOBeZQQYbYS6WxSbIA" \
-d "redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback" \
-d "client_id=your_client_id" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gAWpLynrU"
Related
Standards used on this page
| Plain name | Standard |
|---|---|
| Proof Key for Code Exchange (PKCE) | RFC 7636 |
| Discovery document | RFC 8414 and OpenID Connect Discovery |
See the standards reference for what each one covers and how LumoAuth uses it.