List All OAuth Clients and Audit Scope Assignments

List All OAuth Clients and Audit Scope Assignments

What You Will Build

  • A script that retrieves every OAuth client in a Genesys Cloud organization and prints a table of their names, types, and assigned scopes.
  • This uses the Genesys Cloud Platform API v2 (/api/v2/oauth/clients) and the Python SDK PureCloudPlatformClientV2.
  • The tutorial covers Python 3.9+ with the requests library for direct HTTP access and the official Genesys Cloud Python SDK for SDK-based access.

Prerequisites

  • OAuth Client Type: Machine-to-Machine (M2M) Client.
  • Required Scopes: oauth:client:read is mandatory for listing clients. If you need to inspect detailed configuration, oauth:client:config:read may be useful, but for scope auditing, oauth:client:read suffices.
  • SDK Version: genesys-cloud-sdk-python v100+ (or compatible v90+).
  • Language/Runtime: Python 3.9 or higher.
  • External Dependencies:
    • pip install genesys-cloud-sdk-python
    • pip install requests (if using the raw HTTP approach)

Authentication Setup

To call the Genesys Cloud API, you need a valid Bearer token. For M2M clients, this is obtained via the client_credentials grant type.

Step 1: Generate the Access Token

You must have the Client ID and Client Secret from your M2M client.

import requests
import json
from typing import Optional

# Configuration
GENESYS_CLOUD_REGION = "mypurecloud.com"  # Replace with your region: mypurecloud.com, euw1.pure.cloud, etc.
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"

def get_access_token() -> str:
    """
    Retrieves an OAuth 2.0 access token using the client_credentials grant.
    """
    url = f"https://{GENESYS_CLOUD_REGION}/oauth/token"
    
    # The body must be application/x-www-form-urlencoded for the token endpoint
    data = {
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    }
    
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    try:
        response = requests.post(url, data=data, headers=headers, timeout=10)
        response.raise_for_status()
        token_json = response.json()
        return token_json.get("access_token")
    except requests.exceptions.HTTPError as e:
        print(f"Failed to get token: {e}")
        print(f"Response: {response.text}")
        raise
    except requests.exceptions.RequestException as e:
        print(f"Network error: {e}")
        raise

if __name__ == "__main__":
    token = get_access_token()
    print(f"Token acquired: {token[:20]}...")

Implementation

Step 1: List All OAuth Clients

The endpoint /api/v2/oauth/clients returns a paginated list of OAuth clients. The response object contains a entities array. Each entity represents one OAuth client.

Required Scope: oauth:client:read

Raw HTTP Approach

import requests
import sys

# Assume 'token' is already obtained from the previous step
TOKEN = get_access_token()
HEADERS = {
    "Authorization": f"Bearer {TOKEN}",
    "Accept": "application/json",
    "Content-Type": "application/json"
}

def list_oauth_clients_raw(page: int = 1, page_size: int = 25) -> dict:
    """
    Fetches a page of OAuth clients using raw HTTP requests.
    """
    url = f"https://{GENESYS_CLOUD_REGION}/api/v2/oauth/clients"
    params = {
        "page": page,
        "pageSize": page_size,
        # Optional: filter by client type (e.g., "M2M", "Web", "Mobile")
        # "clientType": "M2M" 
    }
    
    try:
        response = requests.get(url, headers=HEADERS, params=params, timeout=15)
        
        # Handle 401 Unauthorized
        if response.status_code == 401:
            print("Error 401: Invalid or expired token. Ensure the client has 'oauth:client:read' scope.")
            sys.exit(1)
            
        # Handle 403 Forbidden
        if response.status_code == 403:
            print("Error 403: Forbidden. The client lacks the 'oauth:client:read' scope.")
            sys.exit(1)
            
        response.raise_for_status()
        return response.json()
        
    except requests.exceptions.JSONDecodeError:
        print("Error: Response was not valid JSON.")
        print(response.text)
        raise
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        raise

if __name__ == "__main__":
    data = list_oauth_clients_raw()
    print(json.dumps(data, indent=2))

SDK Approach

The SDK handles pagination and serialization automatically.

from genesyscloud import PlatformClient
from genesyscloud.platform_client import PlatformClient

# Initialize the Platform Client
# Note: The SDK can handle token refresh if you pass the client id/secret directly,
# but for this tutorial, we will use the token method for explicit control.

