PKCE
PKCE (Proof Key for Code Exchange, pronounced “pixy”) is a security extension to OAuth 2.0 that protects authorization codes from interception attacks. It’s defined in RFC 7636.
Why PKCE matters
Section titled “Why PKCE matters”In the authorization code flow, the authorization server returns a code to the client via a redirect URI. This code can potentially be intercepted by:
- Malicious apps on mobile devices that register the same custom URL scheme
- Browser extensions or malware that can read URLs
- Network attackers in certain scenarios
PKCE prevents these attacks by ensuring that only the client that initiated the authorization request can exchange the code for tokens.
How PKCE works
Section titled “How PKCE works”PKCE adds two parameters to the OAuth flow:
- Code verifier - A cryptographically random string (43-128 characters) generated by the client
- Code challenge - A SHA256 hash of the code verifier, sent in the authorization request
The flow works like this:
- Client generates a random
code_verifier - Client computes
code_challenge = BASE64URL(SHA256(code_verifier)) - Client sends
code_challengeandcode_challenge_method=S256to the authorize endpoint - Authorization server stores the challenge with the authorization code
- When exchanging the code for tokens, client sends the original
code_verifier - Authorization server verifies that
SHA256(code_verifier)matches the stored challenge
If an attacker intercepts the authorization code, they cannot exchange it for tokens because they don’t have the original code_verifier.
PKCE configuration in Goiabada
Section titled “PKCE configuration in Goiabada”Goiabada supports configurable PKCE enforcement at two levels:
Global setting
Section titled “Global setting”The global PKCE setting determines the default behavior for all clients. This can be configured in Settings → General in the admin console.
- PKCE required (default) - All authorization code flows must use PKCE. This is the OAuth 2.1 recommendation and provides the strongest security.
- PKCE optional - Clients can choose whether to use PKCE. When provided, PKCE parameters are validated strictly.
Per-client override
Section titled “Per-client override”Individual clients can override the global setting. This is configured in the client settings under Clients → [Client Name] → Settings.
- Use global setting - Inherits the global PKCE configuration
- PKCE required - This client must always use PKCE, regardless of global setting
- PKCE not required - This client can skip PKCE, regardless of global setting
When to make PKCE optional
Section titled “When to make PKCE optional”The OAuth 2.1 specification recommends PKCE for all authorization code flows. However, there are scenarios where you might need to make it optional:
- Legacy client compatibility - Older OAuth libraries that don’t support PKCE
- Third-party integrations - External applications that cannot be modified
- Gradual migration - Transitioning existing deployments to PKCE
Strict validation
Section titled “Strict validation”When PKCE is optional but a client provides PKCE parameters, Goiabada validates them strictly:
code_challenge_methodmust beS256(plain is not supported)code_challengemust be 43-128 characters- Both parameters must be provided together (partial PKCE is rejected)
- At the token endpoint,
code_verifiermust be provided if PKCE was used during authorization - If PKCE was not used during authorization, providing
code_verifierat the token endpoint is an error
This ensures that clients using PKCE get full security benefits, even when PKCE is optional globally.
Generating PKCE parameters
Section titled “Generating PKCE parameters”Here’s how to generate PKCE parameters in different languages:
JavaScript/Node.js
Section titled “JavaScript/Node.js”const crypto = require('crypto');
function generateCodeVerifier() { return crypto.randomBytes(32).toString('base64url');}
function generateCodeChallenge(verifier) { return crypto.createHash('sha256') .update(verifier) .digest('base64url');}
const codeVerifier = generateCodeVerifier();const codeChallenge = generateCodeChallenge(codeVerifier);import ( "crypto/sha256" "encoding/base64" "crypto/rand")
func generateCodeVerifier() string { b := make([]byte, 32) rand.Read(b) return base64.RawURLEncoding.EncodeToString(b)}
func generateCodeChallenge(verifier string) string { h := sha256.Sum256([]byte(verifier)) return base64.RawURLEncoding.EncodeToString(h[:])}Python
Section titled “Python”import secretsimport hashlibimport base64
def generate_code_verifier(): return secrets.token_urlsafe(32)
def generate_code_challenge(verifier): digest = hashlib.sha256(verifier.encode()).digest() return base64.urlsafe_b64encode(digest).rstrip(b'=').decode()C# / .NET
Section titled “C# / .NET”using System.Buffers.Text;using System.Security.Cryptography;using System.Text;
string GenerateCodeVerifier(){ var bytes = RandomNumberGenerator.GetBytes(32); return Base64Url.EncodeToString(bytes);}
string GenerateCodeChallenge(string verifier){ var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(verifier)); return Base64Url.EncodeToString(bytes);}Note: System.Buffers.Text.Base64Url requires .NET 8 or later.
Using PKCE in authorization requests
Section titled “Using PKCE in authorization requests”Include PKCE parameters in your authorization request:
GET /auth/authorize? client_id=my-app& redirect_uri=https://my-app.com/callback& response_type=code& scope=openid profile& code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM& code_challenge_method=S256& state=abc123Then include the code verifier when exchanging the code:
curl -X POST https://auth.example.com/auth/token \ -d "grant_type=authorization_code" \ -d "client_id=my-app" \ -d "code=SplxlOBeZQQYbYS6WxSbIA" \ -d "redirect_uri=https://my-app.com/callback" \ -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"Best practices
Section titled “Best practices”- Keep PKCE required globally - Only make it optional when necessary for compatibility
- Use per-client overrides sparingly - Document why each exception exists
- Plan for migration - If you disable PKCE for legacy clients, create a plan to update them
- Generate fresh verifiers - Create a new code verifier for each authorization request
- Store verifiers securely - Keep the code verifier in memory or secure storage until token exchange