Skip to content

Support Microsoft Entra ID for MCP OAuth - #86

Open
josipmrsic wants to merge 2 commits into
plannotator:mainfrom
josipmrsic:DEVPLAT-1369-entra-compatibility
Open

josipmrsic wants to merge 2 commits into
plannotator:mainfrom
josipmrsic:DEVPLAT-1369-entra-compatibility

Conversation

@josipmrsic

Copy link
Copy Markdown

Hi! We found Artifact Server a few weeks ago and really like it. We're setting it up as a private-team installation for our company (about 300 people), and like many companies we sign in with Microsoft Entra ID.

Browser login with Entra worked out of the box. /mcp didn't, and the deployment guide already says so ("Microsoft Entra ID cannot protect /mcp this way"). This PR closes that gap with three optional settings and one narrower discovery check. Without the new settings, every installation behaves exactly as before.

What stood in the way

We tested against a real Entra tenant with Claude Code as the MCP client:

  1. Discovery omits the PKCE methods. Entra supports S256, but its v2.0 discovery document has no code_challenge_methods_supported, so startup turned MCP OAuth off ("OIDC discovery does not advertise S256 PKCE").
  2. aud is the API's client ID. Entra v2.0 access tokens always name the resource app's client ID, never a URL.
  3. Clients need the API's own scope. Without it, Entra hands out a Microsoft Graph token nobody else can verify, and clients learn the scope from scopes_supported, which wasn't published.
  4. sub is pairwise per app registration. oid is the stable per-person ID in the tenant. With sub, moving to a new app registration would orphan every member binding.

What changes

Setting Default Entra value
ARTIFACT_SERVER_OIDC_SUBJECT_CLAIM sub oid
ARTIFACT_SERVER_OIDC_MCP_AUDIENCE <origin>/mcp the app's client ID
ARTIFACT_SERVER_OIDC_MCP_SCOPES none <origin>/mcp/<scope>
  • Discovery accepts an issuer that leaves code_challenge_methods_supported out. One that lists methods without S256 is still refused, and clients always send S256.
  • A configured audience replaces <origin>/mcp rather than adding to it. The protected-resource metadata still names <origin>/mcp as the resource, since MCP clients check it against the URL they connected to.
  • The subject claim applies to browser login and MCP together, so the oidc:<issuer> + subject binding stays one per person. A missing or blank claim is refused.
  • The new variables join the ARTIFACT_SERVER_OIDC_* family, so the all-or-nothing and WorkOS-exclusion checks cover them. Helm, both Compose files and the .env examples carry them.

One piece of the puzzle needs no code: Claude Code sends <origin>/mcp as the RFC 8707 resource parameter, and Entra refuses a resource that doesn't match the scope's app (AADSTS9010010). Setting the app's Application ID URI to <origin>/mcp fixes that; the new "Use Microsoft Entra ID" section in docs/deployment.md walks through it.

What we deliberately left out

  • No email fallback. Entra access tokens carry no email or email_verified. We kept 0028's rule as it is rather than mapping preferred_username; a person who signed in through the browser once is recognized on /mcp by issuer and subject.
  • Cloudflare. Its worker has no generic OIDC MCP path yet, so we didn't touch its deployment contract.

Spec and tests

  • Decision 0029 records the change and amends the audience rule of 0028 (with a pointer added there).
  • New requirement AUTH-029 (implementing, with a proof gap for deployment evidence). Its tests use the existing stub issuer, which happens to omit the PKCE methods just like Entra, plus an oid claim knob.
    • AUTH-029-B: browser login binding oid, a changed sub keeping one membership, and an email-less access token with the client-ID audience reaching /mcp as the same member.
    • AUTH-029-F: refusal of a missing claim, the default audience beside a configured one, ID tokens sharing the audience, PKCE lists without S256, and malformed configuration.
  • MCP-013's wording now allows the configured audience. Its proof gap says no MCP client had completed browser approval against a generic OIDC issuer; Claude Code just did that against Entra. We left the status alone for you to judge.

Verification

  • pnpm check passes locally.
  • Full verify:iteration tier on our fork, Linux and macOS both green: https://github.com/josipmrsic/artifact-server/actions/runs/37789296735
    That run's branch also carries a one-line update of the pinned Amazon RDS CA bundle checksum. AWS replaced global-bundle.pem on 2026-09-29, and since then the OCI build fails on main with a digest mismatch (your nightly full gate too). We're sending that fix as its own small PR.
  • Manual: Entra browser login, then Claude Code /mcp → authenticate → tool calls, recognized as the same single member.

Happy to rename the variables, split the PR, or change anything else to fit how you'd like this to look.

Microsoft Entra ID could sign people in through the browser but could not
protect /mcp. Four provider facts stood in the way, and each now has an
optional setting or a narrower check whose default keeps today's behavior:

- Discovery omits code_challenge_methods_supported although Entra supports
  S256. An issuer that leaves the field out is accepted; one that lists
  methods without S256 is still refused.
- ARTIFACT_SERVER_OIDC_MCP_AUDIENCE replaces <origin>/mcp as the accepted
  aud value, because Entra v2.0 access tokens always name the API's client ID.
- ARTIFACT_SERVER_OIDC_MCP_SCOPES is advertised as scopes_supported in the
  MCP protected-resource metadata, so clients request the API's own scope
  instead of receiving a Microsoft Graph token.
- ARTIFACT_SERVER_OIDC_SUBJECT_CLAIM binds people by a claim other than sub
  on both paths. Entra's sub differs per app registration; oid does not.

Decision 0029 records the change and amends the audience rule of 0028;
AUTH-029 covers it with a stub issuer shaped like Entra. The deployment guide
replaces "Entra cannot protect /mcp" with the Entra setup, including the
Application ID URI that avoids AADSTS9010010.
The chart now names the subject-claim and MCP settings in its OIDC validation error, so the static Helm check matches the stable tail of the message and also covers an MCP audience without an issuer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant