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.
Prerequisites
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
ConfidentialClientApplicationagainst 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
GraphServiceClientwith anHttpClientwhoseBaseAddresspoints at the proxy. The Graph SDK composes request URIs against that base address, sographClient.Users.GetAsync(...)issuesGET {proxy-host}/v1.0/userswith 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.
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
blockverdict. 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-Afterintact. 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: