Skip to content

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

@pallakatos

Description

@pallakatos

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions