PKCE (Proof Key for Code Exchange, RFC 7636) binds an OAuth authorization code to the client that asked for it. The client sends a hash of a secret up front and reveals the secret only when redeeming the code, so an attacker who steals the code alone cannot use it. Public clients must use it.
The problem it solves
In the authorization code flow, the browser or app receives a short-lived code in a redirect and trades it for tokens. On mobile and other public clients, another app can sometimes register the same redirect scheme and read that code. Without PKCE, whoever holds the code can redeem it.
How the flow works
- The client creates a random
code_verifierfor each request. - It derives a
code_challengefrom the verifier and sends the challenge and method in the authorization request. - The server stores the challenge with the issued code.
- At the token endpoint, the client sends the code and the original verifier.
- The server transforms the verifier and compares. A mismatch returns
invalid_grant.
An attacker with only the code does not have the verifier, so the exchange fails.
Verifier rules
The verifier is 43 to 128 characters drawn from letters, digits, and the characters -, ., _ and ~. The RFC suggests at least 256 bits of entropy. The easy way is 32 random bytes encoded as base64url, which gives a 43 character string.
import os, base64, hashlib
verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode("ascii")).digest()
).rstrip(b"=").decode()
Use a cryptographic random source, not a general purpose random function.
S256 versus plain
With S256 the challenge is the base64url of the SHA-256 of the verifier. Seeing the challenge does not reveal the verifier. With plain the challenge equals the verifier, so anyone who can observe the authorization request can recover it, which defeats the purpose.
Two details trip people up. If you omit the method, the default is plain. And a client must not fall back to plain after S256 fails. Use S256 everywhere.
Who must use it
RFC 9700, the OAuth security best current practice, says public clients MUST use PKCE and confidential clients SHOULD. Authorization servers must support it and must reject a token request that includes a verifier when no challenge was sent in the authorization request. That last rule blocks downgrade attacks.
Common mistakes
- Reusing one verifier across requests. Generate a new one each time.
- Logging the verifier or putting it in a URL.
- Sending PKCE but not enforcing it on the server, so an attacker simply leaves it out.
- Keeping the challenge method configurable to plain "for testing" and forgetting it in production.
Quick check
Do server side apps with a client secret still need PKCE?
RFC 9700 recommends it for confidential clients too. It adds protection against code injection and costs almost nothing.
Is PKCE a replacement for the redirect URI check?
No. They address different attacks. Keep exact redirect URI matching as well.
Sources
Last reviewed: 19 October 2026.