How to generate a long-lived API token for a CI/CD pipeline

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 (using curl), which are the most common languages for CI/CD script execution.

Prerequisites

  • Genesys Cloud: A Service Account (OAuth Client) with the admin:api role or specific scopes required for your pipeline tasks (e.g., user:read, analytics:read). You must have the client_id and client_secret.
  • NICE CXone: A Service Account (OAuth Client) with the client_credentials grant type enabled. You must have the client_id and client_secret.
  • Runtime Environment: Python 3.8+ with the requests library installed (pip install requests), or a standard Unix shell with curl.
  • Security Context: A secure secrets manager (e.g., GitHub Actions Secrets, Azure Key Vault, AWS Secrets Manager) to store client_id and client_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:

  1. Genesys Cloud: Use the admin:api scope to generate a token that is valid for up to 24 hours (86400 seconds) by explicitly requesting the expires_in parameter if supported, or by leveraging the default longer-lived tokens available to service accounts with admin privileges.
  2. NICE CXone: Use the client_credentials grant with the offline_access scope (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

  1. Log in to the Genesys Cloud Admin portal.
  2. Navigate to Setup > Security > OAuth Clients.
  3. Create a new OAuth Client or select an existing service account.
  4. Ensure the Grant Types include Client Credentials.
  5. Assign the necessary Scopes. For a CI/CD pipeline that reads analytics and updates user profiles, you might need:
    • analytics:read
    • user:read
    • user:write
    • admin: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 has admin:api or 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_id or client_secret is 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_in value.
  • Fix: Use the GenesysTokenCache class or similar logic to automatically refresh the token before making API calls. Always check the expiry_time before using the token.

Official References