Skip to main content

Authorization Code Grant

Overview

The Authorization Code Grant is the most secure OAuth 2.0 grant type and is the recommended approach for web applications with backend servers and desktop applications with server components. It implements a two-step process where the user authenticates separately from the token exchange, ensuring the client_secret is never exposed to the browser or client application.


Use Cases

Perfect For:

Web Applications:
├─ Server-side API requests (ASP.NET, PHP, Node.js, Python)
├─ User login flows
├─ Backend-to-Genesys integration
└─ Sensitive data handling

Desktop Applications:
├─ Desktop clients with backend server
├─ Thin client architecture
├─ Server-side token management
└─ Secure credential storage

Mobile Applications:
├─ Mobile app + backend server pattern
├─ Backend handles OAuth exchange
├─ App never sees client_secret
└─ Server-side token refresh

NOT Ideal For:
├─ Pure browser JavaScript (no backend)
├─ Serverless functions (no client_secret storage)
├─ Mobile apps without backend
└─ Use PKCE for these cases instead

Complete Authorization Code Flow

Step 1: User Initiates Login

User clicks "Login with Genesys Cloud" button in your application

Your application redirects 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+users:readonly
  &state=random_state_string_abc123

Parameters:

client_id (required):
├─ Public identifier of your application
├─ Displayed during client creation
├─ Example: "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p"
└─ Used to identify which app is requesting access

response_type (required):
├─ Must be "code" for Authorization Code Grant
├─ Tells authorization server to return code, not token
└─ Security boundary between user auth and backend exchange

redirect_uri (required):
├─ Where user is redirected after authorization
├─ Must be EXACTLY as registered in OAuth client
├─ Must use HTTPS (not HTTP)
├─ Example: "https://yourapp.com/callback"
├─ Can include path and query parameters
└─ Must be registered in Genesys Cloud admin UI

scope (required):
├─ Space-separated list of permissions needed
├─ User sees these during authorization
├─ Example: "conversations:readonly users:readonly"
├─ Request MINIMUM scopes needed
└─ User must grant all scopes (all-or-nothing)

state (highly recommended):
├─ Random string generated by your app
├─ Prevents CSRF (Cross-Site Request Forgery) attacks
├─ Your app stores this in session
├─ Your app verifies it in callback
├─ Must be unpredictable (use crypto random)
└─ Example: Generate 32 random characters

Step 2: User Authenticates

User sees Genesys Cloud login screen

User enters:
├─ Email/username
├─ Password
└─ (Optional) Multi-factor authentication code

Genesys Cloud validates credentials

If invalid:
├─ Show error message
└─ User can retry

If valid:
├─ Genesys Cloud displays permission screen
├─ Shows app name and requested scopes
└─ User must grant permission

Step 3: Authorization Server Redirects

After user grants permission, Genesys Cloud redirects browser to your callback URI:

https://yourapp.com/callback
  ?code=AUTH_CODE_12345abcde67890
  &state=random_state_string_abc123

Parameters in Callback:

code (provided):
├─ Short-lived authorization code
├─ Valid for ~10 minutes only
├─ Single-use only (cannot reuse)
├─ Contains user and scope information
├─ Example: Long alphanumeric string
└─ NOT an access token (yet)

state (provided):
├─ Echo of state parameter from Step 1
├─ Your app must verify this matches
├─ If doesn't match: REJECT (CSRF attack)
├─ If matches: Continue with Step 4
└─ Critical security check

Error Response:
If user denies permission:

https://yourapp.com/callback
  ?error=access_denied
  &error_description=User+denied+permission
  &state=random_state_string_abc123

Handle Error:
├─ Check for "error" parameter first
├─ Don't attempt to exchange code
├─ Show user-friendly error message
└─ Offer to try again

Step 4: Backend Exchanges Code for Token

Your backend server (NOT browser) makes this request:

POST https://login.mypurecloud.com/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic BASE64(client_id:client_secret)

grant_type=authorization_code
&code=AUTH_CODE_12345abcde67890
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Request Details:

Authorization Header:
├─ Format: Basic BASE64(client_id:client_secret)
├─ Example: "Basic YWJjMTIzOmRlZjQ1Ng=="
├─ ALWAYS use HTTPS (not HTTP)
├─ Never expose client_secret in URL
└─ Critical for security

Body Parameters:

grant_type (required):
├─ Must be "authorization_code"
└─ Identifies which grant type you're using

code (required):
├─ Authorization code from Step 3
├─ Only valid for ~10 minutes
├─ Can only be exchanged once
├─ If reused: Server rejects with error
└─ Contains scope and user information

redirect_uri (required):
├─ Must EXACTLY match redirect_uri from Step 1
├─ Must match registered URI in OAuth client
├─ Server verifies to prevent code injection
└─ Must be HTTPS

