Authorization Code Grant
Overview
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
No Comments