How to List All OAuth Clients and Audit Their Scope Assignments

How to List All OAuth Clients and Audit Their Scope Assignments

What You Will Build

  • A script that retrieves every OAuth client defined in your Genesys Cloud organization.
  • Code that parses each client’s scopes array to identify permission levels.
  • Logic to flag clients with overly broad permissions or missing critical scopes for security auditing.

Prerequisites

  • OAuth Client Type: Service Account (Confidential Client) or User Agent (Public Client) with sufficient permissions.
  • Required Scopes: admin:oauth:read is the minimum scope required to list OAuth clients. If you intend to update scopes, you need admin:oauth:write.
  • SDK Version: Genesys Cloud Python SDK genesyscloud >= 15.0.0.
  • Language/Runtime: Python 3.9+.
  • External Dependencies:
    • genesyscloud: The official Genesys Cloud Python SDK.
    • pandas (optional): For exporting results to CSV for easier analysis.

Authentication Setup

Genesys Cloud uses OAuth 2.0 for all API interactions. The most robust method for server-side scripts is the Client Credentials Grant. This flow exchanges your OAuth Client ID and Secret for an access token that is valid for 3600 seconds (1 hour).

The following code initializes the Genesys Cloud SDK using environment variables for credentials. This avoids hardcoding secrets in your source code.

import os
from genesyscloud import PlatformClient

def get_platform_client() -> PlatformClient:
    """
    Initializes and returns a configured PlatformClient instance.
    Uses environment variables for credentials.
    """
    # Retrieve credentials from environment
    client_id = os.getenv("GENESYS_OAUTH_CLIENT_ID")
    client_secret = os.getenv("GENESYS_OAUTH_CLIENT_SECRET")
    
    if not client_id or not client_secret:
        raise ValueError("GENESYS_OAUTH_CLIENT_ID and GENESYS_OAUTH_CLIENT_SECRET must be set in environment.")

    # Initialize the platform client
    platform_client = PlatformClient()
    
    # Configure OAuth
    platform_client.set_oauth_client_credentials(client_id, client_secret)
    
    # Set the host (usually api.mypurecloud.com, but can vary by region)
    # The SDK defaults to US, but you can explicitly set it if needed:
    # platform_client.set_host("api.usw2.purecloud.com")

    return platform_client

Critical Note on Token Refresh: The PlatformClient handles token refresh automatically in the background. When a 401 Unauthorized response is received due to an expired token, the SDK will attempt to refresh the token using the stored client credentials and retry the request. You do not need to implement manual refresh logic unless you are using raw HTTP requests without the SDK.

Implementation

Step 1: Retrieve All OAuth Clients

The Genesys Cloud API endpoint for listing OAuth clients is GET /api/v2/oauth/clients. This endpoint supports pagination via the pageSize parameter. By default, it returns 25 clients. To ensure you capture all clients in a large organization, you must handle pagination.

The SDK method get_oauth_clients() encapsulates this logic. It accepts a page_size argument. We will set this to the maximum allowed value (1000) to minimize the number of API calls, though the SDK will still paginate if the total count exceeds 1000.

from genesyscloud.api import OauthApi
from genesyscloud.rest import ApiException

def list_all_oauth_clients(platform_client: PlatformClient) -> list:
    """
    Fetches all OAuth clients from the Genesys Cloud organization.
    Handles pagination internally via the SDK.
    
    Returns:
        list: A list of OAuthClient objects.
    """
    oauth_api = OauthApi(platform_client)
    
    all_clients = []
    page_size = 1000  # Maximum page size allowed by Genesys Cloud
    
    try:
        # The SDK's get_oauth_clients handles pagination automatically 
        # if you iterate over the response or use the specific pagination helpers.
        # However, for explicit control and error handling, we can use the raw method.
        
        response = oauth_api.get_oauth_clients(
            page_size=page_size,
            expand=["scopes"] # Critical: Include scopes in the response
        )
        
        if response.entities:
            all_clients.extend(response.entities)
            
        # Check if there are more pages
        while response.next_page:
            response = oauth_api.get_oauth_clients(
                page_size=page_size,
                expand=["scopes"],
                after=response.next_page # Use the 'after' cursor for pagination
            )
            if response.entities:
                all_clients.extend(response.entities)
                
        return all_clients
        
    except ApiException as e:
        print(f"Exception when calling OauthApi->get_oauth_clients: {e}")
        raise

Why expand=["scopes"] is Critical:
By default, the GET /api/v2/oauth/clients endpoint returns a lightweight representation of the client, including id, name, description, and client_type. It does not include the scopes array. If you omit the expand parameter, you will receive a list of clients with empty or null scope data, rendering your audit useless. The expand parameter tells the API to include nested resources in the response payload.

Step 2: Parse and Analyze Scope Assignments

Once you have the list of OAuthClient objects, you need to extract the scope information. Each client has a scopes attribute, which is a list of strings.

Common scope patterns to audit:

  1. Overly Broad Scopes: Clients with admin:* or api:* scopes. These grant near-omnipotent access and should be rare.
  2. Write vs. Read: Clients with :write scopes should be carefully monitored.
  3. Unused Clients: Clients that are active but have not been used recently (requires checking last_used timestamp, which is available in the client object).
from typing import List, Dict, Any
from genesyscloud.models import OAuthClient

def analyze_client_scopes(clients: List[OAuthClient]) -> List[Dict[str, Any]]:
    """
    Analyzes each client's scopes and returns a structured audit report.
    
    Returns:
        list: A list of dictionaries containing client ID, name, and flagged scopes.
    """
    audit_results = []
    
    # Define high-risk scopes for flagging
    high_risk_scopes = [
        "admin:*",
        "api:*",
        "user:*",
        "user:write",
        "organization:write"
    ]
    
    for client in clients:
        client_id = client.id
        client_name = client.name
        client_type = client.client_type
        is_public = client.public
        scopes = client.scopes or []
        
        # Identify high-risk scopes assigned to this client
        flagged_scopes = []
        for scope in scopes:
            for risk_scope in high_risk_scopes:
                # Check for exact match or wildcard overlap (simple string check)
                if scope == risk_scope or (risk_scope.endswith("*") and scope.startswith(risk_scope[:-1])):
                    flagged_scopes.append(scope)
        
        # Structure the result
        result = {
            "client_id": client_id,
            "client_name": client_name,
            "client_type": client_type,
            "is_public": is_public,
            "total_scopes": len(scopes),
            "flagged_scopes": flagged_scopes,
            "risk_level": "HIGH" if flagged_scopes else "LOW"
        }
        
        audit_results.append(result)
        
    return audit_results

Step 3: Filter and Export Results

For a practical audit, you likely want to focus on specific client types (e.g., Service Accounts vs. Public Clients) or filter by risk level. The following function filters the audit results and prints a summary. In a production environment, you would export this to JSON or CSV.

import json

def generate_audit_report(audit_results: List[Dict[str, Any]], output_file: str = "oauth_audit.json") -> None:
    """
    Filters audit results and saves them to a JSON file.
    Also prints a summary to the console.
    """
    # Filter for High Risk clients
    high_risk_clients = [r for r in audit_results if r["risk_level"] == "HIGH"]
    
    # Filter for Public Clients with Write Scopes (Security Risk)
    public_write_risks = [
        r for r in audit_results 
        if r["is_public"] and any("write" in s for s in r["flagged_scopes"])
    ]
    
    # Prepare final output structure
    final_report = {
        "total_clients": len(audit_results),
        "high_risk_count": len(high_risk_clients),
        "public_write_risks_count": len(public_write_risks),
        "high_risk_details": high_risk_clients,
        "public_write_risk_details": public_write_risks,
        "all_clients": audit_results
    }
    
    # Write to JSON file
    with open(output_file, 'w') as f:
        json.dump(final_report, f, indent=2)
        
    # Console Summary
    print(f"Audit Complete.")
    print(f"Total Clients: {len(audit_results)}")
    print(f"High Risk Clients: {len(high_risk_clients)}")
    print(f"Public Clients with Write Scopes: {len(public_write_risks)}")
    print(f"Report saved to: {output_file}")
    
    # Print details for high-risk clients
    if high_risk_clients:
        print("\n--- High Risk Clients ---")
        for client in high_risk_clients:
            print(f"ID: {client['client_id']} | Name: {client['client_name']}")
            print(f"  Flagged Scopes: {', '.join(client['flagged_scopes'])}")
            print()

Complete Working Example

The following script combines all previous steps into a single, runnable module. It requires the genesyscloud package and environment variables for authentication.

import os
import sys
import json
from typing import List, Dict, Any

# Genesys Cloud SDK Imports
from genesyscloud import PlatformClient
from genesyscloud.api import OauthApi
from genesyscloud.rest import ApiException

def get_platform_client() -> PlatformClient:
    """
    Initializes and returns a configured PlatformClient instance.
    """
    client_id = os.getenv("GENESYS_OAUTH_CLIENT_ID")
    client_secret = os.getenv("GENESYS_OAUTH_CLIENT_SECRET")
    
    if not client_id or not client_secret:
        raise ValueError("GENESYS_OAUTH_CLIENT_ID and GENESYS_OAUTH_CLIENT_SECRET must be set in environment.")

    platform_client = PlatformClient()
    platform_client.set_oauth_client_credentials(client_id, client_secret)
    
    # Optional: Set host if not using default US region
    # platform_client.set_host("api.euw1.purecloud.com")
    
    return platform_client

def list_all_oauth_clients(platform_client: PlatformClient) -> list:
    """
    Fetches all OAuth clients from the Genesys Cloud organization.
    """
    oauth_api = OauthApi(platform_client)
    all_clients = []
    page_size = 1000
    
    try:
        # Initial request
        response = oauth_api.get_oauth_clients(
            page_size=page_size,
            expand=["scopes"] # Essential for scope data
        )
        
        if response.entities:
            all_clients.extend(response.entities)
            
        # Pagination loop
        while response.next_page:
            response = oauth_api.get_oauth_clients(
                page_size=page_size,
                expand=["scopes"],
                after=response.next_page
            )
            if response.entities:
                all_clients.extend(response.entities)
                
        return all_clients
        
    except ApiException as e:
        print(f"Exception when calling OauthApi->get_oauth_clients: {e}")
        raise