client_id (required):
├─ Your application's public identifier
└─ Identifies which application is requesting

client_secret (required):
├─ Your application's secret
├─ Server-side only (NEVER expose)
├─ Used to prove application identity
├─ Sent in Authorization header (not URL)
└─ Should be rotated monthly

Security Note:
├─ This is server-to-server communication
├─ Client_secret is exposed only between your server and Genesys
├─ HTTPS encryption required
├─ User's browser is NOT involved
└─ User cannot see client_secret

Step 5: Receive Access & Refresh Tokens

Genesys Cloud responds with tokens:

HTTP 200 OK
Content-Type: application/json

{
  "access_token": "abc123xyz789...",
  "token_type": "bearer",
  "expires_in": 86400,
  "refresh_token": "refresh_xyz789...",
  "scope": "conversations:readonly users:readonly"
}

Response Fields:

access_token:
├─ Short-lived token (1 hour by default)
├─ Use to call Genesys Cloud APIs
├─ Include in Authorization header
├─ Example: "Bearer abc123xyz789..."
└─ Expires automatically

token_type:
├─ Always "bearer" for Genesys Cloud
├─ Indicates HTTP Bearer token format
└─ Use in header: "Authorization: Bearer {token}"

expires_in:
├─ Token lifetime in seconds
├─ Default: 3600 (1 hour)
├─ Configurable: 300-172,800 seconds
├─ Client should refresh before expiration
└─ Example: 86400 = 24 hours

refresh_token:
├─ Long-lived token (30 days default)
├─ Used to get new access token
├─ Can be up to 450 days (SCIM)
├─ Never expires unless revoked
├─ Must be stored securely
└─ Should not be exposed in browser

scope:
├─ Actual scopes granted by user
├─ May differ from requested scopes
├─ User can deny some scopes
└─ Provided for your reference

Error Response Example:
If code is invalid/expired:

HTTP 400 Bad Request
{
  "error": "invalid_grant",
  "error_description": "Authorization code expired"
}

Common Error Codes:
├─ invalid_grant: Code invalid, expired, or reused
├─ invalid_client: client_id/secret invalid
├─ invalid_request: Missing or malformed parameter
├─ server_error: Genesys Cloud error (retry)
└─ See Genesys documentation for full list

Step 6: Use Access Token

Your backend now has access token

Use to call Genesys Cloud APIs:

GET /api/v2/users/me
Host: api.mypurecloud.com
Authorization: Bearer abc123xyz789...
Content-Type: application/json

Example Response:
HTTP 200 OK
{
  "id": "user-123",
  "email": "[email protected]",
  "name": "John Doe",
  "active": true,
  "state": "active"
}

Token Usage Rules:
├─ Include in Authorization header
├─ Format: "Bearer {access_token}"
├─ Send with every API request
├─ HTTPS connection required
├─ No other authentication needed
├─ Token proves user authorized the app
└─ Genesys validates on each request

What Token Proves:
├─ User authenticated with Genesys Cloud
├─ User granted app permission
├─ User agreed to requested scopes
├─ User's identity verified
└─ Token came from real user (not forgery)

Step 7: Handle Token Expiration

After 1 hour (or configured duration):

Access token expires automatically

Your app makes request with expired token:

GET /api/v2/conversations
Authorization: Bearer abc123xyz789... (expired)

Genesys Cloud responds:

HTTP 401 Unauthorized
{
  "error": "invalid_token",
  "error_description": "Access token expired"
}

How to Handle 401:

1. Detect 401 response
2. Check if token expired
3. Use refresh_token to get new access_token
4. Retry original request with new token

Best Practice:
├─ Refresh token 5 minutes BEFORE expiration
├─ Don't wait for 401 error
├─ Proactive refresh is more efficient
└─ Monitor token expiration timestamps

Step 8: Refresh Access Token

When token expires or about to expire:

POST https://login.mypurecloud.com/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic BASE64(client_id:client_secret)

grant_type=refresh_token
&refresh_token=refresh_xyz789...
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Parameters:

grant_type (required):
├─ Must be "refresh_token"
└─ Different from initial "authorization_code"

refresh_token (required):
├─ Long-lived token from Step 5
├─ Never expires unless revoked
├─ Can be reused multiple times
└─ Must be stored securely

client_id & client_secret (required):
├─ Same as Step 4
├─ Authorization header or body
└─ Identifies your application

Response:

HTTP 200 OK
{
  "access_token": "new_token_abc456...",
  "token_type": "bearer",
  "expires_in": 86400,
  "refresh_token": "new_refresh_token_xyz890..."
}

