OAuth 2.0 PKCE — Complete Mobile and SPA Auth Guide
In this tutorial, you will learn about OAuth 2.0 PKCE. We cover key concepts, practical examples, and best practices to help you master this topic.
PKCE (Proof Key for Code Exchange) is an extension to the OAuth 2.0 authorization code flow designed for public clients (mobile apps and SPAs) that cannot securely store a client_secret.
What You'll Learn
You'll learn how PKCE works, how to generate the code challenge and verifier, and how to implement it in mobile and browser applications.
Why It Matters
Mobile apps and SPAs cannot keep client secrets secret. PKCE eliminates the need for a client_secret by using a dynamically generated cryptographic key that is never stored.
Real-World Use
A React SPA uses PKCE to authenticate users with Auth0. The app generates a code verifier, creates a code challenge, and exchanges the authorization code using the verifier. No client_secret is ever stored in the browser.
sequenceDiagram
participant App as SPA/Mobile App
participant Auth as Authorization Server
App->>App: Generate code_verifier (random string)
App->>App: code_challenge = SHA256(code_verifier)
App->>Auth: Authorize (code_challenge, method=S256)
Auth->>App: Authorization code
App->>Auth: POST /token (code, code_verifier)
Auth->>Auth: Verify SHA256(verifier) == challenge
Auth->>App: access_token + refresh_token
Teacher's Mindset
PKCE is like a self-destructing message. The client sends a locked box (code challenge) with the auth request. When exchanging the code, it sends the key (code verifier). The server verifies the key opens the box, and both the key and box are discarded.
Implementation
import hashlib
import base64
import secrets
import requests
def generate_pkce_pair():
code_verifier = secrets.token_urlsafe(64)[:128]
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip("=").decode()
return code_verifier, code_challenge
def create_auth_url(client_id, redirect_uri, code_challenge):
params = {
"response_type": "code",
"client_id": client_id,
"redirect_uri": redirect_uri,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"scope": "openid profile",
"state": secrets.token_urlsafe(32)
}
return f"https://auth.example.com/authorize?{urllib.parse.urlencode(params)}"
def exchange_code_for_token(auth_code, code_verifier, client_id, redirect_uri):
response = requests.post(
"https://auth.example.com/token",
data={
"grant_type": "authorization_code",
"code": auth_code,
"client_id": client_id,
"redirect_uri": redirect_uri,
"code_verifier": code_verifier
},
headers={"Content-Type": "application/x-www-form-urlencoded"}
)
return response.json()
# Usage
verifier, challenge = generate_pkce_pair()
print(f"Verifier: {verifier[:20]}...")
print(f"Challenge: {challenge[:20]}...")
Common Mistakes
| Mistake | Fix |
|---|---|
| Using "plain" code challenge method | S256 is more secure; plain sends verifier in auth request |
| Incorrect base64 encoding | Use URL-safe base64 without padding |
| Verifier too short | Minimum 43 characters, maximum 128 |
| Not using PKCE for public clients | Without PKCE, public clients need secret they cannot protect |
| Reusing code verifier | Generate new verifier for each auth request |
Practice Questions
- Why is PKCE needed for mobile apps?
- How does the code challenge prevent interception attacks?
- What is the difference between S256 and plain methods?
- Why can't SPAs use a client_secret?
- Does PKCE replace the state parameter?
Challenge
Implement the full PKCE flow with a mock authorization server. The client generates a verifier/challenge, requests authorization, receives the code, and exchanges it using the verifier. Test that invalid verifiers are rejected.
What's Next
Learn about Openid Connect for user authentication on top of OAuth 2.0.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro