- Client keeps a fresh verifier and sends its S256 challenge
- Authorization server binds the challenge to the issued code
- Token endpoint hashes the supplied verifier and compares it
The verifier and challenge aren’t two interchangeable random strings. One is a secret the client retains; the other is a deterministic transformation of it. If an authorization request contains a challenge unrelated to the verifier later sent at redemption, PKCE should fail even when the user signed in successfully.
That is a useful boundary to inspect in assistant-generated code: find where the verifier is created, where it is saved, and where the exact saved value is retrieved. A helper called generatePKCE proves nothing if the callback generates a new pair.
The server checks a commitment made before authorization
PKCE extends the authorization code flow rather than replacing it. The client creates a verifier before navigating to the authorization endpoint, sends its challenge with the request, and later sends the verifier alongside the returned code.
The authorization server associates the challenge and transformation method with the issued code. During redemption, it derives the challenge from the submitted verifier and compares the result with that saved value. If they don’t match, RFC 7636 §4.6 requires rejection with invalid_grant in the standard OAuth response.
The intended protection is against an attacker redeeming an intercepted code without the verifier. It doesn’t make the code harmless to leak, and it doesn’t authenticate the human. The provider’s authentication step still does that job.
PKCE also doesn’t turn the resulting access token into a proof-of-possession token. Once tokens are issued, their own usage and protection rules apply. A bearer token remains usable by its holder unless the token contract adds a separate sender constraint.
S256 has an exact byte-level recipe
The S256 challenge is unpadded base64url encoding of the SHA-256 digest of the verifier’s ASCII bytes. Hash the verifier string, not the random bytes used before encoding it, and encode the binary digest rather than a hexadecimal rendering of that digest.
RFC 7636 specifies a verifier of 43–128 characters drawn from ASCII letters, digits, hyphen, period, underscore, and tilde. It recommends generating 32 random bytes and encoding them as unpadded base64url, producing a 43-character verifier. Use a cryptographically secure random generator and create a fresh value for each transaction.
| Artifact | Construction | Where it goes |
|---|---|---|
| Verifier | Fresh cryptographically random allowed string | Saved transaction, then token request |
| Challenge | SHA-256 of verifier ASCII bytes, then unpadded base64url | Authorization request |
| Method | Literal S256 | Authorization request, bound to the code |
| State | Separate random transaction-correlation value | Authorization request and callback check |
Base64url changes the alphabet used by ordinary base64 and omits trailing padding. A +, /, or trailing = can signal the wrong encoding. Accidental whitespace, hashing a JSON-quoted string, double URL encoding, or using a hex digest can produce a plausible-looking value that fails the actual comparison.
The published Appendix B test vector makes this inspectable without credentials: verifier dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk corresponds to S256 challenge E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM. These are fixed RFC example values, not fresh secrets or output from a live sign-in. Use them to check encoding, never as a reusable login pair.
Changing the verifier must break the proof
A deliberate wrong-verifier case tests the binding, not the provider’s password check. Hold the saved challenge constant and submit a different valid verifier during redemption. Recomputing the challenge from the new value won’t satisfy the commitment attached to the code.
- Authorization request commits to the challenge derived from verifier A.
- Redemption submits verifier B for the code bound to that original challenge.
- Server derives B’s challenge, finds the mismatch, and rejects token issuance.
Correct proof is necessary, not sufficient. An expired or reused code, a different client, or a mismatched redirect can still make redemption fail. Don’t respond by removing PKCE or regenerating the verifier; identify which binding is wrong.
Likewise, a client that sends PKCE parameters isn’t protected unless the authorization server actually enforces them. RFC 9700 requires enforcement of valid challenges and protection against downgrade attacks. Verify provider support through its documented contract or metadata instead of inferring it from a successful redirect.
A client secret and verifier answer different questions
A confidential client’s authentication proves control of its registered client credentials. PKCE proves possession of this transaction’s verifier. A shared backend secret isn’t transaction-specific, while a verifier doesn’t establish that the caller is your registered backend.
This is why “PKCE means no secret” is the wrong generalization. Public clients can’t keep a bundled secret confidential, but confidential clients can still authenticate and use PKCE together. RFC 9700 §2.1.1 requires PKCE for public clients and recommends it for confidential clients too.
State has another purpose: correlating the callback with the initiating browser transaction. Properly bound PKCE can provide CSRF protection under the security BCP’s conditions, but that doesn’t override an integration’s required state validation. In OIDC, a nonce can additionally bind returned identity evidence to the authentication request.
AnyOAuth requires both S256 and backend authentication
AnyOAuth projects receive a client ID and backend secret. The customer authorization request requires a registered callback, browser-bound state, and S256 challenge. Backend /v1/token redemption requires the secret, handoff code, saved verifier, and callback. Client-side packages don’t perform secret-bearing exchange operations.
The supported providers are Google and GitHub. After provider identity checks, AnyOAuth issues its own customer handoff code; the customer verifier binds that handoff, not a provider code the customer never receives. The docs describe transaction helpers and atomic transaction consumption for the host app.
After this proof is checked, your backend still has to retrieve the identity and create its application session. PKCE doesn’t perform those steps. It also says nothing about enterprise federation; OAuth versus SAML addresses that separate topic.
Keep the explanation as narrow as the mechanism: PKCE protects code redemption. OAuth’s broader model includes resource permissions, and the security model includes identity and session boundaries. None of those jobs should disappear behind a single reassuring function name.
Sources
Primary references retrieved 2026-10-06:
- IETF, RFC 7636: Proof Key for Code Exchange, especially §§4.1–4.6, 7.2, and Appendix B.
- IETF, RFC 6749: The OAuth 2.0 Authorization Framework, especially §§2.1–2.3 and 4.1.
- IETF, RFC 9700: Best Current Practice for OAuth 2.0 Security, especially §§2.1.1, 4.7, and 4.8.
Try changing the proof
This local demonstration starts with RFC 7636's published test vector. Change the redemption verifier and compare it against the original challenge. No sign-in request or credential leaves your browser.
Challenge saved at authorizationE9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
The published verifier matches the saved challenge. Comparing a different verifier must fail.