> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aveid.net/llms.txt
> Use this file to discover all available pages before exploring further.

# PKCE for public clients

> Production-grade PKCE implementation for SPAs and mobile apps. Covers the security model, browser storage, common failures, and mobile-specific concerns.

PKCE (Proof Key for Code Exchange, pronounced "pixy") is required for any app that can't safely store a client secret — SPAs, mobile apps, desktop apps, and browser extensions.

## Why PKCE matters

Without PKCE, a leaked authorization code can be exchanged for tokens by anyone who intercepts it. PKCE binds the code exchange to the specific browser session that initiated the login:

1. Your app generates a random `code_verifier` string
2. It hashes it to produce a `code_challenge` and sends that to Ave
3. When exchanging the code for tokens, it sends the original `code_verifier`
4. Ave hashes the verifier and checks it matches the stored challenge

An attacker who intercepts the code can't exchange it without the original `code_verifier`, which never left your app.

## Implementation checklist

<Check>Generate a high-entropy `code_verifier` (at least 43 characters, URL-safe)</Check>
<Check>Hash it with SHA-256 and base64url-encode to get `code_challenge`</Check>
<Check>Send `code_challenge` and `code_challenge_method=S256` in the authorization request</Check>
<Check>Store `code_verifier`, `state`, and `nonce` in `sessionStorage` (not `localStorage`)</Check>
<Check>Verify the returned `state` on the callback before exchanging the code — prevents CSRF attacks</Check>
<Check>Clear stored values immediately after a successful exchange — they are single-use</Check>

## Implementation with `@ave-id/sdk`

<Steps>
  <Step title="Redirect the user">
    ```ts theme={null}
    import { startPkceLogin } from "@ave-id/sdk/client";

    await startPkceLogin({
      clientId: "YOUR_CLIENT_ID",
      redirectUri: "https://yourapp.com/callback",
      scope: "openid profile email offline_access",
    });
    // Automatically generates PKCE verifier/challenge, nonce, and state,
    // stores them in sessionStorage, and redirects to aveid.net/signin
    ```
  </Step>

  <Step title="Handle the callback">
    ```ts theme={null}
    import { finishPkceLogin } from "@ave-id/sdk/client";

    // On your /callback page:
    const tokens = await finishPkceLogin({
      clientId: "YOUR_CLIENT_ID",
      redirectUri: "https://yourapp.com/callback",
    });

    if (tokens) {
      // tokens.id_token, tokens.access_token, tokens.refresh_token, etc.
    }
    ```

    `finishPkceLogin` reads the stored PKCE state, verifies the returned `state` against the stored value (CSRF check), exchanges the code, verifies the returned tokens, and clears `sessionStorage`. It returns `null` when no `code` param is present — safe to call on every page load.
  </Step>
</Steps>

## Common PKCE failures

<AccordionGroup>
  <Accordion title="invalid_grant: code_verifier mismatch">
    The `code_verifier` stored before the redirect doesn't match what you send at exchange time.

    **Most common causes:**

    * User opened a second browser tab, overwriting the stored verifier
    * `sessionStorage` was cleared between redirect and callback (e.g., redirect to a new window)
    * Base64url encoding bug — the verifier must use URL-safe characters (`-` and `_`, not `+` and `/`)

    **Fix:** Use the SDK helpers which handle encoding correctly. If rolling your own, ensure `btoa()` output is made URL-safe.
  </Accordion>

  <Accordion title="code_verifier missing from token request">
    Token exchange returns `invalid_request` even though you're sending the code.

    **Cause:** Forgetting to include `codeVerifier` in the token exchange request when the authorization request included a `code_challenge`.

    **Fix:** Always include `codeVerifier` when using PKCE — even when using a confidential client that also sends a `clientSecret`.
  </Accordion>

  <Accordion title="Callback route handling twice">
    Token exchange fires twice, and the second call fails with `invalid_grant` (code already used).

    **Cause:** React strict mode, HMR, or a route that mounts twice.

    **Fix:** Use a ref or state flag to ensure `exchangeCode` is called exactly once per page load. In React:

    ```ts theme={null}
    const exchanged = useRef(false);
    useEffect(() => {
      if (exchanged.current) return;
      exchanged.current = true;
      handleCallback();
    }, []);
    ```
  </Accordion>
</AccordionGroup>

## Mobile-specific concerns

<AccordionGroup>
  <Accordion title="Deep links and custom URI schemes">
    If you use a deep link (`myapp://callback`) as your `redirect_uri`, validate `state` before trusting any parameters — deep links can be intercepted by other apps on the device.
  </Accordion>

  <Accordion title="App lifecycle and storage">
    On iOS and Android, the app may be backgrounded or terminated between the authorization redirect and the callback. Use app-lifecycle-safe storage (e.g. encrypted shared preferences on Android, Keychain on iOS) rather than in-memory session storage.
  </Accordion>

  <Accordion title="Duplicate callback intents">
    Some Android launchers can deliver the same deep link intent multiple times. Guard against this with the same deduplication pattern as the React strict mode fix above.
  </Accordion>
</AccordionGroup>