def list_oauth_clients_sdk(platform_client: PlatformClient) -> list:
    """
    Uses the Genesys Cloud Python SDK to list all OAuth clients.
    """
    from genesyscloud.oauth_client import OauthClientApi
    
    oauth_api = OauthClientApi(platform_client)
    
    try:
        # The SDK method returns a response object containing the entities
        response = oauth_api.post_oauth_clients(
            page_size=100,  # Max page size is usually 100
            page=1
        )
        
        return response.entities if hasattr(response, 'entities') else []
        
    except Exception as e:
        print(f"SDK Error: {e}")
        raise

# To use this, you would initialize PlatformClient with your credentials:
# platform_client = PlatformClient(CLIENT_ID, CLIENT_SECRET, GENESYS_CLOUD_REGION)

Step 2: Process Pagination and Extract Scopes

The API is paginated. You must loop through pages until the nextPage is null or empty. Each client object has a scopes field, which is an array of strings.

Complete Pagination Logic (Raw HTTP)

def get_all_oauth_clients() -> list:
    """
    Iterates through all pages to retrieve every OAuth client in the org.
    """
    all_clients = []
    page = 1
    page_size = 100
    
    while True:
        data = list_oauth_clients_raw(page=page, page_size=page_size)
        
        if not data.get("entities"):
            break
            
        all_clients.extend(data["entities"])
        
        # Check if there are more pages
        if not data.get("nextPage"):
            break
            
        page += 1
        
        # Optional: Add a small delay to be respectful of rate limits
        # import time
        # time.sleep(0.5)
        
    return all_clients

Step 3: Analyze Scope Assignments

Once you have the list of clients, you can analyze the scopes. Common security audits look for:

  1. Clients with admin:org:read or admin:org:write that are not M2M.
  2. Clients with overly broad scopes like * (if supported/custom) or specific high-privilege scopes.
def audit_scopes(clients: list) -> None:
    """
    Prints a formatted table of clients and their scopes.
    Highlights clients with sensitive scopes.
    """
    sensitive_scopes = [
        "admin:org:read",
        "admin:org:write",
        "user:profile:write",
        "analytics:report:read",
        "conversation:read"
    ]
    
    print(f"{'Client ID':<20} | {'Name':<20} | {'Type':<10} | {'Scope Count':<10} | {'Sensitive Scopes'}")
    print("-" * 100)
    
    for client in clients:
        client_id = client.get("id", "N/A")
        name = client.get("name", "Unnamed")
        client_type = client.get("clientType", "N/A")
        scopes = client.get("scopes", [])
        scope_count = len(scopes)
        
        # Check for sensitive scopes
        found_sensitive = [s for s in scopes if s in sensitive_scopes]
        sensitive_str = ", ".join(found_sensitive[:3]) + ("..." if len(found_sensitive) > 3 else "") if found_sensitive else "None"
        
        # Truncate long names
        if len(name) > 18:
            name = name[:15] + "..."
            
        print(f"{client_id:<20} | {name:<20} | {client_type:<10} | {scope_count:<10} | {sensitive_str}")

Complete Working Example

This is a single, runnable Python script that authenticates, fetches all OAuth clients, and audits their scopes.

import requests
import sys
import time
from typing import List, Dict, Any

# ================= Configuration =================
GENESYS_CLOUD_REGION = "mypurecloud.com"  # UPDATE THIS
CLIENT_ID = "your_client_id"             # UPDATE THIS
CLIENT_SECRET = "your_client_secret"     # UPDATE THIS
# =================================================

def get_access_token() -> str:
    """
    Retrieves an OAuth 2.0 access token using the client_credentials grant.
    """
    url = f"https://{GENESYS_CLOUD_REGION}/oauth/token"
    data = {
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    }
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    
    try:
        response = requests.post(url, data=data, headers=headers, timeout=10)
        response.raise_for_status()
        return response.json().get("access_token")
    except requests.exceptions.RequestException as e:
        print(f"Error acquiring token: {e}")
        if hasattr(e, 'response') and e.response is not None:
            print(f"Response: {e.response.text}")
        sys.exit(1)

def fetch_page(page: int, page_size: int, headers: Dict[str, str]) -> Dict[str, Any]:
    """
    Fetches a single page of OAuth clients.
    Implements basic retry logic for 429 Too Many Requests.
    """
    url = f"https://{GENESYS_CLOUD_REGION}/api/v2/oauth/clients"
    params = {"page": page, "pageSize": page_size}
    
    max_retries = 3
    for attempt in range(max_retries):
        try:
            response = requests.get(url, headers=headers, params=params, timeout=15)
            
            if response.status_code == 429:
                # Rate limit hit
                retry_after = int(response.headers.get("Retry-After", 2))
                print(f"Rate limited. Retrying in {retry_after} seconds...")
                time.sleep(retry_after)
                continue
                
            response.raise_for_status()
            return response.json()
            
        except requests.exceptions.RequestException as e:
            print(f"Request failed (attempt {attempt+1}): {e}")
            if attempt == max_retries - 1:
                raise
            time.sleep(1)
            
    raise Exception("Max retries exceeded")

