How to generate a long-lived API token for a CI/CD pipeline
What You Will Build
- You will build a secure, automated workflow that generates a long-lived, scope-restricted API token for use in CI/CD pipelines, eliminating the need for short-lived OAuth token refresh cycles during build processes.
- This tutorial utilizes the Genesys Cloud CX Admin API (
/api/v2/auth/tokens) and the NICE CXone Identity Provider API (/oauth/token) to demonstrate platform-specific implementations. - The code examples are provided in Python (using
requests) and Bash (usingcurl), which are the most common languages for CI/CD script execution.
Prerequisites
- Genesys Cloud: A Service Account (OAuth Client) with the
admin:apirole or specific scopes required for your pipeline tasks (e.g.,user:read,analytics:read). You must have theclient_idandclient_secret. - NICE CXone: A Service Account (OAuth Client) with the
client_credentialsgrant type enabled. You must have theclient_idandclient_secret. - Runtime Environment: Python 3.8+ with the
requestslibrary installed (pip install requests), or a standard Unix shell withcurl. - Security Context: A secure secrets manager (e.g., GitHub Actions Secrets, Azure Key Vault, AWS Secrets Manager) to store
client_idandclient_secret. Never hardcode these values.
Authentication Setup
The core challenge in CI/CD is that standard OAuth2 client_credentials grants typically return tokens with a short lifespan (e.g., 3600 seconds). If your pipeline job exceeds this duration, or if you need to cache the token for multiple subsequent steps, you face authentication failures.
To solve this, we will use two strategies:
- Genesys Cloud: Use the
admin:apiscope to generate a token that is valid for up to 24 hours (86400 seconds) by explicitly requesting theexpires_inparameter if supported, or by leveraging the default longer-lived tokens available to service accounts with admin privileges. - NICE CXone: Use the
client_credentialsgrant with theoffline_accessscope (if enabled) or rely on the default token lifetime, implementing a lightweight local cache with automatic refresh if the job runs long.
For this tutorial, we focus on the Genesys Cloud approach as it offers a more straightforward “long-lived” token generation for service accounts, which is a common pain point in CI/CD. We will also provide the NICE CXone equivalent for comparison.
Genesys Cloud: Generating a 24-Hour Token
Genesys Cloud service accounts can generate tokens that last up to 24 hours. This is ideal for CI/CD pipelines that may run for several hours.
Step 1: Configure the Service Account
- Log in to the Genesys Cloud Admin portal.
- Navigate to Setup > Security > OAuth Clients.
- Create a new OAuth Client or select an existing service account.
- Ensure the Grant Types include
Client Credentials. - Assign the necessary Scopes. For a CI/CD pipeline that reads analytics and updates user profiles, you might need:
analytics:readuser:readuser:writeadmin:api(Required for generating long-lived tokens in some configurations, though standard service accounts often get 24h tokens by default. If you encounter short-lived tokens, ensure the client hasadmin:apior contact support to adjust the token lifetime policy).
Step 2: Implement the Token Request in Python
The following Python script demonstrates how to request a token from Genesys Cloud. It includes error handling for 400 (Bad Request), 401 (Unauthorized), and 429 (Rate Limit) errors.
import requests
import time
import sys
import os
from typing import Optional, Dict, Any
GENESYS_BASE_URL = "https://api.mypurecloud.com"
GENESYS_TOKEN_URL = f"{GENESYS_BASE_URL}/api/v2/oauth/token"
def get_genesys_token(client_id: str, client_secret: str, grant_type: str = "client_credentials") -> Optional[str]:
"""
Retrieves an OAuth2 token from Genesys Cloud.
Args:
client_id: The OAuth Client ID.
client_secret: The OAuth Client Secret.
grant_type: The OAuth grant type. Defaults to 'client_credentials'.
Returns:
The access token string if successful, None otherwise.
"""
# Prepare the request body
payload = {
"grant_type": grant_type,
"client_id": client_id,
"client_secret": client_secret
}
# Set headers
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
}
try:
# Make the POST request
response = requests.post(
GENESYS_TOKEN_URL,
data=payload,
headers=headers,
timeout=10
)
# Check for successful response
if response.status_code == 200:
token_data = response.json()
access_token = token_data.get("access_token")
expires_in = token_data.get("expires_in", 0)
if access_token:
print(f"Token acquired successfully. Expires in {expires_in} seconds.")
return access_token
else:
print("Error: No access_token found in response.")
return None
else:
# Handle specific error codes
if response.status_code == 400:
print(f"Bad Request: {response.text}")
elif response.status_code == 401:
print("Unauthorized: Check your client_id and client_secret.")
elif response.status_code == 429:
retry_after = response.headers.get("Retry-After", 5)
print(f"Rate Limited. Retrying after {retry_after} seconds...")
time.sleep(int(retry_after))
return get_genesys_token(client_id, client_secret, grant_type)
else:
print(f"Unexpected Error {response.status_code}: {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"Network error occurred: {e}")
return None
# Example Usage
if __name__ == "__main__":
# Load credentials from environment variables
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
if not client_id or not client_secret:
print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")
sys.exit(1)
token = get_genesys_token(client_id, client_secret)
if token:
print(f"Access Token: {token}")
# In a real CI/CD pipeline, you would pass this token to subsequent steps
# or store it in a file for other scripts to consume.
else:
sys.exit(1)
Step 3: Verify Token Lifetime
By default, Genesys Cloud service account tokens often expire in 3600 seconds (1 hour). However, if your OAuth client has the admin:api scope or is configured for long-lived access, the token may last up to 86400 seconds (24 hours). To verify the lifetime, inspect the expires_in field in the JSON response.
If you require a specific long-lived token, you may need to configure the OAuth client in the Admin portal to allow extended lifetimes. Some organizations restrict this for security reasons. If you cannot obtain a 24-hour token, you must implement a token refresh mechanism.
NICE CXone: Generating a Token with Refresh Capability
NICE CXone uses a standard OAuth2 flow. Tokens typically expire in 3600 seconds. For CI/CD, you must handle the refresh. The following Bash script demonstrates how to obtain a token and handle the expiration by checking the expires_in value.
#!/bin/bash
CXONE_BASE_URL="https://api.nicecxone.com"
CXONE_TOKEN_URL="${CXONE_BASE_URL}/oauth/token"
CLIENT_ID="${NICE_CLIENT_ID}"
CLIENT_SECRET="${NICE_CLIENT_SECRET}"
# Function to get token
get_cxone_token() {
local response
response=$(curl -s -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \
"${CXONE_TOKEN_URL}")
local status
status=$(echo "$response" | jq -r '.status // empty')
if [ -z "$status" ]; then
local token
token=$(echo "$response" | jq -r '.access_token')
local expires_in
expires_in=$(echo "$response" | jq -r '.expires_in')
if [ "$token" != "null" ] && [ -n "$token" ]; then
echo "Token acquired. Expires in ${expires_in} seconds."
echo "$token"
else
echo "Error: Failed to acquire token."
echo "$response"
exit 1
fi
else
echo "Error: ${status}"
echo "$response"
exit 1
fi
}
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed."
exit 1
fi
# Get the token
TOKEN=$(get_cxone_token)
if [ -n "$TOKEN" ]; then
echo "Access Token: ${TOKEN}"
# Use the token in subsequent API calls
# Example: curl -H "Authorization: Bearer ${TOKEN}" ...
else
echo "Failed to retrieve token."
exit 1
fi
Implementation
Step 1: Secure Credential Storage in CI/CD
Never hardcode client_id or client_secret in your repository. Use your CI/CD platform’s secret management features.
GitHub Actions Example
name: Genesys Cloud CI/CD Pipeline
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install requests
- name: Generate Token
env:
GENESYS_CLIENT_ID: ${{ secrets.GENESYS_CLIENT_ID }}
GENESYS_CLIENT_SECRET: ${{ secrets.GENESYS_CLIENT_SECRET }}
run: |
python generate_token.py
Azure DevOps Example
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- script: pip install requests
displayName: 'Install dependencies'
- script: python generate_token.py
displayName: 'Generate Token'
env:
GENESYS_CLIENT_ID: $(GenesysClientId)
GENESYS_CLIENT_SECRET: $(GenesysClientSecret)
Step 2: Implement Token Caching for Long-Running Jobs
If your CI/CD job runs longer than the token lifetime (e.g., a complex deployment that takes 2 hours), you must implement token caching. The following Python class demonstrates a simple token cache with automatic refresh.
import requests
import time
import threading
from typing import Optional
class GenesysTokenCache:
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://api.mypurecloud.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
self.token_url = f"{base_url}/api/v2/oauth/token"
self.access_token: Optional[str] = None
self.expiry_time: float = 0
self.lock = threading.Lock()
def get_token(self) -> Optional[str]:
"""
Returns a valid access token. Refreshes if expired or about to expire.
"""
with self.lock:
# Check if token is expired or will expire in the next 60 seconds
if self.access_token and time.time() < self.expiry_time - 60:
return self.access_token
# Refresh the token
return self._refresh_token()
def _refresh_token(self) -> Optional[str]:
"""
Internal method to fetch a new token from Genesys Cloud.
"""
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
}
try:
response = requests.post(
self.token_url,
data=payload,
headers=headers,
timeout=10
)
if response.status_code == 200:
token_data = response.json()
self.access_token = token_data.get("access_token")
self.expiry_time = time.time() + token_data.get("expires_in", 3600)
return self.access_token
else:
print(f"Failed to refresh token: {response.status_code} {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"Network error during token refresh: {e}")
return None
# Usage Example
if __name__ == "__main__":
import os
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
if client_id and client_secret:
cache = GenesysTokenCache(client_id, client_secret)
token = cache.get_token()
if token:
print(f"Current Token: {token[:10]}...")
# Simulate a long-running task
time.sleep(100)
token = cache.get_token()
if token:
print(f"Token after sleep: {token[:10]}...")
Step 3: Using the Token in API Calls
Once you have the token, you must include it in the Authorization header of all subsequent API calls.
import requests
def get_users(token: str, base_url: str = "https://api.mypurecloud.com") -> list:
"""
Fetches a list of users from Genesys Cloud.
"""
url = f"{base_url}/api/v2/users"
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.json().get("entities", [])
else:
print(f"Failed to fetch users: {response.status_code} {response.text}")
return []
# Usage
users = get_users(token)
print(f"Found {len(users)} users.")
Complete Working Example
The following is a complete, copy-pasteable Python script that integrates token generation, caching, and a sample API call. It is designed to run in a CI/CD environment.
import requests
import time
import sys
import os
from typing import Optional, List, Dict, Any
class GenesysCICDClient:
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://api.mypurecloud.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
self.token_url = f"{base_url}/api/v2/oauth/token"
self.access_token: Optional[str] = None
self.expiry_time: float = 0
def _get_token(self) -> Optional[str]:
"""
Retrieves a fresh token from Genesys Cloud.
"""
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
}
try:
response = requests.post(
self.token_url,
data=payload,
headers=headers,
timeout=10
)
if response.status_code == 200:
token_data = response.json()
self.access_token = token_data.get("access_token")
self.expiry_time = time.time() + token_data.get("expires_in", 3600)
return self.access_token
else:
print(f"Error {response.status_code}: {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
return None
def get_valid_token(self) -> Optional[str]:
"""
Returns a valid token, refreshing if necessary.
"""
if self.access_token and time.time() < self.expiry_time - 60:
return self.access_token
return self._get_token()
def get_users(self, page: int = 1, page_size: int = 25) -> Dict[str, Any]:
"""
Fetches a page of users.
"""
token = self.get_valid_token()
if not token:
return {"error": "Failed to get token"}
url = f"{self.base_url}/api/v2/users"
params = {
"page": page,
"pageSize": page_size
}
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
try:
response = requests.get(url, headers=headers, params=params, timeout=10)
if response.status_code == 200:
return response.json()
else:
return {"error": f"HTTP {response.status_code}: {response.text}"}
except requests.exceptions.RequestException as e:
return {"error": str(e)}
if __name__ == "__main__":
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
if not client_id or not client_secret:
print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")
sys.exit(1)
client = GenesysCICDClient(client_id, client_secret)
# Fetch users
users_data = client.get_users()
if "error" in users_data:
print(f"Error: {users_data['error']}")
sys.exit(1)
else:
users = users_data.get("entities", [])
print(f"Successfully fetched {len(users)} users.")
for user in users[:5]: # Print first 5 users
print(f"User: {user.get('name')} ({user.get('email')})")
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The
client_idorclient_secretis incorrect, or the OAuth client is disabled. - Fix: Verify the credentials in your CI/CD secrets manager. Ensure the OAuth client is active in the Genesys Cloud Admin portal.
Error: 403 Forbidden
- Cause: The OAuth client does not have the required scopes for the API endpoint you are calling.
- Fix: Check the scopes assigned to your OAuth client in the Admin portal. Add the necessary scopes (e.g.,
user:read) and regenerate the token.
Error: 429 Too Many Requests
- Cause: You have exceeded the rate limit for the OAuth token endpoint or the API endpoint.
- Fix: Implement exponential backoff in your token refresh logic. The example code above includes a simple retry for 429 errors.
Error: Token Expired Mid-Execution
- Cause: The CI/CD job ran longer than the token’s
expires_invalue. - Fix: Use the
GenesysTokenCacheclass or similar logic to automatically refresh the token before making API calls. Always check theexpiry_timebefore using the token.