Authorization Code with PKCE
Overview
PKCE (Proof Key for Code Exchange) is an extension to the OAuth 2.0 Authorization Code Grant (RFC 7636) that provides enhanced security, especially for public clients like single-page applications (SPAs) and mobile apps that cannot securely store a client_secret. PKCE is now recommended by OAuth 2.0 best practices and is already supported in Genesys Cloud.
Status: Implicit Grant deprecated (May 2027 deadline), PKCE is the secure replacement.
Why PKCE?
The Problem PKCE Solves:
Authorization Code Can Be Intercepted:
├─ Attacker monitors network traffic
├─ Captures authorization code
├─ Attempts to exchange code for token
└─ Without PKCE: Attacker succeeds
Public Clients Cannot Store Secrets:
├─ Browser-based apps: No server backend
├─ Mobile apps: Can be reverse engineered
├─ Desktop apps: Can be analyzed
├─ Cannot safely store client_secret
└─ Traditional approach inadequate
PKCE Solution:
Add Proof to Authorization Code:
├─ Generate random code_verifier
├─ Compute code_challenge (hash)
├─ Send code_challenge to auth server
├─ Auth server stores it
├─ Only original verifier can exchange code
└─ Attacker lacks verifier → cannot exchange
Proof Cannot Be Reversed:
├─ code_challenge = SHA256(code_verifier)
├─ Hash is one-way function
├─ Cannot reverse-engineer verifier from hash
├─ Even if code intercepted: useless without verifier
└─ Verifier never sent over network
Complete PKCE Flow
Step 1: Generate Proof Strings
Your Application Generates:
code_verifier:
├─ Random string, 43-128 characters
├─ Cryptographically secure (use crypto random)
├─ Unrepeatable (different each request)
├─ Only stored in memory
├─ Example: "E9Mrozoa2owusvxrFHo89ejyK3OMVZZWhtbQrHfl"
code_challenge:
├─ SHA256 hash of code_verifier
├─ BASE64-URL encoded
├─ Sent to authorization server
├─ Example: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"
code_challenge_method:
├─ "S256" (SHA256 hash)
├─ Only recommended method
├─ "plain" exists but deprecated
└─ Always use "S256"
Pseudo Code:
code_verifier = generateRandomString(128)
code_challenge = BASE64URL(SHA256(code_verifier))
code_challenge_method = "S256"
Step 2: Redirect to Authorization Endpoint
Redirect user browser to:
https://login.mypurecloud.com/oauth/authorize
?client_id=YOUR_CLIENT_ID
&response_type=code
&redirect_uri=https://yourapp.com/callback
&scope=conversations:readonly
&code_challenge=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
&code_challenge_method=S256
&state=random_state_string
Parameters:
client_id:
├─ Your app's public identifier
└─ Can be embedded in SPA code
response_type:
├─ Must be "code"
└─ Returns authorization code
redirect_uri:
├─ Where user is redirected
├─ Can be embedded in SPA code
├─ Example: https://yourapp.com/callback
└─ Must be registered in OAuth client
scope:
├─ Requested permissions
├─ Space-separated list
└─ Example: "conversations:readonly"
code_challenge (PKCE):
├─ SHA256 hash of random string
├─ Sent to auth server
├─ Auth server stores it
└─ Cannot derive original verifier
code_challenge_method (PKCE):
├─ "S256" (recommended and required)
├─ Indicates SHA256 method used
└─ Only secure method
state (CSRF Protection):
├─ Random string
├─ Prevents CSRF attacks
└─ Verified in callback
Step 3: User Authenticates & Consents
Same as Authorization Code Grant:
1. User sees Genesys Cloud login
2. User enters credentials
3. User sees permission consent screen
4. User grants permission
5. Auth server generates authorization code
6. Auth server stores code_challenge with authorization code
Step 4: Callback with Authorization Code
Auth server redirects to callback:
https://yourapp.com/callback
?code=AUTH_CODE_12345abcde67890
&state=random_state_string
In Callback Handler:
1. Verify state parameter (CSRF check)
2. Retrieve authorization code
3. Verify you have code_verifier in memory
4. Proceed to Step 5 (exchange code)
Never:
├─ Do NOT send code_verifier in callback
├─ Do NOT include code_verifier in URL
├─ Do NOT exchange code in browser
└─ Do NOT expose code_verifier to user
Step 5: Exchange Code with PKCE Proof
Now you have:
├─ Authorization code (from Step 4)
├─ code_verifier (generated in Step 1, stored in memory)
└─ client_id (public, stored in app)
Exchange Code:
POST https://login.mypurecloud.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_12345abcde67890
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&code_verifier=E9Mrozoa2owusvxrFHo89ejyK3OMVZZWhtbQrHfl
Parameters:
grant_type:
├─ "authorization_code"
└─ Standard OAuth parameter
code:
├─ Authorization code from Step 4
└─ Single-use only
client_id:
├─ Your public client ID
└─ Can be public
redirect_uri:
├─ Must match redirect_uri from Step 2
└─ Confirms code ownership
code_verifier (PKCE):
├─ Original random string from Step 1
├─ Proves you own the authorization code
├─ MUST match the code_challenge sent in Step 2
└─ Server recomputes SHA256(code_verifier) and compares
NOTE: NO client_secret needed!
├─ PKCE replaces need for client_secret
├─ Perfect for public clients
├─ Proof of ownership is cryptographic
└─ Signature is binding
Step 6: Server Validates & Issues Token
Genesys Cloud Authorization Server:
1. Receives code_verifier
2. Retrieves authorization code from storage
3. Retrieves stored code_challenge
4. Computes: SHA256(code_verifier) → new_hash
5. Compares: new_hash == stored_code_challenge?
If MATCH (✓):
├─ code_verifier is correct
├─ Same application that requested code
├─ Issue access token
└─ Return token in response
If NO MATCH (✗):
├─ code_verifier is wrong
├─ Likely code theft attempt
├─ Reject request with error
└─ Attack prevented!
Response on Success:
HTTP 200 OK
{
"access_token": "abc123xyz789...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "conversations:readonly"
}
Response on Failure:
HTTP 400 Bad Request
{
"error": "invalid_grant",
"error_description": "code_verifier invalid"
}
Step 7: Use Access Token
Same as Authorization Code Grant:
GET /api/v2/conversations
Authorization: Bearer abc123xyz789...
Response:
HTTP 200 OK
{...conversation data...}
JavaScript Implementation Example
// Configuration
const CLIENT_ID = 'your_client_id';
const REDIRECT_URI = 'https://yourapp.com/callback';
const SCOPES = 'conversations:readonly users:readonly';
const GENESYS_REGION = 'mypurecloud.com';
// Step 1: Generate PKCE code challenge
async function generatePKCE() {
// Generate random code_verifier
const array = new Uint8Array(64);
crypto.getRandomValues(array);
const codeVerifier = btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
// Compute code_challenge = BASE64URL(SHA256(code_verifier))
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashString = String.fromCharCode.apply(null, hashArray);
const codeChallenge = btoa(hashString)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return { codeVerifier, codeChallenge };
}
// Step 2: Redirect to authorization
async function login() {
const { codeVerifier, codeChallenge } = await generatePKCE();
// Store verifier in memory (or sessionStorage for SPA)
sessionStorage.setItem('pkce_verifier', codeVerifier);
// Generate state for CSRF protection
const state = btoa(Math.random().toString()).substring(0, 40);
sessionStorage.setItem('pkce_state', state);
// Redirect to authorization endpoint
const authUrl = new URL(`https://login.${GENESYS_REGION}/oauth/authorize`);
authUrl.searchParams.append('client_id', CLIENT_ID);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
authUrl.searchParams.append('scope', SCOPES);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
authUrl.searchParams.append('state', state);
window.location.href = authUrl.toString();
}
// Step 4: Handle callback
async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
// Check for errors
if (error) {
console.error('Authorization error:', error);
return false;
}
// Verify state (CSRF protection)
const storedState = sessionStorage.getItem('pkce_state');
if (state !== storedState) {
console.error('State mismatch - CSRF attack detected');
return false;
}
// Retrieve code_verifier from memory
const codeVerifier = sessionStorage.getItem('pkce_verifier');
if (!codeVerifier) {
console.error('Code verifier not found');
return false;
}
// Step 5: Exchange code for token
try {
const response = await fetch(`https://login.${GENESYS_REGION}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
code_verifier: codeVerifier // PKCE proof
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const { access_token } = await response.json();
// Step 7: Store token and use it
sessionStorage.setItem('access_token', access_token);
// Clean up PKCE values
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('pkce_state');
console.log('Login successful');
return true;
} catch (error) {
console.error('Token exchange error:', error);
return false;
}
}
// Use access token
async function callAPI(endpoint) {
const token = sessionStorage.getItem('access_token');
if (!token) {
console.error('No access token found');
return null;
}
try {
const response = await fetch(`https://api.${GENESYS_REGION}/api/v2${endpoint}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.status === 401) {
console.error('Token expired, user must log in again');
login();
return null;
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
return null;
}
}
// Example: Login button click handler
document.getElementById('loginBtn').addEventListener('click', login);
// Example: Handle callback on return from auth server
if (window.location.pathname === '/callback') {
handleCallback().then(success => {
if (success) {
window.location.href = '/dashboard';
} else {
window.location.href = '/error';
}
});
}
// Example: Call API
async function getConversations() {
const data = await callAPI('/conversations?pageSize=100&pageNumber=1');
console.log('Conversations:', data);
}
Why Migrate from Implicit Grant to PKCE?
Implicit Grant (DEPRECATED):
├─ Status: Deprecated November 2025
├─ New clients: Blocked March 2026
├─ Existing clients: Must migrate by May 2027
├─ Issues: Token in URL, browser history, no protection
└─ Replacement: PKCE
PKCE (RECOMMENDED):
├─ Status: Supported and recommended
├─ Security: Enhanced via proof mechanism
├─ Suitable for: All client types
├─ Implementation: Slightly more complex
└─ Future-proof: Long-term standard
Migration Path:
Step 1: Update OAuth Client
├─ Delete old Implicit Grant client (or keep if needed)
├─ Create new Authorization Code + PKCE client
└─ Note new client_id
Step 2: Update Application Code
├─ Implement PKCE proof generation
├─ Add code_challenge to authorization
├─ Include code_verifier in token exchange
└─ Remove implicit grant references
Step 3: Test Thoroughly
├─ Test login flow end-to-end
├─ Test token exchange
├─ Test API calls
└─ Verify error handling
Step 4: Deploy
├─ Update production code
├─ Verify working in production
├─ Monitor for errors
└─ Keep old client available briefly
Step 5: Cleanup
├─ Remove old Implicit Grant client
├─ Update documentation
├─ Notify users if applicable
└─ Archive old implementation
Timeline:
├─ Now (March 2026): Implement PKCE
├─ Before May 2027: Migration required
├─ May 2027: Implicit Grant stopped working
└─ Plan ahead to avoid outages!
PKCE vs Implicit Grant
Security Comparison:
Token Exposure:
├─ Implicit: Token in URL fragment (visible)
├─ PKCE: Token in body, 200 response (hidden)
└─ PKCE wins: Less exposed
Token Lifetime:
├─ Implicit: No refresh, static lifetime
├─ PKCE: Can have refresh tokens
└─ PKCE wins: Better UX
Browser History:
├─ Implicit: Token in URL history (risk)
├─ PKCE: No URL tokens (safe)
└─ PKCE wins: No history exposure
Code Interception:
├─ Implicit: Not applicable (no code)
├─ PKCE: Protected by proof (secure)
└─ PKCE wins: Protected exchange
CSRF Protection:
├─ Implicit: State parameter only
├─ PKCE: State + cryptographic proof
└─ PKCE wins: Multiple protections
Standards Alignment:
├─ Implicit: Deprecated OAuth 2.0
├─ PKCE: Modern OAuth 2.0 best practice
└─ PKCE wins: Future-proof
Complexity:
├─ Implicit: Simple (but insecure)
├─ PKCE: Slightly more complex (much more secure)
└─ Trade-off: Worth the extra complexity
Security Checklist for PKCE
PKCE Implementation Security:
□ Generate cryptographically secure code_verifier
├─ Use crypto.getRandomValues() (not Math.random)
├─ 64 bytes minimum (not shorter)
└─ Different every request
□ Compute SHA256 hash of verifier
├─ Use crypto.subtle.digest()
├─ Encode to BASE64URL format
└─ Do not use plain method
□ Store code_verifier securely
├─ Memory only (not localStorage)
├─ Clear after token exchange
├─ Not persisted
└─ Lost on page reload
□ Use S256 method always
├─ Never use "plain" method
├─ Only S256 recommended
└─ Specify in authorization
□ Include state parameter
├─ Separate CSRF protection
├─ Random and unpredictable
├─ Verify in callback
└─ Different from verifier
□ Validate SSL certificates
├─ HTTPS always
├─ Check cert validity
└─ Reject self-signed
□ Do not expose secrets
├─ code_verifier: Memory only
├─ access_token: SessionStorage or memory
├─ Never in localStorage
└─ Clean up after logout
□ Handle errors gracefully
├─ Catch network errors
├─ Retry on failure
├─ Don't expose verifier in errors
└─ Log safely
Key Takeaways: Chapter 4
- Enhanced Security - Cryptographic proof prevents authorization code interception
- No Client Secret - Suitable for public clients (SPAs, mobile, desktop)
- Proof Mechanism - Verifier prevents code theft (cannot reverse-engineer from hash)
- RFC 7636 Standard - Modern OAuth 2.0 best practice
- Implicit Replacement - Use PKCE instead of deprecated Implicit Grant
- Migration Deadline - May 2027 cutoff for Implicit Grant
- Slightly Complex - More code than Implicit, but much more secure
- Future-Proof - Long-term standard, recommended by OAuth 2.0 experts
Interview Prep: PKCE
| Question | Answer |
|---|---|
| What is PKCE? | Proof Key for Code Exchange - enhanced OAuth Code grant security |
| Why PKCE needed? | Prevents authorization code interception attacks |
| code_verifier? | Random string (43-128 chars) generated by client, never sent over network |
| code_challenge? | SHA256(code_verifier), BASE64URL encoded, sent to auth server |
| How prevent intercept? | Only original code_verifier can exchange the code, attacker lacks verifier |
| Why not reverse? | SHA256 is one-way hash function, cannot derive verifier from challenge |
| S256 vs plain? | S256 (SHA256) is secure, plain is deprecated, always use S256 |
| When use PKCE? | Public clients (SPAs, mobile, desktop) that cannot store client_secret |
| Migration deadline? | May 2027 for Implicit Grant existing clients |
| State parameter? | Separate CSRF protection, still needed with PKCE |
Document Version
Chapter: 4 of 8
Last Updated: March 2026
Status: Current with RFC 7636
Scope: PKCE Flow, Security, Implementation, Migration from Implicit
No Comments