Integrate GraphWarden with a .NET application

A .NET application using Microsoft.Identity.Client (MSAL) and Microsoft.Graph (the Microsoft Graph SDK for .NET) 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 — request builders, entity models, pagination, $select and $filter — keeps working unchanged. This page shows a complete working sample.

MSAL 4.x + Microsoft.Graph 5.x

Prerequisites

- .NET 6 or later (the sample targets .NET 8; earlier LTS versions also work) - `Microsoft.Graph` v5.x NuGet package - `Microsoft.Identity.Client` v4.x NuGet package (pulled in transitively by Graph, or installed explicitly for the token flow) - 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 .NET application with an AI assistant

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

My app currently uses 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

Reference: llms.txt

The two overrides

A direct-to-Graph .NET app configures two things: a ConfidentialClientApplication pointed at Azure AD, and a GraphServiceClient that defaults to https://graph.microsoft.com. To route through GraphWarden you override both.

  • Token acquisition — build the ConfidentialClientApplication against the proxy's token endpoint (/proxy/token) instead of Azure AD, 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 — construct the GraphServiceClient with an HttpClient whose BaseAddress points at the proxy. The Graph SDK composes request URIs against that base address, so graphClient.Users.GetAsync(...) issues GET {proxy-host}/v1.0/users with the proxy JWT attached.

Minimal working sample

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

using System.Net.Http.Headers;
using Microsoft.Graph;
using Microsoft.Identity.Client;

// 1. Configuration — in production, load these from IConfiguration / env vars.
string proxyBaseUrl    = "https://proxy.example.com";                 // Issued when the proxy was deployed
string tokenEndpoint   = $"{proxyBaseUrl}/proxy/token";               // GraphWarden-issued proxy JWT endpoint
string proxyClientId   = "00000000-0000-0000-0000-000000000000";      // Proxy Client ID from App Admin
string proxyClientSec  = "REDACTED";                                  // Proxy Client Secret from App Admin

// 2. Acquire a proxy JWT via client_credentials against the proxy token endpoint.
//    Note: Authority is the proxy, NOT Azure AD. The proxy issues its own token.
var cca = ConfidentialClientApplicationBuilder
    .Create(proxyClientId)
    .WithClientSecret(proxyClientSec)
    .WithAuthority(new Uri(tokenEndpoint), validateAuthority: false)
    .Build();

var tokenResult = await cca
    .AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
    .ExecuteAsync();

string proxyJwt = tokenResult.AccessToken;

// 3. Build a GraphServiceClient whose HttpClient points at the proxy.
//    Every SDK call now hits {proxyBaseUrl}/{graph-path}.
var http = new HttpClient
{
    BaseAddress = new Uri(proxyBaseUrl),
};
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", proxyJwt);

var graphClient = new GraphServiceClient(http);

// 4. Standard Graph SDK call — unchanged from a direct-Graph integration.
var users = await graphClient.Users.GetAsync(config =>
{
    config.QueryParameters.Select = new[] { "displayName" };
    config.QueryParameters.Top    = 10;
});

foreach (var user in users?.Value ?? new List<Microsoft.Graph.Models.User>())
{
    Console.WriteLine(user.DisplayName);
}

The important call is graphClient.Users.GetAsync(...). The SDK call is identical to what you would write for a direct Graph integration — no request rewriting, no URL string building, no custom middleware. The HttpClient.BaseAddress override 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 not appear in the deserialized entity — a `User` with `GivenName` stripped will have `GivenName == null`.

Configuration via environment

In production, read the proxy URL and credentials from environment variables or your configuration provider of choice. The conventional names below align with the proxy's own __ double-underscore override pattern.

// Program.cs — Generic Host sample. Adjust for ASP.NET Core startup.
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<GraphServiceClient>(sp =>
{
    var cfg = sp.GetRequiredService<IConfiguration>();

@DOC_CODEFENCE_0000@@
});

Real code should refresh the proxy JWT before expiry rather than acquiring once at startup — MSAL's token cache handles the refresh for you if you keep the same IConfidentialClientApplication instance around and call AcquireTokenForClient on each use. MSAL returns a cached token when one is still valid.

Error handling

The .NET Graph SDK surfaces proxy failures as standard ServiceException instances. Inspect ServiceException.ResponseStatusCode to branch on outcome.

  • 401 Unauthorized — the proxy JWT is expired, never issued, or the Proxy Client ID/Secret was rejected by /proxy/token. Request a new token through MSAL before retrying.
  • 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 five failure modes below cover the common onboarding surprises when wiring a .NET app against GraphWarden for the first time.

MsalServiceException: AADSTS700016 from AcquireTokenForClient

MSAL is trying to authenticate against Azure AD, not the proxy. Confirm you passed WithAuthority(new Uri($"{proxyBaseUrl}/proxy/token"), validateAuthority: false). The validateAuthority: false flag is required because the proxy is not an Azure AD authority and would fail MSAL's default authority validation.

HttpRequestException: The SSL connection could not be established

The proxy is reachable by DNS but its TLS certificate is not trusted by the .NET runtime. In development, verify the certificate chain; in production, use a publicly trusted certificate or install the issuing CA in the machine trust store. Never disable certificate validation in production code.

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

The HttpClient.BaseAddress override was missed. The Graph SDK uses whatever HttpClient you pass to GraphServiceClient; if you rely on the default, the SDK composes URIs against https://graph.microsoft.com. Instantiate the HttpClient with BaseAddress = new Uri(proxyBaseUrl) and inject it through the GraphServiceClient constructor.

All calls return 403 Forbidden with the same rule id

A rule in the ruleset bound to the App Identity is matching aggressively. 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.

SDK version mismatch — methods missing or renamed

The sample targets Microsoft.Graph v5.x. v4.x uses IGraphServiceUsersCollectionRequest and a different request-builder shape; v5.x uses the GetAsync overload with a delegate configurator. Pin your project to v5.x to match the sample verbatim, or adapt the call sites to your installed major version.

Last reviewed:

Last reviewed: .