PKCE
Proof Key for Code Exchange (RFC 7636). The authorization request commits to a random secret; the token request proves possession. Defeats authorization-code interception even when the client cannot store a long-lived secret.
01 The flow
PKCE adds two parameters and two short steps to the standard authorization-code flow:
- Generate a random
code_verifier(43-128 chars from[A-Za-z0-9-._~]) and store it locally. - Hash it with SHA-256 and base64url-encode (no padding). That's your
code_challenge. - Include
code_challengeandcode_challenge_method=S256on/oauth/authorize. - Send the original
code_verifieron/oauth/token. Quota re-hashes it and compares.
02 Authorize with challenge
GEThttps://api.usequota.ai/oauth/authorize
https://api.usequota.ai/oauth/authorize
?response_type=code
&client_id=quota_client_my_app
&redirect_uri=https://myapp.com/callback
&state=xyz
&scope=openid+credits.spend
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256PKCE parameters
| code_challengestring | The transformed challenge. 43-128 chars from [A-Za-z0-9-._~]. For S256: BASE64URL(SHA256(code_verifier)) with no padding. For plain: the verifier itself. |
| code_challenge_methodstring | S256 or plain. Defaults to S256 when omitted — set it explicitly anyway so the hashing choice is visible at the call site. |
03 Exchange with verifier
POSThttps://api.usequota.ai/oauth/token
curl -X POST https://api.usequota.ai/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=$AUTH_CODE" \
-d "redirect_uri=https://myapp.com/callback" \
-d "client_id=quota_client_my_app" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"| code_verifierstringrequired | The original random string you generated and hashed into code_challenge. Must be 43-128 chars from [A-Za-z0-9-._~]. Required when the authorization code was issued with a code_challenge. |
04 Errors
| invalid_request | code_challenge is not 43-128 chars from the allowed set, or code_challenge_method is something other than S256 or plain, or method was set without a challenge. |
| invalid_grant | code_verifier missing on a PKCE-bound code, malformed, or its S256 hash does not match the stored code_challenge. |
05 Manual flow (Browser / Web Crypto)
Server-side Node integrators: use openid-client or another OAuth library — it does PKCE for you and gets the edge cases right. The example below is for browsers, where PKCE is most often hand-rolled.
Same idea on the client side. The Web Crypto API is available in every modern browser; no dependencies needed.
function base64url(buf: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
const code_verifier = base64url(verifierBytes.buffer);
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(code_verifier),
);
const code_challenge = base64url(hash);
sessionStorage.setItem("pkce_verifier", code_verifier);
location.href =
"https://api.usequota.ai/oauth/authorize?" +
new URLSearchParams({
response_type: "code",
client_id: "quota_client_my_app",
redirect_uri: location.origin + "/callback",
state: crypto.randomUUID(),
scope: "openid profile email",
code_challenge,
code_challenge_method: "S256",
});