Skip to main content

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"].

note

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​

  1. The client generates a random code_verifier (43–128 characters, URL-safe).
  2. It hashes the verifier with SHA-256 and base64url-encodes the result to produce code_challenge.
  3. It sends code_challenge and code_challenge_method=S256 in the authorization request.
  4. It sends the original code_verifier when exchanging the code at the token endpoint.

LumoAuth validates that SHA256(code_verifier) == code_challenge.

Generating PKCE Values​

JavaScript (Browser / Node.js)
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(/=+$/, '');
}
Python
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​

Token Exchange with PKCE
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"

Standards used on this page​

Plain nameStandard
Proof Key for Code Exchange (PKCE)RFC 7636
Discovery documentRFC 8414 and OpenID Connect Discovery

See the standards reference for what each one covers and how LumoAuth uses it.