Integrate GraphWarden with a Python application

A Python application using msgraph-sdk (the modern Microsoft Graph SDK for Python) and azure-identity can route every Graph call through GraphWarden by overriding two values: the token endpoint and the Graph base URL. The rest of the SDK surface — GraphServiceClient request builders, pagination, $select and $filter — keeps working unchanged. This page shows a complete working sample.

msgraph-sdk 1.x + azure-identity 1.x

Prerequisites

- Python 3.10 or later (the sample targets 3.11; earlier 3.x versions also work) - `msgraph-sdk` v1.x from PyPI (the modern `GraphServiceClient`, not the legacy `msgraph-core`) - `azure-identity` v1.x from PyPI - A GraphWarden proxy URL reachable from your app - A Proxy Client ID and Proxy Client Secret issued by GraphWarden App Admin for the App Identity that represents this application

Migrate a Python application with an AI assistant

Migrate my existing Python application from direct Microsoft Graph API calls to use GraphWarden as a proxy.

My app currently uses azure-identity.ClientSecretCredential with https://graph.microsoft.com.

I need to:
1. Change the base URL to my GraphWarden proxy URL
2. Replace real Graph credentials with GraphWarden proxy credentials
3. Preserve all existing Graph SDK calls unchanged
4. Add error handling for GraphWarden-specific responses (401 invalid proxy credentials, 403 ruleset denial)

Reference documentation: https://graphwarden.com/llms.txt

Ask me for:
- My GraphWarden proxy URL
- My proxy client_id and proxy client_secret
- Which Graph endpoints and scopes my app uses today
- Whether my app is synchronous or async (asyncio)

Reference: llms.txt

The two overrides

A direct-to-Graph Python app configures two things: a ClientSecretCredential pointed at Azure AD, and a GraphServiceClient whose underlying request adapter defaults to https://graph.microsoft.com. To route through GraphWarden you override both.

  • Token acquisition — point ClientSecretCredential at the proxy's token endpoint (/proxy/token) by supplying authority=proxyBaseUrl and pass the Proxy Client ID and Proxy Client Secret in place of the Azure AD app credentials. The token you receive is a proxy JWT, not a Graph token; treat it as an opaque Bearer value.
  • Graph base URL — after constructing the GraphServiceClient, set client.request_adapter.base_url to the proxy URL. The SDK composes request URIs against that base, so await client.users.get() issues GET {proxy-host}/v1.0/users with the proxy JWT attached.

Install

pip install msgraph-sdk azure-identity
# msgraph-sdk depends on kiota-abstractions and httpx; pip resolves these transitively.

Minimal working sample

The program below acquires a proxy JWT and lists display names for the first page of users. Both overrides are named explicitly so the shape of the change is obvious. Production apps should pull the proxy URL and credentials from environment variables (see the next section) rather than hardcoding them.

# graphwarden_sample.py
import asyncio
import os
from azure.identity.aio import ClientSecretCredential
from msgraph import GraphServiceClient

# 1. Configuration — in production, load these from environment variables.
proxy_base_url   = "https://proxy.example.com"                       # Issued when the proxy was deployed
proxy_client_id  = "00000000-0000-0000-0000-000000000000"            # Proxy Client ID from App Admin
proxy_client_sec = "REDACTED"                                        # Proxy Client Secret from App Admin


async def main() -> None:
    # 2. azure-identity credential pointed at the proxy token endpoint, NOT Azure AD.
    #    The `authority` kwarg overrides the default login.microsoftonline.com host.
    credential = ClientSecretCredential(
        tenant_id="proxy",                                           # Placeholder — the proxy ignores it
        client_id=proxy_client_id,
        client_secret=proxy_client_sec,
        authority=proxy_base_url,                                    # GraphWarden-issued proxy host
    )

    # 3. Build the GraphServiceClient, then rebind its request adapter base URL to the proxy.
    #    The credential supplies the Bearer token on every request; setting base_url points the
    #    SDK at the proxy for all Graph path composition.
    client = GraphServiceClient(
        credentials=credential,
        scopes=["https://graph.microsoft.com/.default"],
    )
    client.request_adapter.base_url = proxy_base_url                 # GraphWarden base URL override

    # 4. Standard Graph SDK call — unchanged from a direct-Graph integration.
    from msgraph.generated.users.users_request_builder import UsersRequestBuilder
    query = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
        select=["displayName"], top=10,
    )
    config = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
        query_parameters=query,
    )
    page = await client.users.get(request_configuration=config)

    for user in (page.value or []):
        print(user.display_name)

    await credential.close()


if __name__ == "__main__":
    asyncio.run(main())

The important call is await client.users.get(...). It is identical to what you would write for a direct Graph integration — no request rewriting, no URL string building, no custom adapter. The base_url rebind is what points the SDK at the proxy.

The proxy accepts the same Graph paths and query strings Microsoft Graph accepts, byte-for-byte. `$select`, `$filter`, `$expand`, pagination with `@odata.nextLink`, and batch requests all pass through unchanged. If a rule's Response Filter removes properties, the missing keys will be `None` on the deserialised object — a `User` with `given_name` stripped will have `user.given_name is None`.

Configuration via environment

In production, read the proxy URL and credentials from environment variables. The conventional names below align with the proxy's own __ double-underscore override pattern used elsewhere in the documentation.

# graphwarden_client.py
import os
from azure.identity.aio import ClientSecretCredential
from msgraph import GraphServiceClient


def _require_env(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"{name} is required.")
    return value


def build_graph_client() -> GraphServiceClient:
    proxy_base_url   = _require_env("GRAPHWARDEN_PROXY_URL")
    proxy_client_id  = _require_env("GRAPHWARDEN_CLIENT_ID")
    proxy_client_sec = _require_env("GRAPHWARDEN_CLIENT_SECRET")

    credential = ClientSecretCredential(
        tenant_id="proxy",
        client_id=proxy_client_id,
        client_secret=proxy_client_sec,
        authority=proxy_base_url,
    )

    client = GraphServiceClient(
        credentials=credential,
        scopes=["https://graph.microsoft.com/.default"],
    )
    client.request_adapter.base_url = proxy_base_url
    return client

ClientSecretCredential caches the proxy JWT until it nears expiry and transparently refreshes it on the next call. Keep a single credential instance for the lifetime of your application to take advantage of the cache. If you run multiple processes (a gunicorn worker pool, a Celery worker fleet), each process keeps its own cache; the cost of an occasional refresh is low because proxy tokens last one hour.

Sync vs async

The sample uses azure.identity.aio and the async GraphServiceClient. If your application is synchronous, import ClientSecretCredential from azure.identity (non-aio) and wrap Graph calls with asyncio.run(...) at the entry point — the SDK itself remains async, so a small sync shim is unavoidable. New codebases should prefer async; the Graph SDK is async-first and every request builder returns an awaitable.

Error handling

The Graph SDK surfaces proxy failures as APIError instances (kiota_abstractions.api_error.APIError) with a response_status_code attribute. Catch and branch on outcome.

  • 401 Unauthorized — the proxy JWT is expired, never issued, or the Proxy Client ID/Secret was rejected by /proxy/token. ClientSecretCredential will request a fresh token on the next call automatically.
  • 403 Forbidden — a rule returned a block verdict. The response body names the matched rule. Do not retry automatically; the caller or the App Identity lacks permission for this endpoint.
  • 502 Bad Gateway — Microsoft Graph returned a non-2xx upstream. Treat it as a transient Graph failure; back off and retry with jitter.
  • 429 Too Many Requests — forwarded from Graph with Retry-After intact. Honour the header exactly as you would against direct Graph.

Troubleshooting

The failure modes below cover the common onboarding surprises when wiring a Python app against GraphWarden for the first time.

401 from /proxy/tokeninvalid_client

The Proxy Client ID or Proxy Client Secret sent to /proxy/token does not match the hash stored for your App Identity. Confirm both values were copied from GraphWarden App Admin for the right App Identity. A common cause in Python is loading .env files with python-dotenv where the secret contains a $ or # character and quoting was missed — wrap secrets in single quotes inside .env to keep interpolation off.

403 Forbidden on every Graph call

A rule in the ruleset bound to your App Identity is matching aggressively. The response body names the rule id. Review the rule's match.endpoint_pattern and conditions in App Admin, or use the What-If Simulator to replay a specific request and see which rule fires first.

502 Bad Gateway

Microsoft Graph returned a non-2xx upstream response and the proxy forwarded it as 502. Check Microsoft's Graph status page and the proxy's /health endpoint. Retry with exponential backoff and jitter; a transient Graph outage is the most common cause.

503 Service Unavailable on startup

The proxy could not read the real Graph credentials from Azure Key Vault. In on-prem deployments this usually means the Managed Identity is missing or the Key Vault access policy was not granted. The proxy operator, not your application, resolves this. Your app should retry with backoff — once the proxy recovers, token acquisition succeeds.

httpx.ConnectError or SSL verification errors

The proxy is reachable by DNS but its TLS certificate is not trusted. The Graph SDK uses httpx under the hood, which in turn reads the SSL_CERT_FILE environment variable and the system CA bundle. In production, use a publicly trusted certificate or install the issuing CA in the system trust store. Never disable verification in production code.

aiohttp vs httpx considerations

The modern msgraph-sdk uses httpx as its HTTP transport — there is no aiohttp dependency. Older tutorials and the legacy msgraph-core package referenced aiohttp; if you see import errors mentioning aiohttp, you are on the deprecated path. Pin msgraph-sdk >= 1.0.0 to stay on the supported stack.

SDK calls go to graph.microsoft.com instead of the proxy

The client.request_adapter.base_url rebind was missed. Without it the SDK composes URIs against https://graph.microsoft.com. Re-run the setup and confirm proxy_base_url has no trailing slash — https://proxy.example.com is correct.

Last reviewed:

Last reviewed: .