# Failed code exchange

## The error

`POST /oauth/token` with `grant_type=authorization_code` returns `401 Unauthorized` or `{"error": "invalid_grant", "error_description": "Failed exchange"}`.

## Causes

1. PKCE `code_verifier` mismatch. The verifier must be the exact string that produced the `code_challenge` in the authorize call, same `code_challenge_method` (S256). SDKs handle this; hand-rolled flows lose the verifier between redirect hops (stored in memory, page reloaded) and fail.
2. `redirect_uri` mismatch. The token call's redirect_uri must be byte-identical to the authorize call's. Trailing slash and port drift strike here too.
3. Code reuse or expiry. Codes are single-use and expire in ~10 minutes. Double-submitting the callback (React StrictMode double effects, double-clicks) exchanges the code twice; the second fails. Guard the callback handler to run once.
4. Wrong client auth. Confidential clients must authenticate (client_secret_basic or post) with the CURRENT secret. A rotated secret breaks the exchange until the app config is updated.
5. Clock skew: the client assertion or request outside the allowed window.

## Diagnose

- Tenant logs: the failed exchange event names the reason (invalid verifier vs unknown code).
- Check the callback handler runs exactly once (log it).
- Diff the authorize redirect_uri against the token call's.

## Checklist

- Verifier persisted across the redirect (SDK default storage, not a JS variable that dies on reload).
- Single exchange per code; idempotent callback handling.