Integrate GraphWarden with a Go application
A Go application using github.com/microsoftgraph/msgraph-sdk-go (the official Microsoft Graph SDK for Go) and github.com/Azure/azure-sdk-for-go/sdk/azidentity 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 request builders, pagination, $select and $filter — keeps working unchanged. This page shows a complete working sample.
Prerequisites
Migrate a Go application with an AI assistant
Migrate my existing Go application from direct Microsoft Graph API calls to use GraphWarden as a proxy.
My app currently uses azidentity.NewClientSecretCredential 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 my binary runs in a container (affects TLS trust store setup)
Reference: llms.txt
The two overrides
A direct-to-Graph Go app configures two things: an azidentity.ClientSecretCredential pointed at Azure AD, and a msgraphsdk.GraphServiceClient whose request adapter defaults to https://graph.microsoft.com. To route through GraphWarden you override both.
- Token acquisition — build
ClientSecretCredentialwithClientOptions.Cloudpointing at a customcloud.ConfigurationwhoseActiveDirectoryAuthorityHostis 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 — after constructing the
GraphServiceClient, callclient.RequestAdapter().SetBaseUrl(proxyBaseUrl). The SDK composes request URIs against that base, soclient.Users().Get(ctx, nil)issuesGET {proxy-host}/v1.0/userswith the proxy JWT attached.
Install
go get github.com/microsoftgraph/msgraph-sdk-go@latest
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity@latest
# Pin exact versions in go.mod for reproducibility (see Troubleshooting below).
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.
// main.go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
"github.com/microsoftgraph/msgraph-sdk-go/users"
)
func main() {
// 1. Configuration — in production, read these from os.Getenv.
proxyBaseURL := "https://proxy.example.com" // Issued when the proxy was deployed
proxyClientID := "00000000-0000-0000-0000-000000000000" // Proxy Client ID from App Admin
proxyClientSec := "REDACTED" // Proxy Client Secret from App Admin
// 2. Build a cloud.Configuration pointed at the proxy, NOT Azure AD.
// azidentity consults the configuration to decide where to POST the token request.
proxyCloud := cloud.Configuration{
ActiveDirectoryAuthorityHost: proxyBaseURL + "/proxy/token", // GraphWarden token endpoint
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{},
}
credential, err := azidentity.NewClientSecretCredential(
"proxy", // tenant ID — placeholder; proxy ignores it
proxyClientID,
proxyClientSec,
&azidentity.ClientSecretCredentialOptions{
ClientOptions: azcore.ClientOptions{Cloud: proxyCloud},
},
)
if err != nil {
log.Fatalf("credential build failed: %v", err)
}
// 3. Build the GraphServiceClient with the custom credential, then rebind
// the request adapter's base URL to the proxy.
client, err := msgraphsdk.NewGraphServiceClientWithCredentials(
credential,
[]string{"https://graph.microsoft.com/.default"},
)
if err != nil {
log.Fatalf("graph client build failed: %v", err)
}
client.RequestAdapter.SetBaseUrl(proxyBaseURL) // GraphWarden base URL override
// 4. Standard Graph SDK call — unchanged from a direct-Graph integration.
selectCols := []string{"displayName"}
topVal := int32(10)
config := &users.UsersRequestBuilderGetRequestConfiguration{
QueryParameters: &users.UsersRequestBuilderGetQueryParameters{
Select: selectCols,
Top: &topVal,
},
}
page, err := client.Users().Get(context.Background(), config)
if err != nil {
log.Fatalf("users.Get failed: %v", err)
}
for _, user := range page.GetValue() {
if user.GetDisplayName() != nil {
fmt.Println(*user.GetDisplayName())
}
}
_ = os.Stdout.Sync()
}
The important call is client.Users().Get(ctx, config). It is identical to what you would write for a direct Graph integration — no request rewriting, no URL string building. The SetBaseUrl call 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.go
package graphwarden
import (
"fmt"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
)
func requireEnv(name string) (string, error) {
v := os.Getenv(name)
if v == "" {
return "", fmt.Errorf("%s is required", name)
}
return v, nil
}
func NewClient() (*msgraphsdk.GraphServiceClient, error) {
proxyBaseURL, err := requireEnv("GRAPHWARDEN_PROXY_URL")
if err != nil {
return nil, err
}
clientID, err := requireEnv("GRAPHWARDEN_CLIENT_ID")
if err != nil {
return nil, err
}
clientSec, err := requireEnv("GRAPHWARDEN_CLIENT_SECRET")
if err != nil {
return nil, err
}
proxyCloud := cloud.Configuration{
ActiveDirectoryAuthorityHost: proxyBaseURL + "/proxy/token",
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{},
}
cred, err := azidentity.NewClientSecretCredential("proxy", clientID, clientSec,
&azidentity.ClientSecretCredentialOptions{
ClientOptions: azcore.ClientOptions{Cloud: proxyCloud},
})
if err != nil {
return nil, err
}
client, err := msgraphsdk.NewGraphServiceClientWithCredentials(
cred, []string{"https://graph.microsoft.com/.default"},
)
if err != nil {
return nil, err
}
client.RequestAdapter.SetBaseUrl(proxyBaseURL)
return client, nil
}
azidentity.ClientSecretCredential caches the proxy JWT in memory and refreshes transparently before expiry. Construct one GraphServiceClient at program startup and reuse it — the SDK is safe to use from multiple goroutines.
Error handling
The Graph SDK surfaces proxy failures as *abstractions.ApiError (or derived types) returned from each request. Inspect the StatusCode field to branch on outcome.
- 401 Unauthorized — the proxy JWT is expired, never issued, or the Proxy Client ID/Secret was rejected by
/proxy/token.ClientSecretCredentialwill request a fresh token on the next call automatically. - 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 Go 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. In container deployments, check whether docker run -e or the Kubernetes secret mount trimmed a trailing newline — Go's os.Getenv preserves newlines literally and they become part of the secret.
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.
x509: certificate signed by unknown authority in containers
The proxy's TLS certificate is not trusted by the minimal base image. Scratch, distroless, and Alpine images ship without a full CA bundle. In a Dockerfile, add RUN apk --no-cache add ca-certificates on Alpine or COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ when building on scratch. For self-signed certificates in development, set the SSL_CERT_FILE environment variable to the CA bundle path. Never disable verification in production.
Module-version pinning
Microsoft's Graph SDK pulls in a deep dependency graph (Kiota abstractions, HTTP client, serialisation). Upgrading one module without the others can produce compilation errors against generated types. Pin the major version set in go.mod and use go mod tidy to reconcile. When upgrading, bump msgraph-sdk-go, kiota-abstractions-go, kiota-http-go, and kiota-serialization-*-go together.
SDK calls go to graph.microsoft.com instead of the proxy
The SetBaseUrl call on the request adapter was missed. Without it the SDK composes URIs against https://graph.microsoft.com. Re-run the setup and confirm proxyBaseURL has no trailing slash — https://proxy.example.com is correct.
Last reviewed: