How to List All OAuth Clients and Audit Scope Assignments in Genesys Cloud
What You Will Build
- This tutorial demonstrates how to retrieve a complete list of OAuth clients within a Genesys Cloud organization using the Python SDK.
- It uses the
PureCloudPlatformClientV2SDK to call the/api/v2/oauth/clientsendpoint and parse the resulting JSON payload. - The code extracts client names, client IDs, and associated OAuth scopes to enable programmatic auditing of permission assignments.
Prerequisites
- OAuth Client Type: Service Account or Resource Owner Password flow with the scope
oauth:client:read. - SDK Version:
genesys-cloud-sdk-pythonversion 138.0.0 or later. - Language/Runtime: Python 3.9+.
- Dependencies:
pip install purecloud-platform-client-v2
Authentication Setup
Genesys Cloud uses OAuth 2.0 for API authentication. For server-to-server operations like auditing clients, a Service Account is the standard approach. You must configure a Service Account in the Genesys Cloud Admin portal with the oauth:client:read scope before running this code.
The following code initializes the SDK and authenticates using a Service Account. It caches the token to avoid unnecessary refresh calls during the script execution.
import os
import sys
from purecloud_platform_client_v2 import PlatformClient, Configuration
from purecloud_platform_client_v2.rest import ApiException
def get_platform_client() -> PlatformClient:
"""
Initializes and returns an authenticated Genesys Cloud PlatformClient.
Uses environment variables for security.
"""
# Load credentials from environment variables
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
environment = os.getenv("GENESYS_ENVIRONMENT", "my.genesys.cloud") # Default to US
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment.")
# Configure the SDK
configuration = Configuration(
client_id=client_id,
client_secret=client_secret,
environment=environment
)
# Create the platform client instance
platform_client = PlatformClient(configuration)
# Force authentication to ensure token is valid
try:
platform_client.oauth_client.authenticate()
except ApiException as e:
print(f"Authentication failed: {e.status} {e.reason}", file=sys.stderr)
raise
return platform_client
if __name__ == "__main__":
try:
client = get_platform_client()
print("Successfully authenticated with Genesys Cloud.")
except Exception as e:
print(f"Error during setup: {e}", file=sys.stderr)
sys.exit(1)
Implementation
Step 1: Retrieve the List of OAuth Clients
The core API endpoint for this task is GET /api/v2/oauth/clients. In the Python SDK, this is exposed via the OAuthApi class. The endpoint supports pagination, so we must handle the page_size and page_number parameters to ensure we retrieve all clients if the organization has more than the default page size (typically 25 or 50).
from purecloud_platform_client_v2.api import OAuthApi
from purecloud_platform_client_v2.models import GetOauthClientsResponse
def list_all_oauth_clients(platform_client: PlatformClient) -> list[GetOauthClientsResponse]:
"""
Retrieves all OAuth clients from the organization.
Handles pagination to ensure all clients are fetched.
"""
oauth_api = OAuthApi(platform_client)
all_clients = []
page_number = 1
page_size = 100 # Max allowed page size is often 100 or 200 depending on endpoint limits
while True:
try:
# Call the API
response = oauth_api.post_oauth_clients_query(
body={
"page_size": page_size,
"page_number": page_number
}
)
# Append current page clients
if response.entities:
all_clients.extend(response.entities)
# Check if there are more pages
if response.page_number >= response.total_pages:
break
page_number += 1
except ApiException as e:
print(f"API Error {e.status}: {e.reason}", file=sys.stderr)
if e.status == 429:
print("Rate limit hit. Retrying in 5 seconds...", file=sys.stderr)
import time
time.sleep(5)
continue
else:
raise
return all_clients
Expected Response Structure:
The post_oauth_clients_query method returns a GetOauthClientsResponse object. The entities list contains OAuthClient objects. Each OAuthClient object has the following key attributes:
id: The unique identifier of the OAuth client.name: The display name of the client.client_id: The public client identifier.scopes: A list of strings representing the assigned OAuth scopes.type: The type of client (e.g.,service,user).
Step 2: Parse and Audit Scope Assignments
Once the list of clients is retrieved, we need to parse the scope assignments. This step is critical for security audits. We will create a function that extracts the client name, ID, and scopes, and flags any clients with overly permissive scopes (e.g., * or admin:all).
from typing import List, Dict
def audit_client_scopes(clients: list) -> List[Dict]:
"""
Parses the list of OAuth clients and extracts scope information.
Flags clients with dangerous scopes.
"""
audit_results = []
dangerous_scopes = ["*", "admin:all", "user:all"]
for client in clients:
client_info = {
"client_id": client.client_id,
"name": client.name,
"type": client.type,
"scopes": client.scopes if client.scopes else [],
"is_dangerous": False
}
# Check for dangerous scopes
if client.scopes:
for scope in client.scopes:
if scope in dangerous_scopes or scope.startswith("admin:"):
client_info["is_dangerous"] = True
break
audit_results.append(client_info)
return audit_results
Step 3: Output Results in a Structured Format
For practical use, we output the results as a JSON object. This allows the data to be consumed by other tools, stored in a database, or displayed in a dashboard.
import json
def print_audit_report(audit_results: List[Dict]):
"""
Prints the audit report in a human-readable format and as JSON.
"""
print("\n--- OAuth Client Scope Audit Report ---")
print(f"Total Clients Found: {len(audit_results)}")
dangerous_clients = [c for c in audit_results if c.get("is_dangerous")]
print(f"Clients with Potential Security Risks: {len(dangerous_clients)}")
print("-" * 50)
for client in audit_results:
status = " [RISK]" if client["is_dangerous"] else " [OK]"
print(f"ID: {client['client_id']}")
print(f"Name: {client['name']}")
print(f"Type: {client['type']}")
print(f"Scopes: {', '.join(client['scopes'])}")
print(f"Status:{status}")
print("-" * 50)
# Output full JSON for machine consumption
print("\n--- Full JSON Output ---")
print(json.dumps(audit_results, indent=2))
Complete Working Example
The following script combines all steps into a single, runnable module. It authenticates, retrieves all OAuth clients, audits their scopes, and prints a detailed report.
import os
import sys
import json
import time
from typing import List, Dict
from purecloud_platform_client_v2 import PlatformClient, Configuration
from purecloud_platform_client_v2.api import OAuthApi
from purecloud_platform_client_v2.rest import ApiException
from purecloud_platform_client_v2.models import GetOauthClientsResponse
def get_platform_client() -> PlatformClient:
"""
Initializes and returns an authenticated Genesys Cloud PlatformClient.
"""
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
environment = os.getenv("GENESYS_ENVIRONMENT", "my.genesys.cloud")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment.")
configuration = Configuration(
client_id=client_id,
client_secret=client_secret,
environment=environment
)
platform_client = PlatformClient(configuration)
try:
platform_client.oauth_client.authenticate()
except ApiException as e:
print(f"Authentication failed: {e.status} {e.reason}", file=sys.stderr)
raise
return platform_client
def list_all_oauth_clients(platform_client: PlatformClient) -> list:
"""
Retrieves all OAuth clients from the organization with pagination handling.
"""
oauth_api = OAuthApi(platform_client)
all_clients = []
page_number = 1
page_size = 100
while True:
try:
response = oauth_api.post_oauth_clients_query(
body={
"page_size": page_size,
"page_number": page_number
}
)
if response.entities:
all_clients.extend(response.entities)
if response.page_number >= response.total_pages:
break
page_number += 1
except ApiException as e:
print(f"API Error {e.status}: {e.reason}", file=sys.stderr)
if e.status == 429:
print("Rate limit hit. Retrying in 5 seconds...", file=sys.stderr)
time.sleep(5)
continue
else:
raise
return all_clients
def audit_client_scopes(clients: list) -> List[Dict]:
"""
Parses the list of OAuth clients and extracts scope information.
Flags clients with dangerous scopes.
"""
audit_results = []
dangerous_scopes = ["*", "admin:all", "user:all"]
for client in clients:
client_info = {
"client_id": client.client_id,
"name": client.name,
"type": client.type,
"scopes": client.scopes if client.scopes else [],
"is_dangerous": False
}
if client.scopes:
for scope in client.scopes:
if scope in dangerous_scopes or scope.startswith("admin:"):
client_info["is_dangerous"] = True
break
audit_results.append(client_info)
return audit_results
def main():
try:
# Step 1: Authenticate
print("Authenticating with Genesys Cloud...")
platform_client = get_platform_client()
# Step 2: Retrieve Clients
print("Fetching OAuth clients...")
clients = list_all_oauth_clients(platform_client)
print(f"Found {len(clients)} OAuth clients.")
# Step 3: Audit Scopes
print("Auditing scopes...")
audit_results = audit_client_scopes(clients)
# Step 4: Output Report
print("\n--- OAuth Client Scope Audit Report ---")
print(f"Total Clients Found: {len(audit_results)}")
dangerous_clients = [c for c in audit_results if c.get("is_dangerous")]
print(f"Clients with Potential Security Risks: {len(dangerous_clients)}")
print("-" * 50)
for client in audit_results:
status = " [RISK]" if client["is_dangerous"] else " [OK]"
print(f"ID: {client['client_id']}")
print(f"Name: {client['name']}")
print(f"Type: {client['type']}")
print(f"Scopes: {', '.join(client['scopes'])}")
print(f"Status:{status}")
print("-" * 50)
# Save to file
with open("oauth_client_audit.json", "w") as f:
json.dump(audit_results, f, indent=2)
print("\nFull report saved to oauth_client_audit.json")
except Exception as e:
print(f"Fatal error: {e}", file=sys.stderr)
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: Verify that
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETare correct. Ensure the Service Account has not been disabled. Check that the token refresh logic in the SDK is functioning by callingplatform_client.oauth_client.authenticate()explicitly.
Error: 403 Forbidden
- Cause: The authenticated Service Account does not have the required
oauth:client:readscope. - Fix: Log in to the Genesys Cloud Admin portal, navigate to Manage > Integrations > OAuth, select the Service Account, and ensure
oauth:client:readis checked under the Scopes section. Save the changes and regenerate the token.
Error: 429 Too Many Requests
- Cause: The API rate limit has been exceeded. This is common when iterating through large datasets or making rapid successive calls.
- Fix: Implement exponential backoff. The provided code includes a basic retry mechanism for 429 errors. For production systems, use a library like
tenacityto handle retries with jitter.
Error: AttributeError: ‘NoneType’ object has no attribute ‘entities’
- Cause: The API call returned
Noneor an unexpected structure, often due to a network error or SDK version mismatch. - Fix: Ensure you are using the latest version of
purecloud-platform-client-v2. Add logging to inspect the raw response if the issue persists.