How requests flow through GraphWarden

GraphWarden is a reverse proxy that stands between a client application and Microsoft Graph. The client authenticates to the proxy, the proxy evaluates the ruleset bound to that client's App Identity, and allowed calls are forwarded to Graph with the real tenant credentials the proxy reads from your Key Vault.

Shipped in Phase 1

Prerequisites

- A Microsoft 365 tenant with an Azure AD application registration you can use against Microsoft Graph - A deployed GraphWarden Proxy instance reachable from your client app (see deployment modes page) - An App Identity issued by GraphWarden App Admin (`proxy_client_id` + `proxy_client_secret`)

The request lifecycle

A request traverses five numbered stages. Each stage is a decision point and a failure point; the troubleshooting section below maps each to its observable error.

  1. Client token request — your app posts client_id + client_secret (the proxy pair issued by GraphWarden) to POST /proxy/token using the OAuth2 client_credentials grant. No Graph call happens at this stage.
  2. Proxy authenticates the client — the proxy looks up the proxy client_id in its identity store, compares a SHA-256 hash of the supplied secret against the stored hash, and — if valid — reads the paired Graph client secret from your Azure Key Vault.
  3. Ruleset evaluation — when your app subsequently calls a Graph endpoint (e.g. GET /v1.0/users), the proxy decodes its own JWT Bearer token, resolves the App Identity, loads the associated ruleset, and walks the rules top-down. The first rule whose match and conditions pass decides the verdict: allow, filter, block, or log_only.
  4. Forward to Graph — for allow, filter, and log_only, the proxy issues the upstream request to https://graph.microsoft.com using the real tenant token it obtained from MSAL. The request path and query string are preserved byte-for-byte by the YARP catch-all route.
  5. Response transformation — on the way back, filter rules apply allow_properties or strip_properties to the response body. block never reaches Graph and returns 403 Forbidden directly. Every outcome (allowed, filtered, blocked) produces an audit event.
┌──────────┐    ┌──────────────┐    ┌──────────────────┐
│ Your app │───▶│  GraphWarden │───▶│ Microsoft Graph  │
└──────────┘    │    Proxy     │    │   graph.ms.com   │
                └──────┬───────┘    └──────────────────┘
                       │
                       ▼
                ┌──────────────┐
                │  Audit log   │
                │ (Elastic)    │
                └──────────────┘

Components

The runtime is a single self-hosted ASP.NET Core service. The pieces below are load-bearing and worth naming explicitly; everything else is auxiliary.

  • Kestrel — the proxy runs as a Kestrel-hosted ASP.NET Core service. It is NOT hosted behind IIS. On Windows Server the binary registers as a Windows Service via sc.exe create; in hosted mode it runs as a container process.
  • YARP reverse proxy — a catch-all route (Match: { Path: "/{**catch-all}" }) forwards every authenticated request to https://graph.microsoft.com with path and query preserved. No per-endpoint routing table is maintained.
  • MSAL client — the proxy uses Microsoft Authentication Library to acquire Graph tokens via the client-credentials flow, keyed on the real Azure AD client_id resolved from the App Identity. Tokens are cached to their natural expiry.
  • YamlDotNet rule loader — rulesets are YAML files read at startup and on each sync cycle. RuleLoader.cs parses the schema, validates conditions and transforms, and materializes an evaluation tree in memory.
  • Serilog — structured JSON logs are emitted to stdout (container) or the Windows Event Log (service). An audit sink ships one event per Graph request to Elasticsearch when Audit:ElasticsearchUrl is configured.

What the proxy does NOT do

GraphWarden's scope is narrow on purpose. Confusion about the proxy usually traces back to assuming it does one of the things below.

  • It does not sign users in. GraphWarden speaks app-to-service OAuth2 only. User authentication (interactive sign-in, delegated scopes, MFA prompts) remains in Azure AD. If your app uses delegated flows today, you still acquire the user token from Azure AD; the proxy only intermediates app tokens.
  • It does not cache Graph response bodies by default. Per-rule cache_ttl caches the rule verdict for a given caller and object, not the raw payload. Response bodies are forwarded fresh on every request unless a rule explicitly opts in.
  • It does not store user data. Audit events are metadata: timestamp, caller App Identity, HTTP method, URI, status code, latency, matched rule. Request and response bodies are NOT written to the audit store unless a response-filter transform inspects them in-flight to redact fields.
  • It does not replace Azure RBAC or Conditional Access. GraphWarden gates Graph API calls from your apps; it does not evaluate user role assignments, device compliance, or sign-in risk. Those continue to live in Azure AD and Microsoft Entra.

Troubleshooting

401 Unauthorized from /proxy/token — the client_id or client_secret your app sent does not match any App Identity. Confirm the pair was issued by GraphWarden App Admin for this environment (proxy credentials differ per deployed proxy instance). Check the proxy log for Unknown proxy client_id or Secret hash mismatch.

403 Forbidden on a Graph request with a valid Bearer token — a ruleset evaluated the request and returned block, OR no rule matched and the default policy in your tenant is deny. Inspect the audit event for the correlation ID in the response header and find the MatchedRuleId field to see which rule fired.

502 Bad Gateway returned from a Graph endpoint that normally works — the proxy reached the upstream but Graph returned an error the proxy could not classify, OR MSAL failed to acquire a tenant token. Check Kestrel stdout for Upstream Graph call failed and confirm the Graph secret in Key Vault has not expired.

Requests hang for minutes, then time out — the proxy is failing to reach Key Vault. Managed Identity may be misconfigured, or the outbound egress rule from the proxy host blocks *.vault.azure.net. Confirm az login --identity works from the host if applicable.

See it in production

Scenarios that anchor this architecture to concrete customer patterns — multi-tenant SaaS, internal HR integrations, SIEM-fed audit — are collected on the use cases page. For a walkthrough against your own Graph-integrated estate, contact the GraphWarden team.

Last reviewed:

Last reviewed: .