SeloraXDEVELOPERS

PKCE

PKCE (Proof Key for Code Exchange)

PKCE (pronounced "pixy") is a security extension for OAuth 2.0 that protects the authorization code from interception. It is required for public clients (SPAs, mobile apps) and recommended for all clients.

Why PKCE?

Public clients (browser apps, mobile apps) cannot securely store a client_secret. Without PKCE, an attacker who intercepts the authorization code could exchange it for tokens. PKCE prevents this by binding the code to a secret known only to the original requester.

How It Works

1. Client generates random code_verifier (43-128 chars)
2. Client computes code_challenge = BASE64URL(SHA256(code_verifier))
3. Client sends code_challenge in the authorize request
4. SeloraX stores the challenge with the auth code
5. Client sends code_verifier in the token exchange
6. SeloraX verifies: SHA256(code_verifier) === stored challenge

Step 1: Generate Code Verifier and Challenge

// Generate a cryptographically random code verifier
const codeVerifier = crypto.randomBytes(32).toString('base64url');
 
// Compute the S256 challenge
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

The code_verifier must be between 43 and 128 characters. Store it securely (e.g. in sessionStorage) for use during token exchange.

Step 2: Include Challenge in Authorize Request

GET /api/oauth/authorize?response_type=code
  &client_id=sx_oc_...
  &redirect_uri=https://example.com/callback
  &scope=openid+profile+email
  &state=random_string
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
ParameterRequiredDescription
code_challengeYesBASE64URL-encoded SHA256 hash of the code_verifier
code_challenge_methodYesS256 (recommended) or plain

:::tip Always use S256. The plain method sends the verifier as-is and provides no protection against interception. :::

Step 3: Exchange Code with Verifier

When exchanging the authorization code for tokens, include the original code_verifier:

POST /api/oauth/token
{
  "grant_type": "authorization_code",
  "client_id": "sx_oc_...",
  "code": "sx_ic_...",
  "redirect_uri": "https://example.com/callback",
  "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}

Note: No client_secret is needed for public clients. The code_verifier proves the requester is the same entity that initiated the flow.

Full SPA Example

// --- Step 1: Start login ---
async function startLogin() {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');
 
  const state = crypto.randomUUID();
 
  // Store for later
  sessionStorage.setItem('code_verifier', codeVerifier);
  sessionStorage.setItem('oauth_state', state);
 
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'sx_oc_...',
    redirect_uri: window.location.origin + '/auth/callback',
    scope: 'openid profile email',
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });
 
  window.location.href = `https://api.selorax.io/api/oauth/authorize?${params}`;
}
 
// --- Step 2: Handle callback ---
async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');
  const state = params.get('state');
  const error = params.get('error');
 
  if (error) {
    console.error('Authorization denied:', error);
    return;
  }
 
  // Verify state
  if (state !== sessionStorage.getItem('oauth_state')) {
    throw new Error('State mismatch — possible CSRF attack');
  }
 
  const codeVerifier = sessionStorage.getItem('code_verifier');
 
  // Exchange code for tokens
  const res = await fetch('https://api.selorax.io/api/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      client_id: 'sx_oc_...',
      code,
      redirect_uri: window.location.origin + '/auth/callback',
      code_verifier: codeVerifier,
    }),
  });
 
  const tokens = await res.json();
 
  // Fetch user profile
  const userRes = await fetch('https://api.selorax.io/api/oauth/userinfo', {
    headers: { 'Authorization': `Bearer ${tokens.access_token}` },
  });
 
  const user = await userRes.json();
  console.log('Logged in as:', user.name, user.email);
 
  // Clean up
  sessionStorage.removeItem('code_verifier');
  sessionStorage.removeItem('oauth_state');
}

:::warning Token storage in SPAs Store tokens in memory (JavaScript variables) rather than localStorage or sessionStorage to reduce XSS risk. Use short-lived access tokens and refresh via the token endpoint when needed. :::

Challenge Methods

MethodDescriptionSecurity
S256BASE64URL(SHA256(code_verifier))Recommended — verifier cannot be derived from challenge
plaincode_challenge === code_verifierWeak — only use for debugging/testing

Error Handling

If PKCE verification fails during token exchange:

{
  "message": "Invalid PKCE code_verifier.",
  "error": "invalid_grant",
  "status": 400
}

Common causes:

  • code_verifier doesn't match the code_challenge sent during authorization
  • code_verifier was not provided but code_challenge was stored with the auth code
  • Using S256 method but computing the hash incorrectly (must be SHA256, then base64url-encoded without padding)