althor
All writing
Pattern · 2026

Entra ID workload identities for agent systems

Four identity options, one decision you're making whether you realize it or not. A working note on picking — and configuring — the right Entra ID workload identity for an AI agent in a Microsoft tenant.

TL;DR
  • Four identity shapes — app-only, on-behalf-of, federated credentials, managed identity — and one decision: which fits the call's downstream-attribution requirement.
  • OBO is the only correct answer for user-scoped data; FIC kills the long-lived client_secret; managed identity is the cheapest path when the agent runs on Azure compute.
  • Five gotchas the wizard doesn't surface, four OBO traps, and a pre-flight list before going live.

This is the deep dive on layer 01 of Making agent deployments pass security review — identity. Most agent projects in M365 / Azure tenants hit the same wall at the same moment: the security architect asks who the agent is acting as, and the answer is "a service account I made last Tuesday with Global Administrator and a 90-day-rotation policy." That answer is the audit-review killer. Workload identities are how you make a different one available.

Why this matters now

An AI agent that reaches into Microsoft Graph, SharePoint, Dynamics, Cosmos, or any custom internal API is making API calls under some identity. That identity is what shows up in the downstream audit log — Entra sign-in logs, Graph audit logs, your own application's audit trail. There are exactly two outcomes:

Workload identities are the structural difference between those two outcomes. They've existed in Entra ID for years; what's changed is that agents now make many requests fast, against scoped data, and the audit shape of those requests is what determines whether the deployment survives review. Microsoft has explicitly named this problem space with Entra Agent ID as of 2026 — but the building blocks below predate it and remain the foundation for non-Microsoft-platform agents too.

The four-way decision matrix

Four identity shapes cover the realistic option space. The decision is not "which is best" — it's "which fits this call's downstream-attribution requirement." Multiple options can coexist in one agent system; the wrong one is the one that hides whose action it was.

option                    downstream identity   RBAC inherits     token lifetime      audit clarity
App-only (SP)             agent app's SPN        app role assignment 60–90 min          "the agent did it"
On-behalf-of (OBO)        signed-in user's UPN   user's directory    60–90 min          "user X's agent did it"
Federated cred (FIC)      agent app's SPN        app role assignment ~60 min (exchanged) "the agent did it"
                                                                                       + external IdP trail
Managed identity          managed identity SPN   identity role asgn  ~24h (platform)    "the resource did it"

Read the matrix this way. App-only is the agent acting as itself — fine when the agent operates on shared, non-user-scoped data (an alerting bot reading a tenant-wide telemetry table). OBO is the agent acting as a specific user — the only correct answer when the agent reads or writes user-scoped data. FIC is the agent runtime authenticating without a stored client secret — same downstream identity as app-only, but the credential bootstrap is a federated token exchange instead of client_secret. Managed identity is the platform-managed version of the agent's machine identity — applies when the agent runs on Azure compute.

The matrix above flattens nuance for legibility. Microsoft's own taxonomy folds applications, service principals, and managed identities into one umbrella — "workload identities" — with FIC as a credential type rather than an identity type. Mentally hold it both ways: four credential-and-attribution shapes, three Entra object types.

Federated credentials for agent runtimes

