Skip to main content
Microsoft Foundry Entra OBO integration
10 min read

Shipping Entra OBO Passthrough on Microsoft Foundry: What It Actually Takes

Most ISVs publishing to Microsoft Foundry Toolbox will wrap a Bearer API key and call it done. Here's why OAuth On-Behalf-Of is the real differentiator, and what it takes to ship it on a vertical SaaS MCP server.

Tom Pinder
Tom Pinder

Microsoft's Foundry Agent Service went GA in March 2026. Three things shipped together: hosted agents with persistent memory, a unified MCP-compatible tool catalog called Toolbox, and OAuth On-Behalf-Of passthrough on the authentication layer.

Most ISVs publishing tools to Toolbox will skip the last one. They'll wrap a Bearer API key, call it done, and move on. That's a mistake, and this post is about why.

We just shipped Entra OBO for IdeaLift's MCP server. The engineering delta compared to a well-executed API key integration is maybe four to six times the effort. The positioning delta is a different product category. That asymmetry is the reason we did it. It's also the reason you should, if you're building anything on Foundry.

Why OBO is the differentiator, not the tool surface

Anyone can publish tools to Toolbox. The MCP spec is well-documented. The catalog listing is a form. The interesting question isn't "what tools does your agent expose." It's "whose permissions is the agent acting under."

A Bearer API key answers that question with "the workspace as a whole." The key grants blanket access to the workspace. That's fine for automations where a single service account owns the integration β€” cron jobs, webhook handlers, Zapier-style flows. It reads wrong in an enterprise agent context, where the calling user is a specific person with specific responsibilities and specific visibility.

If a user has read-only access to a workspace in your UI, and then asks an AI agent a question that routes through the same workspace, the expectation is that the agent's answer is scoped the same way. Bearer API keys can't express that without custom impersonation plumbing.

OBO answers it correctly. The agent presents a token that Microsoft minted on behalf of the specific calling user, scoped to a specific resource API, with specific claims (oid, email, tid, scp) that the resource server validates and uses to build a per-user security context. The agent can only do what the user can do. If the user's role changes, the next token reflects the new role. If the user leaves the tenant, their tokens stop working at the Entra boundary. No custom code.

This is the pattern Microsoft's field organization is actively pitching to enterprise buyers. Foundry Agent Service without OBO is a developer tool. With OBO, it's enterprise-ready infrastructure. ISVs who ship OBO integrations become the reference implementations field sellers can cite. ISVs who don't become generic Toolbox listings.

Dual-auth on the same endpoint

Existing integrations depend on Bearer API keys. Claude.ai's Connector Directory submission uses API keys. Our internal Agent SDK wrapper uses API keys. Customer-facing automations use API keys. Ripping all of that out to go OBO-only would break customers who are doing nothing wrong.

So we kept the API key path untouched and added the OBO path alongside it on the same endpoint. The dispatcher is simple: if the credential matches the API key prefix (il_live_ or il_test_), route to the API key path. If it's JWT-shaped, route to the OBO path. Neither, return 401.

The resulting context object carries an authMode field. Tool handlers that need different behavior branch on that. Most don't need to. The write-tool role gate is a no-op for API key callers; they keep their historical workspace-wide scope. For OBO callers, it enforces a minimum workspace role.

This keeps the "nothing in production changed" property that mattered for rollout, while the new OBO surface gets the stricter per-user semantics it deserves. It also means existing customers see zero disruption, which is the right default for any auth rework on a live product.

Row-level scoping and the UX win we didn't plan for

The original design doc called for role-based scoping on write tools. That landed: create_idea, update_idea, attach_signal, create_relationship require member role or higher. log_decision requires admin. API key callers skip this check entirely to preserve backward compatibility.

What didn't land in the original plan, but became obvious during implementation, was that OBO tokens carry enough identity to populate the actorEmail and actorName columns on the decision-event audit log. Those columns already existed for human-driven UI decisions. They were null for any write coming through the MCP path.

OBO writes populate them for free. Every decision made through an OBO-authenticated agent gets a "decided by X" attribution in the same UI widget that already shows human-attributed decisions. No custom rendering, no special-casing agent actions in the audit view.

This is a small UX thing. It's also exactly the kind of thing enterprise buyers notice when they're evaluating whether to trust an agent integration with their decision history. "Every agent action is attributed to a specific user in the audit log" reads as mature. It's one line of code per write handler plus one column population, but the narrative weight is disproportionate.

Per-user rate limits and session pinning

Two smaller design decisions worth calling out, because they come up during any Foundry ISV security review.

Per-user rate limit sub-bucket. OBO requests get two rate limit checks in series: the workspace-wide bucket (shared with API keys) plus a per-user bucket keyed on workspaceId:userId, sized at half the workspace allowance. This prevents one chatty user from exhausting the workspace's quota for everyone else. We picked a fixed fraction rather than computing it from live member count to avoid a per-request database hit. Fairness is rough, not exact. Fine for current load; tighten later if needed.

Per-user session pinning. MCP's Streamable HTTP transport uses a session ID header. The existing session map pinned each session to a workspace. OBO sessions additionally pin to the user ID. Replay of a session by a different user (even within the same workspace) returns 403 with a "Session user mismatch" error and skips tool dispatch entirely. API key sessions carry null userId and match other API key sessions normally. Zero behavior change for legacy callers.

Neither of these is novel. Both matter when the auditor asks how the integration prevents cross-user confusion.

The GUID URI surprise

Entra tenant policy can block App ID URIs that aren't tied to a tenant-verified domain. We learned this while trying to set a clean URI (api://mcp.idealift.app) on our resource app registration:

All newly added URIs must contain a tenant verified domain, tenant ID, or app ID, as per the default tenant policy of your organization.

We went with the GUID fallback (api://<client-id>) because verifying a subdomain via DNS TXT was overhead we didn't want on the critical path. The functional difference is zero. The aud claim on tokens is either the raw GUID or the api:// URI, and the validator accepts both shapes:

const acceptedAudiences = [appId, `api://${appId}`];
const extraUri = process.env.AZURE_MCP_APP_URI;
if (extraUri) acceptedAudiences.push(extraUri);

The AZURE_MCP_APP_URI env var lets us add a domain-verified URI later without a code change. If a customer tenant admin is reviewing consent screens and cares about the URI looking polished, we verify the subdomain and flip the env var. One deploy, no code. That's the right seam.

The lesson for other ISVs: don't bake the pretty App ID URI into your validator. Accept both forms. You'll hit tenant policies you didn't expect, and the fix should be configuration, not deployment.

The GitGuardian detour

We had a test fixture with a three-segment base64url string starting with eyJ. A syntactically valid JWT, semantically total garbage. The header decoded to {"alg":"RS256"}, the payload to {"sub":"1234"}. It lived in a test file under tests/, surrounded by test-fixture comments, and was used solely to verify that our dispatcher's looksLikeJwt heuristic correctly identified the shape. It was obviously not a real secret to any human reading it.

GitGuardian flagged it. Correctly, if narrowly. Their generic-JWT detector matches on shape, not context. The fix was renaming the fixture to test-fixture-header.test-fixture-payload.test-fixture-signature, which still passes the dispatcher's shape check but no longer starts with eyJ. History was rewritten via a soft reset and single re-commit. GitGuardian re-scanned, passed.

The takeaway: secret scanners run on shape, not semantics. Test fixtures that resemble real credentials will get flagged regardless of how obviously-fake they are. Either use clearly-off-pattern strings in fixtures from day one, or be prepared to dismiss alerts individually. We chose the first. Worth keeping in mind before your PR gets blocked on a false positive you have to explain to security.

Takeaways for other ISVs

Three pragmatic suggestions if you're evaluating Foundry integration.

Ship OBO, not just an API key wrapper. The engineering delta is bounded. Four to six weeks of focused work if you already have a mature MCP server. The positioning delta is large. Microsoft's App Accelerate nomination path exists specifically to reward ISVs shipping the newest enterprise-ready patterns. OBO is one of those patterns. Generic API-key Toolbox listings won't get you into that conversation.

Accept both GUID-form and domain-form App ID URIs in your validator. Tenant policies vary. Hardcoding one shape means you get a mandatory pre-launch DNS verification exercise that doesn't need to block shipping. Build the seam; add a domain URI via env var later.

Document the build in real time. We kept three parallel artifacts during implementation: a raw working journal for future-self debugging, a clean architecture document that reflects the current state, and a public writeup draft that captures narratives as they emerge. The discipline adds roughly 15-20% to build time. The payoff is that the Medium post, the LinkedIn announcement, the one-pager for your ISV manager, and the Toolbox catalog listing copy all derive from material that was written while the decisions were fresh. Post-hoc writeups read as marketing. In-flight journals read as practitioner work. Microsoft's ecosystem and other ISVs can tell the difference.

What comes next

The integration is in production. The architecture doc lives in our repo, the journal is live through the build, and this post is the first public artifact. The conversation with our ISV manager about App Accelerate nomination goes next, and it leads with the technical differentiator rather than the product pitch: here's a vertical SaaS product that shipped per-user OAuth passthrough on Foundry, documented every architectural decision publicly, and is ready to be showcased as a reference implementation of the pattern Microsoft's own field sellers are pitching to enterprise buyers.

If you're an ISV founder evaluating Foundry integration and want to compare notes, get in touch. The pattern is repeatable. The window for early-adopter positioning is roughly the next six to twelve months before the ecosystem saturates.


Technical appendix for developers

If you're building on Foundry Agent Service and want the short version of the implementation shape:

  • Separate Entra app registration for the MCP resource API (not the consumer login app). Expose the scope mcp.workspace (or whatever name fits your domain). Set accessTokenAcceptedVersion to 2 in the manifest. Mark the registration multitenant for cross-customer use.
  • Use JWT validation against the Entra common JWKS endpoint. Accept any tenant-scoped v2 issuer (validate the issuer shape manually so you can emit specific error codes rather than generic claim mismatches).
  • Map the user via the oid claim first (fast path after first successful call), email claim second (legacy users). Auto-provision only if you have a tenant-to-workspace consent mapping in place.
  • Rate limit on two keys in series: the workspace bucket and a per-user sub-bucket. Surface the more restrictive one in response headers.
  • Session state should pin to both workspace and user for OBO; just workspace for API keys (backward compatible).
  • Tool annotations need the full block (readOnlyHint, destructiveHint, idempotentHint, requiresConfirmation) for both Anthropic Connectors Directory submission and Foundry Toolbox catalog listing. Audit every tool.
  • Schema changes: add an AzureObjectId column to your users table and a tenant-to-workspace consent table. Keep both nullable/loose-referenced so you don't collide with existing FK type mismatches.

We'll update this post with the end-to-end smoke test results and Foundry Toolbox catalog submission outcome once those complete.

πŸ†˜

Free Resource

Rescue Your Lost Feature Requests

A 5-step audit to find the ideas hiding in your team chat

Ready to stop losing ideas?

Capture feedback from Slack, Discord, and Teams. Send it to Jira, GitHub, or Linear with one click.