Quota
/ docs
Dashboard

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.

Using a Quota SDK? You can skip this page.
If you're using @usequota/core or @usequota/nextjs, PKCE is handled for you. This page is for direct OAuth integrators implementing the flow in raw HTTP.
PKCE is required
Every OAuth client must send code_challenge on /oauth/authorize and code_verifier on /oauth/token. Requests without PKCE are rejected with invalid_request (authorize) or invalid_grant (token). This applies to confidential and public clients alike.

01 The flow

PKCE adds two parameters and two short steps to the standard authorization-code flow:

  1. Generate a random code_verifier (43-128 chars from [A-Za-z0-9-._~]) and store it locally.
  2. Hash it with SHA-256 and base64url-encode (no padding). That's your code_challenge.
  3. Include code_challenge and code_challenge_method=S256 on /oauth/authorize.
  4. Send the original code_verifier on /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=S256

PKCE parameters

code_challengestringThe transformed challenge. 43-128 chars from [A-Za-z0-9-._~]: BASE64URL(SHA256(code_verifier)) with no padding.
code_challenge_methodstringS256 only. Defaults to S256 when omitted — set it explicitly anyway so the hashing choice is visible at the call site.
S256 only
Quota accepts code_challenge_method=S256 exclusively, per OAuth 2.1. The plain method is a downgrade — it sends the verifier in cleartext and offers no protection against intercepted authorization codes — so it is rejected with invalid_request.

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_verifierstringrequiredThe 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.
Public clients can omit client_secret
If your client is registered with token_endpoint_auth_methods=none, drop client_secret from the form body and authenticate by code_verifier alone. This is the canonical mobile and SPA setup.

04 Errors

invalid_requestcode_challenge is not 43-128 chars from the allowed set, or code_challenge_method is something other than S256, or method was set without a challenge.
invalid_grantcode_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",
  });
Bind the verifier to the request
The code_verifier must survive the redirect. Store it in sessionStorage (same-tab) or in a server-side store keyed by state. Never put it in the URL or local storage shared across tabs — that defeats the purpose.