Sigma Auth
OAuth Features

PKCE (Proof Key for Code Exchange) adds an extra layer of security to OAuth flows by preventing authorization code interception attacks. Sigma Auth fully supports PKCE for both public and confidential clients.

What is PKCE?

PKCE protects against authorization code interception by ensuring that only the client that initiated the OAuth flow can exchange the code for tokens. This is especially important for:

  • Single Page Applications (SPAs) that can't securely store client secrets
  • Mobile applications where the redirect can be intercepted
  • Public clients that don't have backend servers

Supported Methods

Sigma Auth supports both PKCE transformation methods:

Uses SHA256 hashing of the code verifier:

code_challenge = base64url(sha256(code_verifier))

Plain (Fallback)

Uses the code verifier directly as the challenge:

code_challenge = code_verifier

Security Note: Always use S256 method unless your environment doesn't support SHA256. The plain method provides less security.

Implementation Guide

1. Generate Code Verifier

Create a cryptographically random string between 43-128 characters:

function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64urlEncode(array);
}

function base64urlEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
}

const codeVerifier = generateCodeVerifier();
// Store this securely (sessionStorage, memory, etc.)
sessionStorage.setItem('pkce_verifier', codeVerifier);

2. Create Code Challenge

Generate the challenge from your verifier:

async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return base64urlEncode(digest);
}

const codeChallenge = await generateCodeChallenge(codeVerifier);

3. Authorization Request

Include PKCE parameters in your authorization URL:

function createPKCELoginUrl(codeChallenge) {
  const params = new URLSearchParams({
    client_id: 'your-app-name',
    redirect_uri: 'https://yourapp.com/callback',
    response_type: 'code',
    provider: 'sigma',
    state: generateState(), // CSRF protection
    // PKCE parameters
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  });
  
  return `https://auth.sigmaidentity.com/authorize?${params}`;
}

// Redirect user to authorization server
window.location.href = createPKCELoginUrl(codeChallenge);

4. Token Exchange

Include the original code verifier in your token request:

