The problem
An app needs to call an API on a user's behalf. It must never handle the user's password, and the tokens it receives must not leak on the way. The older answers each had a hole: the resource owner password grant hands the credentials to the app, and the implicit grant returns the access token in the redirect URL's fragment, where it lands in browser history and any script on the page can read it. The plain authorization code grant is better, because only a short-lived, single-use code travels through the browser and the tokens come back over a direct request. But public clients (single-page apps, mobile and desktop apps) can't keep a client secret, so anyone who intercepts the code could redeem it as if they were the app: a malicious app registered for the same custom URL scheme, a Referer header, a log file.
How it works
PKCE (Proof Key for Code Exchange, RFC 7636, pronounced "pixy") binds the code to the app instance that started the flow:
- Authorization request (front channel). The app creates a fresh, high-entropy
code_verifierand keeps it. It sends onlycode_challenge = BASE64URL(SHA-256(code_verifier))andcode_challenge_method=S256, along with the usualresponse_type=code,client_id,redirect_uri,scopeandstate. - Sign-in and consent. The authorization server authenticates the user and gets consent, then redirects the browser back to the app with a
code(bound to the stored challenge) and the samestate. - Code exchange (back channel). The app sends the code and the
code_verifierto the token endpoint in a direct HTTPS request. The server hashes the verifier and compares it with the stored challenge. Only the instance that generated the verifier can pass, so an intercepted code is worthless. - Use the token. The app sends the access token as a bearer token; the API validates it and serves the request. The refresh token gets new access tokens later without involving the user.
The hash only works one way. Seeing the challenge in step 1 doesn't reveal the verifier, and seeing the code in step 2 isn't enough to redeem it.
When to use it
- Any application that signs users in and calls APIs on their behalf: single-page apps, native mobile and desktop apps, and server-side web apps.
- It's the default for new work. The OAuth 2.0 Security Best Current Practice (RFC 9700) requires PKCE for public clients and recommends it for confidential ones, and the OAuth 2.1 draft builds it into the authorization code grant.
- With OpenID Connect the same flow also signs the user in: add
openidto the scope and the token response includes an ID token. - Not for service-to-service calls with no user (use client credentials) or for devices without a usable browser, such as TVs and CLIs (use the device authorization grant).
Trade-offs
- More moving parts than the implicit flow. Two redirects, a token request, token storage and refresh. Use your identity provider's SDK or a certified library rather than hand-rolling it.
- PKCE protects the code, not the tokens. A bearer token works for whoever holds it. Keep access tokens short-lived and narrowly scoped, and consider sender-constrained tokens (DPoP, RFC 9449, or mutual TLS, RFC 8705) for high-value APIs.
- Browsers are the hardest place to keep tokens. Any XSS in a single-page app can act as the app and use its tokens. The IETF guidance for browser-based apps prefers a backend-for-frontend that runs this flow as a confidential client and gives the browser only an HttpOnly session cookie.
- Refresh tokens are long-lived secrets. For public clients, RFC 9700 requires them to be sender-constrained or rotated on every use.
- Self-contained JWTs can't be revoked instantly. The API trusts a JWT until it expires. To cut access off sooner, keep lifetimes short or use token introspection (RFC 7662).
Implementation notes
- Verifier: 32 bytes from a cryptographically secure random generator, base64url-encoded without padding (43 characters). Use one per authorization request, keep it next to
state, and delete both after the exchange. Always useS256: theplainmethod sends the verifier itself and only exists for clients that can't compute SHA-256. stateandnonce: send an unguessablestateand check it on the callback. PKCE (when the server enforces it) and the OpenID Connectnoncealso stop CSRF, butstateis cheap and carries app context across the redirect.- Redirect URIs: register them exactly; the server must compare them by exact string match, except that it must accept any port on a native app's loopback redirect URI (RFC 9700, RFC 8252 §7.3). Native apps open the system browser, never an embedded web view, and receive the redirect on a claimed HTTPS link, a private-use URI scheme or a loopback address (RFC 8252).
- Confidential clients (a web backend) also authenticate at the token endpoint, preferably with
private_key_jwtor mutual TLS rather than a shared secret. PKCE still applies on top. - Authorization server: make codes single-use and short-lived (RFC 6749 recommends at most 10 minutes), bind them to
client_id,redirect_uriand the challenge, and reject a token request that carries acode_verifierwhen the authorization request had no challenge (a PKCE downgrade). - API: validate JWT access tokens locally (RFC 9068): the signature with keys from the issuer's JWKS (cached, and refreshed when an unknown
kidshows up), theniss,aud,expand the required scope. Send opaque tokens to the introspection endpoint instead. - Products: every mainstream identity provider supports this flow, for example Microsoft Entra ID, Amazon Cognito, Google Identity, Okta, Auth0, Keycloak and Ping Identity. Client libraries include MSAL, AppAuth (iOS and Android), oidc-client-ts, openid-client (Node.js) and Spring Security.