How to List All OAuth Clients in an Org and Check Their Scope Assignments Programmatically
What You Will Build
- A Python script that retrieves every OAuth client application registered in a Genesys Cloud CX organization.
- Logic that iterates through each client to extract and validate assigned OAuth scopes against a required permission set.
- Implementation using the Genesys Cloud Python SDK (
genesyscloud) with production-grade error handling and pagination.
Prerequisites
- OAuth Client Type: Service Account or JWT Service Account with the
admin:oauthclient:readscope. - SDK Version:
genesyscloud>= 2.0.0 (Python). - Language/Runtime: Python 3.8+.
- External Dependencies:
genesyscloud: The official Genesys Cloud CX Python SDK.python-dotenv(optional): For managing environment variables securely.
Install the SDK via pip:
pip install genesyscloud python-dotenv
Authentication Setup
The Genesys Cloud SDK handles OAuth2 token management automatically. You must provide a client ID, client secret, and the API host (usually api.mypurecloud.com for US regions). The SDK caches the token and refreshes it when it expires.
Create a .env file in your project root:
GENESYS_CLIENT_ID=your_client_id_here
GENESYS_CLIENT_SECRET=your_client_secret_here
GENESYS_API_HOST=api.mypurecloud.com
Initialize the API client in your code. This step establishes the connection and validates your credentials.
import os
from dotenv import load_dotenv
from purecloudplatform.client.configuration import Configuration
from purecloudplatform.client.api_client import ApiClient
# Load environment variables
load_dotenv()
def get_api_client() -> ApiClient:
"""
Initializes and returns a configured Genesys Cloud API Client.
Raises ValueError if required environment variables are missing.
"""
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
api_host = os.getenv("GENESYS_API_HOST", "api.mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set.")
# Configure the client
configuration = Configuration(
host=f"https://{api_host}",
client_id=client_id,
client_secret=client_secret
)
# Create the API client instance
api_client = ApiClient(configuration)
return api_client
Implementation
Step 1: Retrieve All OAuth Clients
The endpoint for listing OAuth clients is paginated. The Genesys Cloud API returns a maximum of 100 items per page by default, though you can request up to 500. To ensure you capture every client in large organizations, you must implement pagination logic.
The SDK method is get_oauth_client_list. It returns a OAuthClientListResponse object containing an entities list and a next_page URL.
Required Scope: admin:oauthclient:read
from purecloudplatform.client.api.oauth_api import OAuthApi
from purecloudplatform.client.rest import ApiException
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def list_all_oauth_clients(api_client: ApiClient) -> list:
"""
Fetches all OAuth clients from the Genesys Cloud organization using pagination.
Args:
api_client: An initialized ApiClient instance.
Returns:
A list of OAuthClient objects.
"""
oauth_api = OAuthApi(api_client)
all_clients = []
# Initial request with maximum page size
page_size = 500
page_number = 1
try:
while True:
logger.info(f"Fetching OAuth clients page {page_number}...")
# Call the API
response = oauth_api.get_oauth_client_list(
page_size=page_size,
page_number=page_number
)
# Append entities to the master list
if response.entities:
all_clients.extend(response.entities)
logger.info(f"Retrieved {len(response.entities)} clients on this page.")
else:
logger.info("No more clients found.")
break
# Check if there is a next page
if not response.next_page:
logger.info("No next page URL. Pagination complete.")
break
# Extract page number from next_page URL or increment manually
# The SDK response usually provides the next page URL, but the
# get_oauth_client_list method doesn't automatically follow it.
# We manually increment page_number for the next iteration.
page_number += 1
except ApiException as e:
logger.error(f"API Exception when listing OAuth clients: {e.status} - {e.reason}")
if e.status == 401:
logger.error("Authentication failed. Check Client ID and Secret.")
elif e.status == 403:
logger.error("Forbidden. Ensure the service account has 'admin:oauthclient:read' scope.")
raise
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise
logger.info(f"Total OAuth clients retrieved: {len(all_clients)}")
return all_clients
Step 2: Extract and Validate Scopes
Each OAuthClient object contains a scopes attribute, which is a list of strings representing the permissions granted to that client. To audit these clients, you often need to check if they possess specific high-risk scopes or if they are missing required scopes for a specific integration.
We will create a validation function that checks each client against a dictionary of required scope sets.
from typing import Dict, List, Optional
def audit_client_scopes(
clients: List,
required_scopes_map: Dict[str, List[str]]
) -> Dict[str, Dict]:
"""
Audits OAuth clients against a map of required scopes.
Args:
clients: List of OAuthClient objects.
required_scopes_map: Dictionary where keys are client names/IDs and values
are lists of required scopes.
Returns:
A dictionary mapping client ID to audit results.
"""
audit_results = {}
for client in clients:
client_id = client.id
client_name = client.name
client_scopes = set(client.scopes) if client.scopes else set()
# Initialize result for this client
result = {
"name": client_name,
"client_type": client.client_type,
"all_scopes": client_scopes,
"missing_scopes": [],
"has_required": True
}
# Check if this client is in the required_scopes_map
# We can match by name or ID. Here we try name first, then ID.
required_scopes = None
if client_name in required_scopes_map:
required_scopes = required_scopes_map[client_name]
elif client_id in required_scopes_map:
required_scopes = required_scopes_map[client_id]
if required_scopes:
# Calculate missing scopes
missing = set(required_scopes) - client_scopes
if missing:
result["missing_scopes"] = list(missing)
result["has_required"] = False
audit_results[client_id] = result
return audit_results
Step 3: Processing Results and Reporting
After auditing, you need to format the output for consumption. This could be a JSON file for another system or a console report for a developer. We will generate a JSON summary that highlights clients failing the scope check.
import json
def generate_audit_report(audit_results: Dict) -> str:
"""
Generates a JSON string report of the audit results.
Only includes clients that are missing required scopes or have high-risk scopes.
"""
flagged_clients = {}
for client_id, data in audit_results.items():
# Flag clients missing required scopes
if not data["has_required"]:
flagged_clients[client_id] = {
"name": data["name"],
"type": data["client_type"],
"missing_scopes": data["missing_scopes"],
"current_scopes": list(data["all_scopes"])
}
# Optional: Flag clients with dangerous scopes (e.g., admin:*)
dangerous_scopes = {"admin:*", "user:*"}
if dangerous_scopes.intersection(data["all_scopes"]):
if client_id not in flagged_clients:
flagged_clients[client_id] = {
"name": data["name"],
"type": data["client_type"],
"dangerous_scopes_found": list(dangerous_scopes.intersection(data["all_scopes"])),
"current_scopes": list(data["all_scopes"])
}
else:
flagged_clients[client_id]["dangerous_scopes_found"] = list(
dangerous_scopes.intersection(data["all_scopes"])
)
return json.dumps(flagged_clients, indent=2)
Complete Working Example
This script combines all previous steps into a single executable module. It sets up the client, fetches all OAuth applications, audits them against a defined policy, and outputs the results.
import os
import json
import logging
from dotenv import load_dotenv
from purecloudplatform.client.configuration import Configuration
from purecloudplatform.client.api_client import ApiClient
from purecloudplatform.client.api.oauth_api import OAuthApi
from purecloudplatform.client.rest import ApiException
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def initialize_client() -> ApiClient:
"""Initializes the Genesys Cloud API Client."""
load_dotenv()
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
api_host = os.getenv("GENESYS_API_HOST", "api.mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("Missing GENESYS_CLIENT_ID or GENESYS_CLIENT_SECRET in environment variables.")
configuration = Configuration(
host=f"https://{api_host}",
client_id=client_id,
client_secret=client_secret
)
return ApiClient(configuration)
def fetch_all_oauth_clients(api_client: ApiClient) -> list:
"""Fetches all OAuth clients with pagination."""
oauth_api = OAuthApi(api_client)
all_clients = []
page_size = 500
page_number = 1
try:
while True:
logger.info(f"Fetching page {page_number}...")
response = oauth_api.get_oauth_client_list(
page_size=page_size,
page_number=page_number
)
if not response.entities:
break
all_clients.extend(response.entities)
if not response.next_page:
break
page_number += 1
except ApiException as e:
logger.error(f"API Error: {e.status} - {e.reason}")
raise
return all_clients
def audit_clients(clients: list, policy: dict) -> dict:
"""
Audits clients against a policy.
Policy format:
{
"client_name_or_id": ["required_scope_1", "required_scope_2"],
"dangerous_scopes": ["admin:*", "user:*"]
}
"""
results = {}
dangerous_scopes = set(policy.get("dangerous_scopes", []))
for client in clients:
client_id = client.id
client_name = client.name
client_scopes = set(client.scopes) if client.scopes else set()
entry = {
"id": client_id,
"name": client_name,
"type": client.client_type,
"scopes": list(client_scopes),
"issues": []
}
# Check for dangerous scopes
found_dangerous = dangerous_scopes.intersection(client_scopes)
if found_dangerous:
entry["issues"].append(f"Contains dangerous scopes: {list(found_dangerous)}")
# Check for required scopes if this client is in the policy
required = policy.get(client_name) or policy.get(client_id)
if required:
missing = set(required) - client_scopes
if missing:
entry["issues"].append(f"Missing required scopes: {list(missing)}")
results[client_id] = entry
return results
def main():
try:
# 1. Initialize
api_client = initialize_client()
# 2. Define Audit Policy
# In a real scenario, load this from a JSON file or config service
audit_policy = {
"MyCriticalIntegration": ["admin:analytics:read", "api:conversations:read"],
"dangerous_scopes": ["admin:*", "user:*", "admin:oauthclient:write"]
}
# 3. Fetch Data
logger.info("Fetching all OAuth clients...")
clients = fetch_all_oauth_clients(api_client)
logger.info(f"Found {len(clients)} OAuth clients.")
# 4. Audit
logger.info("Auditing clients against policy...")
audit_results = audit_clients(clients, audit_policy)
# 5. Report
flagged_count = sum(1 for r in audit_results.values() if r["issues"])
logger.info(f"Audit complete. {flagged_count} clients have issues.")
# Output specific flagged clients
for client_id, data in audit_results.items():
if data["issues"]:
print(f"\n--- ISSUE FOUND: {data['name']} ({client_id}) ---")
for issue in data["issues"]:
print(f" * {issue}")
# Save full results to JSON
with open("oauth_audit_results.json", "w") as f:
json.dump(audit_results, f, indent=2)
logger.info("Full results saved to oauth_audit_results.json")
except Exception as e:
logger.error(f"Script failed: {e}")
raise
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
Cause: The service account used for authentication lacks the admin:oauthclient:read scope.
Fix:
- Navigate to the Genesys Cloud Admin UI.
- Go to Security > OAuth.
- Find your service account client.
- Click Edit.
- In the Scopes tab, search for
admin:oauthclient:readand add it. - Save the changes. Note: You may need to regenerate the client secret if you are using a JWT service account and the certificate has changed, but scope updates usually take effect immediately for new tokens.
Error: 429 Too Many Requests
Cause: You are hitting the API rate limit. The Genesys Cloud API enforces rate limits per client ID. Fetching large pages or rapid successive calls can trigger this.
Fix:
Implement exponential backoff. The Genesys Cloud Python SDK does not automatically retry 429s in all versions, so you must handle it explicitly.
import time
def fetch_with_retry(api_client, api_call_func, *args, max_retries=3):
for attempt in range(max_retries):
try:
return api_call_func(*args)
except ApiException as e:
if e.status == 429:
wait_time = 2 ** attempt
logger.warning(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
raise Exception("Max retries exceeded for 429 error")
Error: AttributeError: ‘NoneType’ object has no attribute ‘entities’
Cause: The API call returned a response object, but the entities field is None or missing because the response structure changed or an unexpected error occurred that was not raised as an ApiException.
Fix:
Always check if response is not None and if response.entities exists before iterating. The code above includes this check:
if response.entities:
all_clients.extend(response.entities)