def analyze_client_scopes(clients: list) -> List[Dict[str, Any]]:
    """
    Analyzes each client's scopes and returns a structured audit report.
    """
    audit_results = []
    
    # Define high-risk scopes
    high_risk_scopes = [
        "admin:*",
        "api:*",
        "user:*",
        "user:write",
        "organization:write",
        "routing:*",
        "analytics:*"
    ]
    
    for client in clients:
        client_id = client.id
        client_name = client.name
        client_type = client.client_type
        is_public = client.public
        scopes = client.scopes or []
        
        flagged_scopes = []
        for scope in scopes:
            for risk_scope in high_risk_scopes:
                # Check for exact match
                if scope == risk_scope:
                    flagged_scopes.append(scope)
                # Check for wildcard overlap (e.g., admin:org:read matches admin:*)
                elif risk_scope.endswith("*") and scope.startswith(risk_scope[:-1]):
                    flagged_scopes.append(scope)
        
        # Remove duplicates in flagged_scopes
        flagged_scopes = list(set(flagged_scopes))
        
        result = {
            "client_id": client_id,
            "client_name": client_name,
            "client_type": client_type,
            "is_public": is_public,
            "total_scopes": len(scopes),
            "flagged_scopes": flagged_scopes,
            "risk_level": "HIGH" if flagged_scopes else "LOW"
        }
        
        audit_results.append(result)
        
    return audit_results

def generate_audit_report(audit_results: List[Dict[str, Any]], output_file: str = "oauth_audit.json") -> None:
    """
    Filters audit results and saves them to a JSON file.
    """
    high_risk_clients = [r for r in audit_results if r["risk_level"] == "HIGH"]
    public_write_risks = [
        r for r in audit_results 
        if r["is_public"] and any("write" in s for s in r["flagged_scopes"])
    ]
    
    final_report = {
        "total_clients": len(audit_results),
        "high_risk_count": len(high_risk_clients),
        "public_write_risks_count": len(public_write_risks),
        "high_risk_details": high_risk_clients,
        "public_write_risk_details": public_write_risks,
        "all_clients": audit_results
    }
    
    with open(output_file, 'w') as f:
        json.dump(final_report, f, indent=2)
        
    print(f"Audit Complete.")
    print(f"Total Clients: {len(audit_results)}")
    print(f"High Risk Clients: {len(high_risk_clients)}")
    print(f"Public Clients with Write Scopes: {len(public_write_risks)}")
    print(f"Report saved to: {output_file}")

def main():
    try:
        # 1. Authenticate
        print("Authenticating with Genesys Cloud...")
        platform_client = get_platform_client()
        
        # 2. Fetch Clients
        print("Fetching OAuth clients...")
        clients = list_all_oauth_clients(platform_client)
        print(f"Retrieved {len(clients)} clients.")
        
        # 3. Analyze Scopes
        print("Analyzing scopes...")
        audit_results = analyze_client_scopes(clients)
        
        # 4. Generate Report
        print("Generating audit report...")
        generate_audit_report(audit_results)
        
    except Exception as e:
        print(f"An error occurred: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 401 Unauthorized

Cause: The OAuth token is invalid, expired, or the client credentials are incorrect.
Fix:

  1. Verify that GENESYS_OAUTH_CLIENT_ID and GENESYS_OAUTH_CLIENT_SECRET are set correctly in your environment.
  2. Ensure the OAuth client in the Genesys Cloud Admin Console is not disabled.
  3. Check that the client has the admin:oauth:read scope assigned to itself. If the client used for authentication lacks this scope, the API will return a 403 Forbidden, not 401. A 401 usually indicates a bad token or credentials.

Error: 403 Forbidden

Cause: The authenticated user or service account does not have the required permissions to view OAuth clients.
Fix:

  1. Navigate to the Genesys Cloud Admin Console.
  2. Go to Setup > Security > OAuth Clients.
  3. Select the client used for authentication.
  4. Click Edit and add the scope admin:oauth:read.
  5. Save the changes. Note that scope changes may take up to 1 minute to propagate.

Error: 429 Too Many Requests

Cause: You have exceeded the API rate limit. Genesys Cloud enforces rate limits per organization and per endpoint.
Fix:

  1. Implement exponential backoff in your retry logic.
  2. Reduce the frequency of requests.
  3. The SDK’s PlatformClient has built-in retry logic for 429s, but you can configure it:
    platform_client.set_retry_on_rate_limit(True)
    platform_client.set_max_retries(5)
    

Error: scopes is None or Empty

Cause: The expand parameter was omitted in the API call.
Fix:

  1. Ensure you include expand=["scopes"] in the get_oauth_clients call.
  2. If using raw HTTP requests, append ?expand=scopes to the query string.

Official References