Skip to content

Slice 4d.5: OAuth On-Behalf-Of (OBO) for MCP outbound auth #319

Description

Summary

Design and implement OAuth On-Behalf-Of (OBO) delegation for the MCP CRD lane (Slice 4d.5). This is the proper long-term replacement for the static-bearer shortcut shipped in Slice 4d.4.1 (spec.bearerFromEnv).

Motivation

Slice 4d.4.1 added static-bearer outbound auth (McpServer.spec.bearerFromEnv). It works, but has a structural problem:

  • Token reuse: a single token (typically COPILOT_GITHUB_TOKEN — the user's Copilot/PAT) is shared across every agent / sub-agent that talks to the MCP server. There is no per-user attribution upstream.
  • Scope inheritance: the upstream MCP server sees the full scope of the token, even if the agent only needs repo:read. Audit logs on the upstream side can't tell which sub-agent triggered which call.
  • Rotation: rotating the token requires a sandbox restart (env var). No short-lived token story.
  • Multi-tenant story: in production AKS with many agents/users, a single shared PAT is a blast-radius problem.

Proposed solution

Per-call OAuth On-Behalf-Of: the inference router exchanges the agent's identity (Workload Identity / federated identity / signed JWT) for a short-lived, narrowly-scoped token issued by the configured oauth.issuer, then attaches that token on the outbound MCP request.

CRD shape

Add a delegation mode discriminator to the existing spec.oauth block:

apiVersion: azureclaw.azure.com/v1alpha1
kind: McpServer
metadata:
  name: github
  namespace: azureclaw-system
spec:
  url: https://api.githubcopilot.com/mcp
  oauth:
    issuer: https://github.com/login/oauth
    delegationMode: on-behalf-of
    # Optional: scope down what the router asks the IdP for
    requestedScopes:
      - repo:read
      - public_repo
    # Cache exchanged tokens per (agent-identity, server) tuple
    tokenCacheTTL: 600s

delegationMode: static-bearer keeps the 4d.4.1 path. delegationMode: on-behalf-of (new) triggers the OBO exchange.

Router-side flow

  1. Sandbox sends tools/call to the local router. Router has the agent's identity (Workload Identity token in /var/run/secrets/azure/tokens/azure-identity-token, or the AGT-issued agent JWT).
  2. Router POSTs to the IdP's token endpoint with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, the agent identity assertion, and scope=<requestedScopes>.
  3. Receives a short-lived OAuth access token scoped to that specific agent.
  4. Caches in-memory keyed by (agent_principal, mcp_server_name) with TTL = tokenCacheTTL.
  5. Attaches Authorization: Bearer <delegated-token> on the outbound MCP request.

Controller-side work

  • Reconcile spec.oauth.delegationMode into meta.json so the router can discover the mode + IdP URL.
  • Validate that delegationMode: on-behalf-of requires oauth.issuer (CEL rule).
  • Existing JWKS discovery already handles the incoming OAuth path — this is only about outgoing.

Per-IdP adapters

Different IdPs have different OBO flavours:

  • Entra IDurn:ietf:params:oauth:grant-type:jwt-bearer + Workload Identity federation.
  • GitHub — no native OBO; need to evaluate whether App-level token exchange or per-user GitHub Apps is the right primitive.
  • Generic OAuth2 — RFC 8693 Token Exchange.

Probably ship Entra ID first (matches our prod target), then evaluate GitHub.

Telemetry / audit

Every OBO exchange + outbound MCP call is audit-logged with:

  • agent principal (sub of the agent JWT)
  • requested scopes
  • granted scopes (from the IdP response)
  • upstream MCP server name
  • tools/call method (so we can correlate to AGT policy decisions)

Out of scope

  • Per-user (human) delegation — agents are non-human principals; if a user wants per-human OBO they can wire that at the agent level.
  • Token refresh — short-lived tokens cached for a few minutes is enough; if expired, just re-exchange. No refresh-token flow.
  • The static-bearer path (Slice 4d.4.1) is NOT removed; it stays as a dev/prototyping shortcut.

Acceptance criteria

  • CRD field spec.oauth.delegationMode: on-behalf-of accepted by admission.
  • Controller propagates the new mode into meta.json.
  • Router exchanges agent identity → IdP token → outbound bearer for Entra ID flow.
  • Cache hit/miss metrics emitted.
  • Live test: palclawe2e agent talks to a configured upstream MCP server using an OBO-issued token; audit log shows the exchanged token's sub matches the agent principal.
  • Static-bearer (4d.4.1) regression suite still green.
  • Doc updated at docs/governance/mcp-outbound-auth.md.

Related

  • Slice 4d.4.1 (static-bearer) — shipped 2026-05-15.
  • inference-router/src/mcp/forwarder.rs — extend with OBO branch.
  • controller/src/mcp_server.rs::McpServerSpec.oauth — add delegationMode field.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions