Skip to content

PKCE — Securing OAuth2 Authorization Code for Mobile Apps and SPAs

DodaTech Updated 2026-06-28 4 min read

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

  1. What does PKCE stand for and what problem does it solve?
  2. What is the difference between code_verifier and code_challenge?
  3. Why can't mobile apps use the standard Authorization Code grant with a client secret?
  4. What is the code_challenge_method and which is recommended?
  5. Does PKCE replace the state parameter?

Answers:

  1. Proof Key for Code Exchange. It prevents authorization code interception attacks on public clients.
  2. 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.
  3. Mobile apps are public clients — the binary can be decompiled to extract any embedded secrets. PKCE provides security without a client secret.
  4. S256 (SHA-256) is the recommended method. plain sends the verifier directly and is less secure.
  5. 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

Why is PKCE called 'pixie'?

PKCE was originally pronounced 'pixie' by the authors. Some people say 'P-K-C-E'. Both are accepted.

Does PKCE work with all authorization servers?

Most modern OAuth2 servers support PKCE. Check the provider's documentation. Since OAuth2 Security BCP, PKCE is recommended for all clients.

Can PKCE be used with confidential clients?

Yes. PKCE is recommended for all clients, even confidential ones with a client secret. It provides defense in depth.

What happens if the code_verifier doesn't match?

The token endpoint returns an error (invalid_grant). The authorization server computed SHA256(the verifier you sent) and it did not match the challenge from the auth request.

Is PKCE vulnerable to man-in-the-middle attacks?

PKCE protects the token exchange, not the initial redirect. HTTPS protects the entire flow. With HTTPS + PKCE + state, the flow is well protected.

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