OAuth 2.0 with PKCE Explained
PKCE hardens the OAuth 2.0 Authorization Code flow so a stolen authorization code can't be redeemed. The client proves it started the flow with a code_verifier. Here's how the pieces fit and the check that makes it secure.
Why PKCE exists
In the plain Authorization Code flow, an attacker who intercepts the one-time code (via a
redirect leak, a malicious app, etc.) can exchange it for tokens. PKCE (Proof Key for
Code Exchange) fixes that: the client proves it's the same party that started the flow.
It's now the standard for public clients (SPAs, mobile) - and replaces the deprecated
implicit grant.
The flow
- Client generates a random
code_verifierand itscode_challenge=base64url(SHA256(verifier)). - /authorize: client sends the challenge (+
code_challenge_method=S256). The server stores it with the issuedcode. - /token: client exchanges the
codeplus the rawcode_verifier. - Server verifies:
SHA256(verifier)must equal the storedchallenge. Match -> issue tokens. Mismatch -> reject.
A stolen code is useless without the matching verifier, which never left the client.
The check that makes it secure
This is the step that's easy to skip (and fatal to skip) - the token endpoint must verify the verifier before issuing a token:
import hashlib, base64
def verify_pkce(code_verifier: str, stored_challenge: str) -> bool:
digest = hashlib.sha256(code_verifier.encode()).digest()
computed = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() # base64url, no padding
return computed == stored_challenge
# in /token:
if not verify_pkce(request.form["code_verifier"], entry["code_challenge"]):
return jsonify({"error": "invalid_grant"}), 400
# only now issue the access token
The details that bite people
- Use S256, not
plain. Theplainmethod sends the verifier as the challenge - no protection. Alwayscode_challenge_method=S256. - base64url, no padding. The challenge is base64url (URL-safe alphabet) with the
=padding stripped - get this wrong and every comparison fails. - One-time codes, short TTL. The
codemust be single-use and expire fast. - Still validate
redirect_uri- PKCE complements, not replaces, exact redirect-URI matching.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Generating a code_verifier + S256 code_challenge
- Verifying the verifier at the token endpoint (base64url, no padding)
- Knowing why S256 + one-time codes matter
FAQ
What is PKCE in OAuth 2.0?
Proof Key for Code Exchange - the client generates a secret code_verifier and sends its hash (code_challenge) when starting the flow, then sends the raw verifier when exchanging the code. The server checks they match, so a stolen authorization code can't be redeemed.
Why use PKCE instead of the implicit grant?
The implicit grant returns tokens directly in the redirect (exposed in URLs/history) and is deprecated. Auth Code + PKCE keeps tokens out of the front channel and protects the code from interception, which is why it's the standard for SPAs and mobile apps.
How does the server verify PKCE?
At the token endpoint it computes base64url(SHA256(code_verifier)) and compares it to the code_challenge stored at /authorize. If they don't match it rejects the exchange with invalid_grant. Use the S256 method, not plain.
What is the PKCE flow in OAuth?
PKCE (Proof Key for Code Exchange) hardens the authorization-code flow: the client sends a hashed code_challenge up front and the original code_verifier when exchanging the code. A stolen auth code is useless without the verifier, so it is safe for mobile apps and SPAs.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →