Deploy GraphWarden and make your first proxied Graph call

Deploy the GraphWarden proxy on-premises as a Windows Service or in a Docker container, provision one App Identity in the Admin app, and route one Graph call through the proxy. The steps below target a prepared host with Azure AD credentials already set up; expected time is around ten minutes end to end.

On-premises Windows Service path

Prerequisites

- Windows Server 2019 or later, OR a Linux or Windows host running Docker - Access to the GraphWarden Admin app with permission to provision an App Identity - An Azure AD tenant where you have already registered a Graph application and written its client secret to an Azure Key Vault you operate - PowerShell 5.1 or later, or .NET 6 or later, for the verification call
This quickstart uses the on-premises Windows Service deployment. Hosted customers can skip step 1 — the proxy URL is provided to you and the runtime is managed.
The ten-minute target assumes a prepared host with Azure AD credentials already provisioned in Key Vault. First-time Azure AD registration and Key Vault setup take longer and are covered by the trust model and deployment modes concept pages.

Step 1: Install the proxy

Download the GraphWarden proxy binary onto your Windows Server host and register it as a Windows Service. Deeper installation coverage — silent installers, upgrade flow, multi-tenant hosts, network hardening — is part of the Phase 2 deployment documentation.

# Run in an elevated PowerShell session on the proxy host.
$InstallDir = 'C:\Program Files\GraphWardenProxy'
$ServiceName = 'GraphWardenProxy'

# 1. Copy the release binaries to $InstallDir (done out of band — unzip or MSI).

# 2. Register the Windows Service. Quoting matters when the path contains spaces.
sc.exe create $ServiceName `
    binPath= "`"$InstallDir\GraphProxy.Proxy.exe`"" `
    DisplayName= "GraphWarden Proxy" `
    start= auto

# 3. Start the service.
Start-Service -Name $ServiceName

# 4. Confirm it is listening.
Get-Service -Name $ServiceName
Invoke-WebRequest -UseBasicParsing -Uri http://localhost:5000/health

Configuration lives in appsettings.json next to the executable. Set GraphClient:TenantId, GraphClient:ClientId, KeyVault:VaultUri, and ProxyJwt:SigningKey for a minimal startup. The environment variables page lists every key the proxy reads.

Step 2: Create an App Identity in the Admin app

The App Identity is the GraphWarden-side record that represents your client application. It carries the proxy credentials your app will send to /proxy/token and binds the app to a ruleset.

  1. Sign in to the GraphWarden Admin app.
  2. Open the ruleset you want to bind this application to. If you have no ruleset yet, create one — it can be a permissive starter ruleset for the smoke test and tightened later.
  3. Create a new App Identity. Give it a name that matches the calling application so the audit trail is readable.
  4. Copy the Proxy Client ID and Proxy Client Secret the Admin app displays. The secret is shown once; if you lose it you must rotate.
  5. Note the proxy URL you will use in step 3. For the on-premises deployment in step 1 this is the host and port Kestrel listens on.

Step 3: Make the proxied call

The samples below all do the same thing: acquire a proxy JWT from /proxy/token, point the Graph client at the proxy URL, and run one verification call against GET /v1.0/users?$top=1. Each sample reads the three environment variables you set from the values copied in step 2. Pick the tab for your stack; the per-language deep-dive guides at ../developers/dotnet, ../developers/nodejs, ../developers/python, ../developers/php-laravel, and ../powershell/quickstart cover the full integration details.

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

// GraphWarden-specific: read proxy URL + proxy credentials.
string proxyUrl     = Environment.GetEnvironmentVariable('GRAPHWARDEN_PROXY_URL')!;
string clientId     = Environment.GetEnvironmentVariable('GRAPHWARDEN_CLIENT_ID')!;
string clientSecret = Environment.GetEnvironmentVariable('GRAPHWARDEN_CLIENT_SECRET')!;

// Authority is the proxy, not login.microsoftonline.com.
var cca = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithClientSecret(clientSecret)
    .WithAuthority(new Uri($'{proxyUrl}/proxy/token'), validateAuthority: false)
    .Build();

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

// GraphWarden-specific: base URL is the proxy.
var http = new HttpClient { BaseAddress = new Uri(proxyUrl) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue('Bearer', token.AccessToken);

var graph = new GraphServiceClient(http);
var users = await graph.Users.GetAsync(r => { r.QueryParameters.Top = 1; });
Console.WriteLine(users?.Value?[0].DisplayName);
import { ConfidentialClientApplication } from '@azure/msal-node';
import { Client } from '@microsoft/microsoft-graph-client';

// GraphWarden-specific: read proxy URL + proxy credentials.
const proxyUrl     = process.env.GRAPHWARDEN_PROXY_URL;
const clientId     = process.env.GRAPHWARDEN_CLIENT_ID;
const clientSecret = process.env.GRAPHWARDEN_CLIENT_SECRET;

// Authority + knownAuthorities point MSAL Node at the proxy.
const pca = new ConfidentialClientApplication({
  auth: {
    clientId,
    clientSecret,
    authority: proxyUrl + '/proxy/token',
    knownAuthorities: [new URL(proxyUrl).host],
  },
});

const { accessToken } = await pca.acquireTokenByClientCredential({
  scopes: ['https://graph.microsoft.com/.default'],
});

// GraphWarden-specific: baseUrl is the proxy.
const graph = Client.init({ baseUrl: proxyUrl, authProvider: d => d(null, accessToken) });
const users = await graph.api('/users').top(1).get();
console.log(users.value[0].displayName);
import os
from azure.identity.aio import ClientSecretCredential
from msgraph import GraphServiceClient

# GraphWarden-specific: read proxy URL + proxy credentials.
proxy_url     = os.environ['GRAPHWARDEN_PROXY_URL']
client_id     = os.environ['GRAPHWARDEN_CLIENT_ID']
client_secret = os.environ['GRAPHWARDEN_CLIENT_SECRET']

# authority = the proxy, not login.microsoftonline.com.
credential = ClientSecretCredential(
    tenant_id='proxy',
    client_id=client_id,
    client_secret=client_secret,
    authority=proxy_url,
)

# GraphWarden-specific: host_name override routes every Graph call via the proxy.
graph = GraphServiceClient(credentials=credential, scopes=['https://graph.microsoft.com/.default'])
graph.request_adapter.base_url = proxy_url

users = await graph.users.get(request_configuration=lambda r: setattr(r.query_parameters, 'top', 1))
print(users.value[0].display_name)
<?php
require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client as GuzzleClient;
use Microsoft\Graph\GraphServiceClient;

// GraphWarden-specific: read proxy URL + proxy credentials.
$proxyUrl     = getenv('GRAPHWARDEN_PROXY_URL');
$clientId     = getenv('GRAPHWARDEN_CLIENT_ID');
$clientSecret = getenv('GRAPHWARDEN_CLIENT_SECRET');

// Acquire proxy JWT from the proxy token endpoint.
$http = new GuzzleClient();
$tokenResponse = $http->post($proxyUrl . '/proxy/token', [
    'form_params' => [
        'grant_type'    => 'client_credentials',
        'client_id'     => $clientId,
        'client_secret' => $clientSecret,
    ],
]);
$accessToken = json_decode((string) $tokenResponse->getBody())->access_token;

// GraphWarden-specific: construct Graph client against the proxy base URL.
$graph = new GraphServiceClient($accessToken, $proxyUrl);
$users = $graph->users()->get(fn ($r) => $r->queryParameters->top = 1)->wait();
echo $users->getValue()[0]->getDisplayName();
# 1. Configuration — read from the environment.
$ProxyUrl     = $env:GRAPHWARDEN_PROXY_URL     # e.g. https://proxy.example.com
$ClientId     = $env:GRAPHWARDEN_CLIENT_ID     # Proxy Client ID from the Admin app
$ClientSecret = $env:GRAPHWARDEN_CLIENT_SECRET # Proxy Client Secret from the Admin app

# 2. Acquire the proxy JWT.
$TokenResponse = Invoke-RestMethod `
    -Method Post `
    -Uri ($ProxyUrl + '/proxy/token') `
    -ContentType 'application/x-www-form-urlencoded' `
    -Body @{
        grant_type    = 'client_credentials'
        client_id     = $ClientId
        client_secret = $ClientSecret
    }

# 3. Point Microsoft.Graph at the proxy BEFORE Connect-MgGraph.
$env:MICROSOFT_GRAPH_API_ENDPOINT = $ProxyUrl
$SecureToken = ConvertTo-SecureString -String $TokenResponse.access_token -AsPlainText -Force

# 4. Connect and run one verification call.
Connect-MgGraph -AccessToken $SecureToken -NoWelcome
Get-MgUser -Top 1 | Select-Object Id, DisplayName, UserPrincipalName

The PowerShell quickstart covers the same flow in more depth, including the TLS 1.2 workaround for Windows PowerShell 5.1 and the override variables that let Invoke-MgGraphRequest reach the proxy directly. The per-language deep-dive guides explain the SDK-specific wiring (MSAL knownAuthorities for Node, request_adapter.base_url for Python, Laravel service-container binding for PHP, cloud.Configuration.ActiveDirectoryAuthorityHost for Go).

Verifying the call hit GraphWarden

Two checks confirm the request went through the proxy rather than bypassing it.

  • In the Admin app, open the audit log for your App Identity. You should see one entry for the verification call — timestamp, HTTP method, path, status, and the rule that matched. The correlation ID in the audit row matches the response header the proxy returned to your client.
  • The response body itself has the same shape as a direct Graph response. If a filter rule applied, specific properties will be absent; if an allow rule matched, the body is unchanged.

If the audit log is empty but your script succeeded, the request probably bypassed the proxy and hit Microsoft Graph directly. The troubleshooting section below covers the pattern.

Troubleshooting

The five failure modes below cover the common first-run surprises.

Service fails to start — "ProxyJwt:SigningKey is required"

appsettings.json is missing the ProxyJwt:SigningKey value or left it blank. Set a high-entropy secret of at least 32 characters and restart the service. Do not reuse a staging value in production.

Invoke-RestMethod returns 401 Unauthorized on /proxy/token

The Proxy Client ID or Proxy Client Secret in your environment does not match the hash stored for the App Identity. Confirm both values come from the same Admin app record and that nothing in your pipeline trimmed the secret. Rotate in the Admin app if you suspect the secret was logged or pasted incorrectly.

Proxy logs show KeyVault: access denied

The Managed Identity the proxy runs under does not have get permission on the Graph Client Secret in Key Vault. Grant Key Vault Secrets User scoped to the specific secret (RBAC mode) or an equivalent access policy entry (legacy policy mode). Confirm KeyVault:VaultUri points at the vault holding the secret.

403 Forbidden on the verification call despite a valid token

A rule in the bound ruleset returned a block verdict. The response body names the matched rule ID. Open the ruleset in the Admin app, review the match pattern and conditions, and use the What-If Simulator to replay the call without hitting Graph.

The verification call succeeds but the audit log is empty

The request reached Microsoft Graph directly and skipped the proxy. Check that MICROSOFT_GRAPH_API_ENDPOINT was set before Connect-MgGraph (PowerShell) or that HttpClient.BaseAddress was set on the HttpClient passed to GraphServiceClient (.NET). Run Get-MgContext to confirm the endpoint resolved to your proxy URL.

Last reviewed:

Last reviewed: .