PKCE — Securing OAuth2 Authorization Code for Mobile Apps and SPAs
In this tutorial, you will learn about PKCE. We cover key concepts, practical examples, and best practices to help you master this topic.
Proof Key for Code Exchange (PKCE, pronounced "pixie") extends the Authorization Code flow with a cryptographic challenge that prevents interception attacks on public clients.
What You'll Learn
How PKCE works, the code verifier and code challenge mechanism, implementation for mobile and SPA clients, and why PKCE replaces the client secret.
Why It Matters
Mobile apps and single-page applications cannot keep a client secret — anyone can decompile the app or inspect the source code. PKCE adds a cryptographic proof that the same client that started the flow completes it, without needing a secret.
Real-World Use
Auth0 uses PKCE for all public clients by default. Google and GitHub recommend PKCE for mobile and SPA OAuth2 implementations. Any app using "Sign in with Google" on mobile uses PKCE.
sequenceDiagram
participant App as Mobile App
participant Auth as Auth Server
App->>App: Generate code_verifier (random string)
App->>App: code_challenge = SHA256(code_verifier)
App->>Auth: /auth?code_challenge=xyz&code_challenge_method=S256
Auth->>App: Authorization Code
App->>Auth: POST /token & code + code_verifier
Auth->>Auth: SHA256(code_verifier) == code_challenge?
Auth->>App: Access Token (if match)
Code Example: PKCE Flow
import requests
import hashlib
import base64
import secrets
def generate_pkce_pair():
"""Generate code_verifier and code_challenge for PKCE."""
code_verifier = secrets.token_urlsafe(64)[:128]
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
return code_verifier, code_challenge
# Step 1: Generate PKCE pair
code_verifier, code_challenge = generate_pkce_pair()
print(f"Verifier: {code_verifier[:20]}...")
print(f"Challenge: {code_challenge[:20]}...")
# Step 2: Build authorization URL with challenge
AUTH_URL = "https://auth.example.com/oauth/authorize"
CLIENT_ID = "public-client"
REDIRECT_URI = "myapp://callback"
auth_params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"state": secrets.token_urlsafe(32)
}
# (User is redirected to authorization URL)
# Step 3: Exchange code for token (in callback handler)
TOKEN_URL = "https://auth.example.com/oauth/token"
code = "authorization-code-from-redirect"
token_response = requests.post(TOKEN_URL, data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"code_verifier": code_verifier # No client_secret needed!
})
print(f"Token response: {token_response.status_code}")
print(token_response.json())
PKCE vs Client Secret
| Aspect | Client Secret | PKCE |
|---|---|---|
| Secret | Static string shared at registration | Dynamic, single-use cryptographic proof |
| Storage | Must be kept confidential | No secret to store |
| Replay | Secret can be stolen and reused | Verifier is single-use |
| Public Clients | Impossible to keep secret | Works perfectly |
| Implementation Complexity | Low | Medium (crypto operations) |
Common Mistakes
1. Using a Short or Predictable Code Verifier
The verifier must be at least 43 characters and at most 128 characters, using unreserved URL characters. Use secrets.token_urlsafe(64).
2. Not Stripping Base64 Padding
The code challenge must have = padding stripped. Otherwise, the authorization server may reject it.
3. Forgetting to Store the Verifier
The verifier is generated on the client and must be available when the redirect arrives. Store it in sessionStorage (SPA) or secure device storage (mobile).
4. Using PKCE Without State Parameter
PKCE prevents code interception, not CSRF. Always use the state parameter alongside PKCE.
5. Implementing PKCE with Plain Method
The S256 method hashes the verifier. The plain method sends it directly. Only use plain if the authorization server does not support S256.
Practice Questions
- What does PKCE stand for and what problem does it solve?
- What is the difference between code_verifier and code_challenge?
- Why can't mobile apps use the standard Authorization Code grant with a client secret?
- What is the code_challenge_method and which is recommended?
- Does PKCE replace the state parameter?
Answers:
- Proof Key for Code Exchange. It prevents authorization code interception attacks on public clients.
- The code_verifier is a random secret stored on the client. The code_challenge is a transformed version (SHA256 hash) sent in the auth request.
- Mobile apps are public clients — the binary can be decompiled to extract any embedded secrets. PKCE provides security without a client secret.
S256(SHA-256) is the recommended method.plainsends the verifier directly and is less secure.- No. PKCE prevents code interception, while state prevents CSRF. Both are needed for complete security.
Challenge: Implement a mobile app or SPA OAuth2 flow with PKCE. Generate the verifier, compute the challenge, store the verifier, exchange the code, and handle errors.
FAQ
Mini Project
Build a Python script that implements the full PKCE flow: generate code_verifier and code_challenge, construct the authorization URL, simulate the redirect callback, and exchange the code for tokens using the verifier.
What's Next
Now learn about OAuth2 Scopes — how to define and request granular permissions for fine-grained access control.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro