Skip to main content

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

QuestionAnswer
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