Skip to content

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.

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.

PKCE adds two parameters to the OAuth flow:

  1. Code verifier - A cryptographically random string (43-128 characters) generated by the client
  2. Code challenge - A SHA256 hash of the code verifier, sent in the authorization request

The flow works like this:

  1. Client generates a random code_verifier
  2. Client computes code_challenge = BASE64URL(SHA256(code_verifier))
  3. Client sends code_challenge and code_challenge_method=S256 to the authorize endpoint
  4. Authorization server stores the challenge with the authorization code
  5. When exchanging the code for tokens, client sends the original code_verifier
  6. 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.

Goiabada supports configurable PKCE enforcement at two levels:

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.

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

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

When PKCE is optional but a client provides PKCE parameters, Goiabada validates them strictly:

  • code_challenge_method must be S256 (plain is not supported)
  • code_challenge must be 43-128 characters
  • Both parameters must be provided together (partial PKCE is rejected)
  • At the token endpoint, code_verifier must be provided if PKCE was used during authorization
  • If PKCE was not used during authorization, providing code_verifier at the token endpoint is an error

This ensures that clients using PKCE get full security benefits, even when PKCE is optional globally.

Here’s how to generate PKCE parameters in different languages:

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[:])
}
import secrets
import hashlib
import 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()
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.

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=abc123

Then include the code verifier when exchanging the code:

Terminal window
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"
  1. Keep PKCE required globally - Only make it optional when necessary for compatibility
  2. Use per-client overrides sparingly - Document why each exception exists
  3. Plan for migration - If you disable PKCE for legacy clients, create a plan to update them
  4. Generate fresh verifiers - Create a new code verifier for each authorization request
  5. Store verifiers securely - Keep the code verifier in memory or secure storage until token exchange