async function exchangeCodeWithPKCE(code, codeVerifier) {
  const response = await fetch('https://auth.sigmaidentity.com/token', {
    method: 'POST',
    headers: { 
      'Content-Type': 'application/x-www-form-urlencoded' 
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      client_id: 'your-app-name',
      redirect_uri: 'https://yourapp.com/callback',
      // PKCE verification
      code_verifier: codeVerifier
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token exchange failed: ${error.error_description}`);
  }

  return await response.json();
}

// In your callback handler
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const storedVerifier = sessionStorage.getItem('pkce_verifier');

if (code && storedVerifier) {
  try {
    const tokens = await exchangeCodeWithPKCE(code, storedVerifier);
    // Clean up stored verifier
    sessionStorage.removeItem('pkce_verifier');
    
    // Store access token and proceed with authentication
    console.log('Access token:', tokens.access_token);
  } catch (error) {
    console.error('PKCE authentication failed:', error);
  }
}

Framework Examples

React with Hooks

import { useState, useEffect, useCallback } from 'react';

function usePKCEAuth() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState(null);

  const generatePKCEPair = useCallback(async () => {
    const verifier = generateCodeVerifier();
    const challenge = await generateCodeChallenge(verifier);
    
    sessionStorage.setItem('pkce_verifier', verifier);
    return { verifier, challenge };
  }, []);

  const login = useCallback(async () => {
    const { challenge } = await generatePKCEPair();
    const loginUrl = createPKCELoginUrl(challenge);
    window.location.href = loginUrl;
  }, [generatePKCEPair]);

  const handleCallback = useCallback(async () => {
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get('code');
    const verifier = sessionStorage.getItem('pkce_verifier');

    if (code && verifier) {
      try {
        const tokens = await exchangeCodeWithPKCE(code, verifier);
        sessionStorage.removeItem('pkce_verifier');
        
        // Get user info
        const userResponse = await fetch('https://auth.sigmaidentity.com/userinfo', {
          headers: { Authorization: `Bearer ${tokens.access_token}` }
        });
        const userData = await userResponse.json();
        
        setUser(userData);
        setIsAuthenticated(true);
        
        // Clean up URL
        window.history.replaceState({}, document.title, window.location.pathname);
      } catch (error) {
        console.error('Authentication failed:', error);
      }
    }
  }, []);

  useEffect(() => {
    handleCallback();
  }, [handleCallback]);

  return { isAuthenticated, user, login };
}

Next.js with App Router

// app/auth/callback/page.js
'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';

export default function CallbackPage() {
  const router = useRouter();

  useEffect(() => {
    const handlePKCECallback = async () => {
      const urlParams = new URLSearchParams(window.location.search);
      const code = urlParams.get('code');
      const verifier = sessionStorage.getItem('pkce_verifier');

      if (code && verifier) {
        try {
          const response = await fetch('/api/auth/token', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ code, codeVerifier: verifier })
          });

          if (response.ok) {
            sessionStorage.removeItem('pkce_verifier');
            router.push('/dashboard');
          } else {
            router.push('/login?error=auth_failed');
          }
        } catch (error) {
          console.error('Auth error:', error);
          router.push('/login?error=auth_failed');
        }
      }
    };

    handlePKCECallback();
  }, [router]);

  return <div>Processing authentication...</div>;
}

Error Handling

PKCE introduces specific error scenarios that you should handle:

Invalid Code Challenge

{
  "error": "invalid_request",
  "error_description": "Invalid code_challenge_method. Must be 'S256' or 'plain'"
}

Code Verifier Mismatch

{
  "error": "invalid_grant", 
  "error_description": "Code verifier does not match challenge"
}

Missing Code Verifier

{
  "error": "invalid_request",
  "error_description": "code_verifier required when code_challenge was used"
}

Handle these errors gracefully in your application:

async function exchangeCodeWithErrorHandling(code, codeVerifier) {
  try {
    const response = await fetch('https://auth.sigmaidentity.com/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        client_id: 'your-app-name',
        redirect_uri: 'https://yourapp.com/callback',
        code_verifier: codeVerifier
      })
    });

    const result = await response.json();

    if (!response.ok) {
      switch (result.error) {
        case 'invalid_grant':
          // Code verifier mismatch - restart flow
          sessionStorage.removeItem('pkce_verifier');
          throw new Error('Authentication session expired. Please try again.');
        
        case 'invalid_request':
          // Malformed request - likely client bug
          throw new Error('Authentication request malformed. Please contact support.');
        
        default:
          throw new Error(`Authentication failed: ${result.error_description}`);
      }
    }

    return result;
  } catch (networkError) {
    throw new Error('Network error during authentication. Please try again.');
  }
}

Security Best Practices

Code Verifier Storage

  • Browser applications: Use sessionStorage (cleared on tab close)
  • Mobile apps: Use secure, temporary storage
  • Never store in localStorage: Persists across sessions unnecessarily

Code Verifier Generation

// Good: Cryptographically secure
function generateSecureCodeVerifier() {
  const array = new Uint8Array(32); // 256 bits
  crypto.getRandomValues(array);
  return base64urlEncode(array);
}

// Bad: Predictable
function generateWeakCodeVerifier() {
  return Math.random().toString(36); // Don't do this
}

Challenge Method Validation

// Always prefer S256
const challengeMethod = crypto.subtle ? 'S256' : 'plain';

if (challengeMethod === 'plain') {
  console.warn('Using plain PKCE method. Upgrade to S256 when possible.');
}

Migration from Basic OAuth

If you're currently using basic OAuth without PKCE, migration is straightforward:

Before (Basic OAuth)

const loginUrl = `https://auth.sigmaidentity.com/authorize?` +
  `client_id=your-app&` +
  `redirect_uri=https://yourapp.com/callback&` +
  `response_type=code`;

After (With PKCE)

const { challenge } = await generatePKCEPair();
const loginUrl = `https://auth.sigmaidentity.com/authorize?` +
  `client_id=your-app&` +
  `redirect_uri=https://yourapp.com/callback&` +
  `response_type=code&` +
  `code_challenge=${challenge}&` +
  `code_challenge_method=S256`;

Backward Compatibility: Basic OAuth flows without PKCE continue to work, but PKCE is strongly recommended for all new implementations.

Testing PKCE Implementation

Manual Testing

  1. Generate verifier/challenge pair and log both values
  2. Start OAuth flow with the challenge
  3. Complete authentication and note the authorization code
  4. Exchange code using the original verifier
  5. Verify success by checking access token validity

Automated Testing

describe('PKCE Flow', () => {
  it('should complete full PKCE authentication', async () => {
    const verifier = generateCodeVerifier();
    const challenge = await generateCodeChallenge(verifier);
    
    // Mock OAuth flow
    const code = await mockAuthorizationFlow(challenge);
    const tokens = await exchangeCodeWithPKCE(code, verifier);
    
    expect(tokens.access_token).toBeDefined();
    expect(tokens.token_type).toBe('Bearer');
  });

  it('should reject invalid verifier', async () => {
    const verifier = generateCodeVerifier();
    const challenge = await generateCodeChallenge(verifier);
    const wrongVerifier = generateCodeVerifier();
    
    const code = await mockAuthorizationFlow(challenge);
    
    await expect(exchangeCodeWithPKCE(code, wrongVerifier))
      .rejects.toThrow('Code verifier does not match challenge');
  });
});

PKCE significantly improves the security of your OAuth implementation with minimal complexity. The additional request parameters and client-side code generation provide strong protection against authorization code interception attacks.