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:
S256 (Recommended)
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. Theplain
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
- Generate verifier/challenge pair and log both values
- Start OAuth flow with the challenge
- Complete authentication and note the authorization code
- Exchange code using the original verifier
- 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.