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
scopesarray 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:readis the minimum scope required to list OAuth clients. If you intend to update scopes, you needadmin: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:
- Overly Broad Scopes: Clients with
admin:*orapi:*scopes. These grant near-omnipotent access and should be rare. - Write vs. Read: Clients with
:writescopes should be carefully monitored. - Unused Clients: Clients that are active but have not been used recently (requires checking
last_usedtimestamp, 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:
- Verify that
GENESYS_OAUTH_CLIENT_IDandGENESYS_OAUTH_CLIENT_SECRETare set correctly in your environment. - Ensure the OAuth client in the Genesys Cloud Admin Console is not disabled.
- Check that the client has the
admin:oauth:readscope 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:
- Navigate to the Genesys Cloud Admin Console.
- Go to Setup > Security > OAuth Clients.
- Select the client used for authentication.
- Click Edit and add the scope
admin:oauth:read. - 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:
- Implement exponential backoff in your retry logic.
- Reduce the frequency of requests.
- The SDK’s
PlatformClienthas 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:
- Ensure you include
expand=["scopes"]in theget_oauth_clientscall. - If using raw HTTP requests, append
?expand=scopesto the query string.