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 SDKPureCloudPlatformClientV2. - The tutorial covers Python 3.9+ with the
requestslibrary 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:readis mandatory for listing clients. If you need to inspect detailed configuration,oauth:client:config:readmay be useful, but for scope auditing,oauth:client:readsuffices. - SDK Version:
genesys-cloud-sdk-pythonv100+ (or compatible v90+). - Language/Runtime: Python 3.9 or higher.
- External Dependencies:
pip install genesys-cloud-sdk-pythonpip 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:
- Clients with
admin:org:readoradmin:org:writethat are not M2M. - 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:readscope assigned. - Fix:
- Log in to Genesys Cloud Admin.
- Navigate to Setup > Integrations > OAuth Clients.
- Select your M2M client.
- In the Scopes tab, ensure
oauth:client:readis checked. - 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:
- Verify the
CLIENT_IDandCLIENT_SECRETmatch the values in the Admin console exactly. - Ensure the token request returns a 200 OK. If it returns 401, the credentials are wrong.
- If the token was generated previously, check if it has expired (default TTL is usually 1 hour). Regenerate the token.
- Verify the
Error: 429 Too Many Requests
- Cause: You are hitting the rate limit for the
/api/v2/oauth/clientsendpoint. The default limit is often 10 requests per second for this endpoint, but it can vary by organization tier. - Fix:
- Implement exponential backoff.
- Check the
Retry-Afterheader in the response. - Reduce the
page_sizeif you are making many small requests, or increase it to 100 to reduce the number of HTTP calls. - 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:
- Verify manually in the Admin console that clients exist.
- Check the
nextPagefield in the JSON response. If it is null, the API has no more data. - Ensure you are not filtering by
clientTypeinadvertently in the query parameters.