def get_all_oauth_clients(token: str) -> List[Dict[str, Any]]:
    """
    Retrieves all OAuth clients by handling pagination.
    """
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json"
    }
    
    all_clients = []
    page = 1
    page_size = 100
    
    print("Fetching OAuth clients...")
    
    while True:
        try:
            data = fetch_page(page, page_size, headers)
        except Exception as e:
            print(f"Failed to fetch page {page}: {e}")
            break
            
        entities = data.get("entities", [])
        if not entities:
            break
            
        all_clients.extend(entities)
        print(f"Fetched {len(entities)} clients on page {page}. Total so far: {len(all_clients)}")
        
        # Check for next page
        if not data.get("nextPage"):
            break
            
        page += 1
        # Be polite to the API
        time.sleep(0.2)
        
    return all_clients

def audit_and_report(clients: List[Dict[str, Any]]) -> None:
    """
    Analyzes scopes and prints a report.
    """
    # Define scopes that require attention
    high_risk_scopes = {
        "admin:org:read",
        "admin:org:write",
        "admin:user:read",
        "admin:user:write",
        "conversation:read",
        "conversation:write",
        "analytics:report:read"
    }
    
    print("\n" + "="*80)
    print("OAUTH CLIENT SCOPE AUDIT REPORT")
    print("="*80)
    
    for client in clients:
        client_id = client.get("id", "Unknown")
        name = client.get("name", "Unnamed Client")
        client_type = client.get("clientType", "Unknown")
        scopes = client.get("scopes", [])
        
        # Identify high-risk scopes assigned to this client
        risky_scopes = [s for s in scopes if s in high_risk_scopes]
        
        # Display logic
        status = " [HIGH RISK]" if risky_scopes else " [OK]"
        
        print(f"\nClient: {name} ({client_id})")
        print(f"Type: {client_type} | Status: {status}")
        print(f"Total Scopes: {len(scopes)}")
        
        if risky_scopes:
            print(f"High-Risk Scopes: {', '.join(risky_scopes)}")
        else:
            print(f"Scopes: {', '.join(scopes[:5])}{'...' if len(scopes) > 5 else ''}")
            
        print("-" * 40)

def main():
    """
    Main execution flow.
    """
    try:
        # 1. Authenticate
        print("Authenticating...")
        token = get_access_token()
        
        # 2. Fetch Data
        clients = get_all_oauth_clients(token)
        
        if not clients:
            print("No OAuth clients found in the organization.")
            return
            
        print(f"\nTotal clients retrieved: {len(clients)}")
        
        # 3. Audit
        audit_and_report(clients)
        
    except KeyboardInterrupt:
        print("\nProcess interrupted by user.")
        sys.exit(0)
    except Exception as e:
        print(f"\nFatal error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 403 Forbidden

  • Cause: The M2M client used for authentication does not have the oauth:client:read scope assigned.
  • Fix:
    1. Log in to Genesys Cloud Admin.
    2. Navigate to Setup > Integrations > OAuth Clients.
    3. Select your M2M client.
    4. In the Scopes tab, ensure oauth:client:read is checked.
    5. Save the changes. Note that scope changes may take up to 1 minute to propagate.

Error: 401 Unauthorized

  • Cause: The access token is invalid, expired, or the Client ID/Secret is incorrect.
  • Fix:
    1. Verify the CLIENT_ID and CLIENT_SECRET match the values in the Admin console exactly.
    2. Ensure the token request returns a 200 OK. If it returns 401, the credentials are wrong.
    3. If the token was generated previously, check if it has expired (default TTL is usually 1 hour). Regenerate the token.

Error: 429 Too Many Requests

  • Cause: You are hitting the rate limit for the /api/v2/oauth/clients endpoint. The default limit is often 10 requests per second for this endpoint, but it can vary by organization tier.
  • Fix:
    1. Implement exponential backoff.
    2. Check the Retry-After header in the response.
    3. Reduce the page_size if you are making many small requests, or increase it to 100 to reduce the number of HTTP calls.
    4. Add a time.sleep(0.5) between requests in your loop, as shown in the complete example.

Error: Empty Entities List

  • Cause: There are no OAuth clients in the organization, or the pagination logic stopped prematurely.
  • Fix:
    1. Verify manually in the Admin console that clients exist.
    2. Check the nextPage field in the JSON response. If it is null, the API has no more data.
    3. Ensure you are not filtering by clientType inadvertently in the query parameters.

Official References