Proof Key for Code Exchange (PKCE)
PKCE (pronounced "pixy") is a security extension to OAuth 2.0 that protects the authorization code exchange from interception attacks. Originally designed for mobile and single-page apps that cannot store a client secret, PKCE is now a best practice for all OAuth clients.
SingleSign requires PKCE for public clients and recommends it for confidential (server) clients.
The Problem PKCE Solves
In the standard Authorization Code flow, an attacker who intercepts the authorization code could exchange it for tokens. This is especially risky for:
- Mobile apps — Custom URI schemes can be registered by multiple apps. A malicious app could register the same scheme and intercept the redirect.
- SPAs — JavaScript in the browser is inherently exposed. An XSS vulnerability could leak the authorization code.
- Network attacks — An attacker on the same network could observe the redirect URL in certain scenarios.
Without PKCE, whoever has the authorization code can exchange it for tokens. With PKCE, only the party that started the flow can complete it.
How PKCE Works
PKCE adds a cryptographic proof that links the authorization request to the token exchange. Here's the process:
1. Generate a Code Verifier
The code verifier is a cryptographically random string between 43 and 128 characters, using unreserved characters (A-Z, a-z, 0-9, -, ., _, ~).
function generateCodeVerifier(): string {
const array = new Uint8Array(32); // 32 bytes → 43 base64url chars
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
Why 32 bytes? 32 bytes of randomness provides 256 bits of entropy, which is more than enough to prevent brute-force attacks. The base64url encoding expands this to 43 characters, meeting the minimum length requirement.
2. Compute the Code Challenge
The code challenge is a SHA-256 hash of the verifier, base64url-encoded. This is the S256 method.
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(digest));
}
The relationship: code_challenge = BASE64URL(SHA256(code_verifier))
3. Send the Challenge with the Authorization Request
Include the code challenge and method in the authorization URL:
GET https://api.singlesign.com/oauth/authorize
?platform_guid=YOUR_PLATFORM_GUID
&response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid+profile+email
&state=RANDOM_STATE
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
SingleSign stores the challenge and associates it with the authorization session.
4. Send the Verifier with the Token Request
When exchanging the authorization code, include the original code verifier:
POST https://api.singlesign.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&client_id=YOUR_CLIENT_ID
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&platform_guid=YOUR_PLATFORM_GUID
5. Server Verification
SingleSign computes BASE64URL(SHA256(code_verifier)) and compares it to the stored code_challenge. If they match, the tokens are issued. If not, the request is rejected with invalid_grant.
The S256 Method
SingleSign supports the S256 (SHA-256) challenge method. The plain method (where the challenge equals the verifier) is not supported because it provides no security benefit — an attacker who intercepts the challenge could replay it.
S256: code_challenge = BASE64URL(SHA256(code_verifier))
Base64url Encoding
Standard base64 uses +, /, and = characters that are not safe in URLs. Base64url replaces them:
| Standard Base64 | Base64url |
|---|---|
+ | - |
/ | _ |
= (padding) | removed |
function base64UrlEncode(bytes: Uint8Array): string {
let binary = "";
bytes.forEach((b) => (binary += String.fromCharCode(b)));
return btoa(binary)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
When to Use PKCE
| Client Type | PKCE | Reason |
|---|---|---|
| SPA (public) | Required | No client secret; PKCE is the only protection. |
| Mobile / Native (public) | Required | Custom URI schemes are susceptible to interception. |
| Server web app (confidential) | Recommended | Defense in depth alongside the client secret. |
| Machine-to-machine | N/A | Uses Client Credentials grant, not Authorization Code. |
Security Properties
- One-time use — Each code verifier + challenge pair is valid for a single authorization flow.
- Cryptographic binding — The SHA-256 hash makes it computationally infeasible to derive the verifier from the challenge.
- Replay protection — Even if an attacker captures both the authorization code and the challenge, they cannot compute the verifier.
- No shared secrets — Unlike client secrets, PKCE works without any pre-shared key material.
Common Mistakes
1. Reusing the Same Verifier
Problem: Generating a single verifier and using it for all login attempts.
Fix: Generate a fresh code_verifier for every authorization request.
2. Storing the Verifier in the Wrong Place
Problem: Storing the verifier in localStorage (persists across tabs and sessions) or a cookie (sent with requests).
Fix: Use sessionStorage (browser) or an in-memory variable. For mobile apps, use the platform keychain/keystore.
3. Using the plain Method
Problem: Sending code_challenge_method=plain (or omitting it, which some servers interpret as plain).
Fix: Always use S256. SingleSign does not support plain.
4. Mismatched Verifier and Challenge
Problem: The verifier sent with the token request doesn't correspond to the challenge sent with the authorization request (e.g., regenerated instead of stored).
Fix: Store the verifier before redirecting and retrieve the exact same value on callback.
PKCE in Different Languages
Python
import hashlib
import base64
import os
code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode("ascii")
code_challenge = (
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("ascii")).digest())
.rstrip(b"=")
.decode("ascii")
)
Java
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
byte[] digest = MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes("US-ASCII"));
String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
Go
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
)
verifierBytes := make([]byte, 32)
rand.Read(verifierBytes)
codeVerifier := base64.RawURLEncoding.EncodeToString(verifierBytes)
challengeHash := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(challengeHash[:])
Next Steps
- SPA with PKCE Guide — Complete browser-app integration.
- Mobile / Native Guide — PKCE with custom URI schemes.
- OAuth 2.0 & OpenID Connect — The protocols behind PKCE.
- Troubleshooting — Fix PKCE-related errors.
Last updated: 2026-02-16
Related docs
- SingleSign Developer DocumentationLearn how to integrate SingleSign identity and authentication into your applications using OAuth 2.0 and OpenID Connect.
- React authentication with PKCEReact authentication done the recommended way: Authorization Code with PKCE, and no client secret in your JavaScript bundle.
- Mobile app authenticationMobile app authentication for iOS and Android: Authorization Code with PKCE in a system browser, custom scheme redirects, and secure token storage on device.
- SaaS single sign-onSSO for SaaS applications using OAuth 2.0 and OpenID Connect: the flow to pick, the scopes to request, and a working integration path with SingleSign.
- the SPA with PKCE docsComplete guide to integrating SingleSign into a single-page application using the Authorization Code flow with PKCE.
- Identity provider security, by designIdentity provider security at SingleSign: PKCE-protected code exchange, redirect URI allow-lists, MFA, session and device control, and rate limiting.
- SingleSign vs Firebase AuthenticationSingleSign vs Firebase Authentication on lock-in, portability and consent, plus the cases where staying on Firebase is right.
- Redirect URIsRules for redirect URIs in SingleSign: HTTPS requirements, exact matching, allow-listing, and common mistakes to avoid.
- Auth0 comparisonSingleSign vs Auth0 compared on the difference that matters: who owns the account. A side-by-side table, then the three cases where each one is the right call.
- authentication for startupsAuthentication for startups without a monthly-active-user meter: standards-based OAuth 2.0 sign-in you will not have to replace when the product finds traction.
- the Quickstart Guide docsGo from zero to authenticated in six steps. Complete JavaScript/TypeScript code samples included.
- the Mobile & Native Apps docsIntegrate SingleSign into iOS, Android, and desktop applications using PKCE with custom URI schemes and deep links.