New Tokens Provided:
├─ New access_token (1 hour lifetime)
├─ Same token_type (bearer)
├─ Same expires_in duration
├─ New refresh_token (optional, but provided)
└─ Can use immediately

Error Handling:

If refresh token invalid/expired:
├─ User must re-authenticate (start from Step 1)
└─ Cannot recover without new user authorization

If any error:
├─ Redirect user to login again
├─ Start fresh authorization flow
└─ Don't keep retrying with bad refresh_token

Refresh Token Rotation:
├─ Genesys provides new refresh_token on each refresh
├─ Old token still works briefly
├─ Provides security rotation
├─ Discard old token after refresh
└─ Never try to reuse old tokens

Implementation Pattern

High-Level Application Flow:

User Visit App
  ↓
User Not Logged In?
  ↓
Click "Login with Genesys"
  ↓
[Step 1-3: Browser Redirect Flow]
Authorization Server Redirects to Callback
  ↓
[Step 4-5: Server-to-Server Token Exchange]
Backend Exchanges Code for Tokens
  ↓
Backend Stores Tokens in Session/Database
  ↓
User Logged In to App
  ↓
User Makes API Call to App
  ↓
App Backend Uses Access Token
  ↓
Call Genesys Cloud API with Token
  ↓
Get Response from Genesys
  ↓
Return Result to User
  ↓
...Time Passes...
  ↓
Token Expires (1 hour)
  ↓
User Makes Another API Call
  ↓
Backend Detects Expired Token
  ↓
[Step 8: Refresh Token Exchange]
Backend Refreshes Token
  ↓
Continue with New Token
  ↓
Repeat as needed

Complete Node.js Example

const express = require('express');
const axios = require('axios');
const session = require('express-session');
const crypto = require('crypto');

const app = express();

// Configuration
const CLIENT_ID = process.env.GENESYS_CLIENT_ID;
const CLIENT_SECRET = process.env.GENESYS_CLIENT_SECRET;
const REDIRECT_URI = 'https://yourapp.com/callback';
const SCOPES = 'conversations:readonly users:readonly';
const GENESYS_REGION = process.env.GENESYS_REGION || 'mypurecloud.com';

// Session middleware
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: true
}));

// Step 1: User clicks login button
app.get('/login', (req, res) => {
  // Generate state for CSRF protection
  const state = crypto.randomBytes(32).toString('hex');
  req.session.state = state;
  
  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('state', state);
  
  res.redirect(authUrl.toString());
});

// Step 3-4: Handle callback and exchange code
app.get('/callback', async (req, res) => {
  const { code, state, error } = req.query;
  
  // Check for errors
  if (error) {
    console.error('Authorization error:', error);
    return res.status(400).send('Authorization denied');
  }
  
  // Verify state (CSRF protection)
  if (state !== req.session.state) {
    return res.status(403).send('Invalid state parameter');
  }
  
  try {
    // Step 4: Exchange code for tokens
    const response = await axios.post(
      `https://login.${GENESYS_REGION}/oauth/token`,
      {
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: REDIRECT_URI,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET
      }
    );
    
    // Step 5: Store tokens
    req.session.accessToken = response.data.access_token;
    req.session.refreshToken = response.data.refresh_token;
    req.session.expiresAt = Date.now() + (response.data.expires_in * 1000);
    
    res.redirect('/dashboard');
  } catch (error) {
    console.error('Token exchange error:', error.message);
    res.status(500).send('Token exchange failed');
  }
});

// Helper function to ensure valid access token
async function ensureAccessToken(req) {
  // Check if token needs refresh (within 5 minutes of expiration)
  if (req.session.expiresAt - Date.now() < 5 * 60 * 1000) {
    try {
      // Step 8: Refresh token
      const response = await axios.post(
        `https://login.${GENESYS_REGION}/oauth/token`,
        {
          grant_type: 'refresh_token',
          refresh_token: req.session.refreshToken,
          client_id: CLIENT_ID,
          client_secret: CLIENT_SECRET
        }
      );
      
      // Update tokens
      req.session.accessToken = response.data.access_token;
      req.session.refreshToken = response.data.refresh_token;
      req.session.expiresAt = Date.now() + (response.data.expires_in * 1000);
    } catch (error) {
      console.error('Token refresh failed:', error.message);
      throw new Error('Failed to refresh token');
    }
  }
  
  return req.session.accessToken;
}