Federated identity credentials (FIC) are the path that doesn't get enough airtime in the agent-identity discussion. The premise is straightforward: an agent runtime outside Azure — a Vercel function, a GitHub Actions job, an on-prem Kubernetes pod, an AWS Lambda — can authenticate to an Entra app registration without storing a client secret. The runtime's native identity provider (GitHub's OIDC issuer, Kubernetes' service-account token, AWS's STS) issues a token; Entra trusts that token via a pre-configured trust relationship and exchanges it for an Entra access token. Workload identity federation is the canonical doc.

Why this matters for agents: the alternative is a long-lived client_secret sitting in Vercel env vars, GitHub repo secrets, or a Kubernetes Secret. Every one of those is a credential rotation problem with a non-zero leak surface. FIC eliminates the secret entirely. The cost is a one-time trust setup with three values you need to get right:

The trust-setup gotchas: subject mismatches fail silently (Entra rejects the exchange, returns nothing helpful), the issuer URL must exactly match what the IdP advertises in its OIDC discovery document, and FIC tokens issued by Entra ID itself are not eligible — federation is for external token issuers only. Microsoft documents the GitHub flow end-to-end in Authenticate to Azure from GitHub Actions by OpenID Connect; the same shape applies for Kubernetes, GCP, and SPIFFE.

On-behalf-of for user-triggered agent actions

The OAuth 2.0 on-behalf-of flow is the only correct answer for an agent that reads or writes user-scoped data. The shape is delegation: a user authenticates to your front-end (Teams app, Copilot Studio agent, custom web UI); the front-end gets an access token for the agent's API (token A); the agent's API exchanges token A — using its own client credentials plus token A as an assertion — for token B, which targets Microsoft Graph (or whatever downstream API the agent needs to call). Token B carries the user's identity.

The exchange happens at the Entra token endpoint with a specific shape — grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer and requested_token_use=on_behalf_of — and the resulting token B has the user's UPN in its claims. When the agent uses token B to call Graph, the resulting Graph audit-log entry attributes the action to the user, not the agent. The agent is a middle tier; the user is the principal.

Why API-key auth fails this case: an API key is the agent's identity. The downstream audit log reads "the agent's service principal did it" — and the user the agent was acting for is invisible. If your agent has 200 users and one of them triggers a sensitive Graph write, you cannot answer "which user" from the downstream log alone. OBO makes the answer the first claim in the token.

Practical OBO gotchas worth surfacing before you build:

What this looks like in code

Two short examples — one for each of the two paths agent developers actually write. A managed-identity call to Graph (the simplest path when the agent runs on Azure compute), and an OBO exchange (the path you write once and reuse in every user-triggered agent endpoint). Python because most agent runtimes use it; the MSAL surface is similar in C# and Node.

Managed identity → Graph (agent on Azure compute, app-only call):

# pip install azure-identity httpx
from azure.identity import DefaultAzureCredential
import httpx

# DefaultAzureCredential picks up the managed identity automatically
# when running on App Service / Functions / AKS with workload identity.
credential = DefaultAzureCredential()
token = credential.get_token("https://graph.microsoft.com/.default")

resp = httpx.get(
    "https://graph.microsoft.com/v1.0/groups",
    headers={"Authorization": f"Bearer {token.token}"},
)
resp.raise_for_status()

On-behalf-of exchange (agent API endpoint, acting as the calling user):

# pip install msal httpx
import msal, httpx

# Confidential client app — agent's own app registration.
app = msal.ConfidentialClientApplication(
    client_id=AGENT_CLIENT_ID,
    client_credential=AGENT_CLIENT_SECRET,  # or FIC in production
    authority=f"https://login.microsoftonline.com/{TENANT_ID}",
)

# user_token: the access token the front-end sent to this endpoint.
# Its `aud` claim must be the agent's own API.
result = app.acquire_token_on_behalf_of(
    user_assertion=user_token,
    scopes=["https://graph.microsoft.com/User.Read"],
)

if "access_token" not in result:
    # Surface CA challenges (interaction_required) back to the caller.
    raise RuntimeError(result.get("error_description"))

resp = httpx.get(
    "https://graph.microsoft.com/v1.0/me",
    headers={"Authorization": f"Bearer {result['access_token']}"},
)

The OBO snippet is the one most agent developers end up rewriting because the wizard-driven guides hand them an API-key copy-paste instead. The 15 lines above are the difference between "the agent did it" and "Samuel's agent, acting as Samuel, did it" in the Graph audit log.

Five gotchas the wizard doesn't surface

What I'd ship before going live

A short pre-flight list specific to agent-identity work, ordered:

Closing

Identity is layer 01 of the five-layer pattern in Making agent deployments pass security review for a reason: get it wrong and every layer above it inherits the wrong audit shape. The four-way decision in this essay is the underlying machinery — Entra workload identities are how you separate "the agent did it" from "the user's agent did it" in the only log that matters during a compliance review.

None of these primitives are new. The OAuth flows are a decade old; managed identities have been GA for years; FIC has been GA since 2022. The novelty is that agents make many requests fast, against scoped data, and the audit shape of those requests is now the deployment-blocking question. The shape is fixable. The fix is upstream of the agent framework.

All writing