Integrate GraphWarden with a Node.js application
A Node.js application using @microsoft/microsoft-graph-client (the Microsoft Graph JavaScript SDK) and @azure/msal-node 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 — fluent builders, pagination, $select and $filter — keeps working unchanged. This page shows a complete working sample.
Prerequisites
Migrate a Node.js application with an AI assistant
Migrate my existing Node.js application from direct Microsoft Graph API calls to use GraphWarden as a proxy.
My app currently uses @azure/msal-node 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 I use ESM or CommonJS
Reference: llms.txt
The two overrides
A direct-to-Graph Node app configures two things: a ConfidentialClientApplication pointed at Azure AD, and a Client instance from @microsoft/microsoft-graph-client that defaults to https://graph.microsoft.com. To route through GraphWarden you override both.
- Token acquisition — build
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 — initialise the
Clientwith abaseUrloption pointing at the proxy. The SDK composes request URIs against that base, soclient.api('/users').get()issuesGET {proxy-host}/v1.0/userswith the proxy JWT attached.
Install
npm install @microsoft/microsoft-graph-client@^3 @azure/msal-node@^2
# The Graph SDK uses the global `fetch` on Node 18+. No isomorphic-fetch polyfill is needed.
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.ts
import { ConfidentialClientApplication } from '@azure/msal-node';
import { Client } from '@microsoft/microsoft-graph-client';
// 1. Configuration — in production, read these from process.env.
const proxyBaseUrl = 'https://proxy.example.com'; // Issued when the proxy was deployed
const tokenEndpoint = `${proxyBaseUrl}/proxy/token`; // GraphWarden-issued proxy JWT endpoint
const proxyClientId = '00000000-0000-0000-0000-000000000000'; // Proxy Client ID from App Admin
const proxyClientSec = 'REDACTED'; // Proxy Client Secret from App Admin
// 2. Build an MSAL confidential client whose authority is the proxy, NOT Azure AD.
// knownAuthorities tells MSAL to trust this host as a valid authority.
const cca = new ConfidentialClientApplication({
auth: {
clientId: proxyClientId,
clientSecret: proxyClientSec,
authority: tokenEndpoint, // Proxy token endpoint
knownAuthorities: [new URL(proxyBaseUrl).host],
},
});
// 3. Acquire a proxy JWT via client_credentials.
const tokenResponse = await cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default'],
});
if (!tokenResponse?.accessToken) {
throw new Error('GraphWarden token acquisition failed.');
}
const proxyJwt = tokenResponse.accessToken;
// 4. Build a Graph Client pointed at the proxy. Every SDK call now hits {proxyBaseUrl}/{graph-path}.
const graphClient = Client.init({
baseUrl: proxyBaseUrl, // GraphWarden base URL override
authProvider: (done) => done(null, proxyJwt), // Supplies the proxy JWT
});
// 5. Standard Graph SDK call — unchanged from a direct-Graph integration.
const page = await graphClient.api('/users').select('displayName').top(10).get();
for (const user of page.value) {
console.log(user.displayName);
}
The important call is graphClient.api('/users').get(). It is identical to what you would write for a direct Graph integration — no request rewriting, no URL string building, no custom middleware. The baseUrl option is what points the SDK at the proxy.
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.ts
import { ConfidentialClientApplication } from '@azure/msal-node';
import { Client } from '@microsoft/microsoft-graph-client';
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required.`);
return value;
}
const proxyBaseUrl = requireEnv('GRAPHWARDEN_PROXY_URL');
const proxyClientId = requireEnv('GRAPHWARDEN_CLIENT_ID');
const proxyClientSec = requireEnv('GRAPHWARDEN_CLIENT_SECRET');
// Keep a single MSAL instance — its in-memory token cache returns the cached
// proxy JWT until it is near expiry and silently refreshes afterwards.
const cca = new ConfidentialClientApplication({
auth: {
clientId: proxyClientId,
clientSecret: proxyClientSec,
authority: `${proxyBaseUrl}/proxy/token`,
knownAuthorities: [new URL(proxyBaseUrl).host],
},
});
export async function getGraphClient(): Promise<Client> {
const result = await cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default'],
});
if (!result?.accessToken) throw new Error('Proxy token acquisition failed.');
return Client.init({
baseUrl: proxyBaseUrl,
authProvider: (done) => done(null, result.accessToken),
});
}
MSAL Node's token cache is in-memory by default. Call acquireTokenByClientCredential on each request — MSAL returns the cached token when one is still valid and transparently refreshes before expiry. If you run multiple processes (a worker pool, a serverless runtime), each process keeps its own cache; the cost of an occasional refresh is low because proxy tokens last one hour.
Error handling
The Graph SDK surfaces proxy failures as GraphError instances with a statusCode field. Inspect the code 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 failure modes below cover the common onboarding surprises when wiring a Node.js app against GraphWarden for the first time.
401 from /proxy/token — invalid_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. Watch for trailing whitespace introduced by shell redirection (echo $SECRET > .env) — a newline at the end of the secret string is a common culprit.
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.
UNABLE_TO_VERIFY_LEAF_SIGNATURE or TLS errors
The proxy is reachable by DNS but its TLS certificate is not trusted by the Node.js runtime. In development, verify the certificate chain. In production, use a publicly trusted certificate or install the issuing CA in the system trust store (Node honours the NODE_EXTRA_CA_CERTS environment variable as a last resort). Never disable certificate validation in production code.
SDK calls go to graph.microsoft.com instead of the proxy
The baseUrl option was not passed to Client.init. Without it the SDK composes URIs against https://graph.microsoft.com. Double-check the init call and confirm proxyBaseUrl has no trailing slash — https://proxy.example.com is correct; https://proxy.example.com/ can produce double slashes in composed URIs on some SDK versions.
Last reviewed: