All browser and mobile clients must use PKCE (RFC 7636). OakID supports code_challenge_method=S256 only.

Generate verifier and challenge

function randomBase64Url(bytes = 32) {
  const arr = crypto.getRandomValues(new Uint8Array(bytes));
  return btoa(String.fromCharCode(...arr))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

async function sha256Base64Url(input) {
  const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
  return btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

const verifier = randomBase64Url(32);
const challenge = await sha256Base64Url(verifier);
Store verifier in sessionStorage (or secure server session) until the callback.

Authorize

Add to the authorize URL:
code_challenge=CHALLENGE
code_challenge_method=S256

Token exchange

Send the original verifier:
code_verifier=VERIFIER
OakID verifies BASE64URL(SHA256(verifier)) === code_challenge.
If you see Invalid OAuth state on the callback, you likely opened a sample authorize URL from the Developer Portal instead of starting login in your app. See Errors & troubleshooting.