// Step 6: Use access token
app.get('/api/conversations', async (req, res) => {
  try {
    const accessToken = await ensureAccessToken(req);
    
    // Step 6: Use token to call Genesys API
    const response = await axios.get(
      `https://api.${GENESYS_REGION}/api/v2/conversations`,
      {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    res.json(response.data);
  } catch (error) {
    console.error('API call failed:', error.message);
    res.status(500).send('Failed to fetch conversations');
  }
});

// Logout
app.get('/logout', (req, res) => {
  // Optional: Revoke token on Genesys side
  // DELETE /oauth/sessions/me with access token
  
  req.session.destroy((err) => {
    if (err) console.error('Session destroy error:', err);
    res.redirect('/');
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Security Checklist

Authorization Code Grant Security:

□ Use HTTPS everywhere (never HTTP)
□ Validate SSL/TLS certificates
□ Generate unpredictable state parameter
□ Verify state in callback (CSRF protection)
□ Store client_secret securely (vault/environment)
□ Never expose client_secret in browser
□ Never commit secrets to git
□ Exchange code on backend (never frontend)
□ Validate redirect_uri matches registered
□ Store tokens securely server-side
□ Refresh token before expiration
□ Implement 401 handling (refresh + retry)
□ Never log token values
□ Implement audit logging (no tokens)
□ Rotate secrets monthly
□ Revoke tokens on logout
□ Implement HTTPS redirect
□ Validate SSL certificate
□ Handle errors gracefully
□ Implement rate limiting
□ Monitor for suspicious activity

Comparison with Other Grant Types

Authorization Code vs:

Authorization Code + PKCE:
├─ Both equally secure for web apps
├─ PKCE added for public clients (SPAs, mobile)
├─ Both send user to browser for authentication
├─ PKCE adds code_verifier to prevent interception
└─ Code is better for web apps, PKCE for public

Client Credentials:
├─ Both are OAuth 2.0 standard grants
├─ Code requires user interaction
├─ Client Credentials: Service-to-service
├─ Code: User-initiated access
├─ Code: User sees permission screen
├─ Client Credentials: No user consent needed
└─ Different use cases

Implicit Grant (DEPRECATED):
├─ Both result in access token
├─ Code: Two-step (safer)
├─ Implicit: One-step (simpler but risky)
├─ Code: Client_secret protected
├─ Implicit: No secret possible
├─ Code: Token in backend
├─ Implicit: Token in URL/browser
├─ Code is clearly better (implicit deprecated)
└─ Never use Implicit for new apps

Common Errors & Solutions

Error: "invalid_grant"
├─ Cause: Authorization code invalid/expired/reused
├─ Solution: User must re-authenticate
├─ Timeline: Code valid ~10 minutes
└─ Prevention: Use code immediately

Error: "invalid_client"
├─ Cause: client_id or client_secret invalid
├─ Solution: Verify credentials in OAuth client
├─ Check: Admin → Integrations → OAuth
└─ Action: Regenerate credentials if needed

Error: "redirect_uri_mismatch"
├─ Cause: Callback URI doesn't match registered
├─ Solution: Ensure EXACT match (case-sensitive)
├─ Check: Admin → Integrations → OAuth
└─ Include: Protocol, domain, path, query params

Error: "invalid_scope"
├─ Cause: Requested scope doesn't exist
├─ Solution: Verify scope name is correct
└─ Use: Format "resource:action" or "resource:action:scope"

Error: "access_denied"
├─ Cause: User denied permission
├─ Solution: Show friendly message, offer retry
└─ Expected: Normal user action

Error: "server_error"
├─ Cause: Genesys Cloud temporary error
├─ Solution: Retry with exponential backoff
└─ Contact: Support if persists

Key Takeaways: Chapter 2

  • Most Secure Grant - Recommended for web and desktop applications
  • Two-Step Process - Separates user authentication from token exchange
  • Client Secret Protected - Never exposed to browser or client application
  • Short-Lived Tokens - Access tokens expire (1 hour by default)
  • Refresh Capability - Refresh tokens enable long-lived access without re-authentication
  • CSRF Protected - State parameter prevents cross-site attacks
  • Server-Side Exchange - Code exchanged on backend (never browser)
  • Standard Approach - RFC 6749 compliant, industry best practice

Interview Prep: Authorization Code Grant

Question Answer
When use Auth Code? Web/desktop apps with backend server
Two-step process? User auth separate from token exchange
Authorization code purpose? Single-use code exchanged for access token
State parameter? CSRF protection (random string verified in callback)
Why exchange on backend? Keep client_secret secure (never exposed)
Client_secret exposure? Never - only used between server and Genesys
Access token lifetime? 1 hour by default (configurable 300-172,800 sec)
Refresh token lifetime? 30 days default (up to 450 days for SCIM)
401 error handling? Refresh token to get new access token, retry request
Rate limiting? 60 requests/minute per application

Document Version

Chapter: 2 of 8
Last Updated: March 2026
Status: Current with RFC 6749
Scope: Authorization Code Grant Flow, Implementation, Security