Skip to content

Embedded auth server should support RFC 8693 token exchange so agents can act on behalf of users #5194

@jhrozek

Description

@jhrozek

Description

ToolHive's embedded authorization server today issues JWTs that identify
only the user. In an agentic deployment — where an LLM-based agent
calls an MCP server on a user's behalf — this leaves us with two bad
options at the MCP server / backend:

  1. Pretend the agent is the user. The agent forwards the user's JWT
    verbatim. The MCP server, audit log, and rate limiter see only "Alice".
    An audit entry that reads "Alice deleted the repo" cannot tell whether
    Alice typed the command herself or whether her agent did it after a
    prompt injection.

  2. Treat the agent as the only identity. The agent uses
    client_credentials. Now the user is invisible: per-user policy and
    audit are lost.

We need a token format that carries both identities at once, with a
clear distinction between principal (whose data) and actor (who is
acting on their behalf).

The standard answer

RFC 8693 OAuth 2.0 Token Exchange
solves exactly this. The agent calls the AS's token endpoint with
grant_type=urn:ietf:params:oauth:grant-type:token-exchange, presents
the user's JWT as subject_token, and gets back a delegated JWT:

{
  "iss": "https://auth.toolhive.example.com",
  "sub": "[email protected]",
  "act": { "sub": "coding-agent" },
  "aud": "https://mcp.example.com",
  "exp": 1714000899
}

Backends authorise on sub (the user); audit and per-agent policy use
act.sub (the agent).

Why the existing tokenexchange middleware (RFC-0007) doesn't solve it

pkg/oauth/tokenexchange is outbound middleware: it exchanges the
incoming token at an external token endpoint before forwarding to a
backend. It cannot mint a token that carries act because the external
AS has no notion of our agent. This issue is about extending our own
AS
to do inbound token exchange.

Proposed Solution (TL;DR)

  1. Add the RFC 8693 token-exchange grant to the embedded AS's fosite
    provider. Confidential clients only; lifetime capped at
    min(subject_remaining, configured_max) (default 15m, max 24h).

  2. Support two trust models for the subject token:

    • Same domain — the subject token is an AS-issued JWT (validated
      against the AS's own JWKS).
    • Federated — the subject token is issued by an external OIDC
      provider trusted via a new oidc-trust upstream type. The AS
      validates against that provider's JWKS via OIDC discovery.
    Alice ──► (login) ──► ToolHive AS ──► JWT-A
    Agent ──► (POST /oauth/token, subject_token=JWT-A) ──► ToolHive AS ──► JWT-B
                                                                           (sub=alice,
                                                                            act=coding-agent)
    Agent ──► (Bearer JWT-B) ──► MCP Server
    
  3. Resolve the actor identity in this order:

    • If actor_token is supplied: validate it against the AS's own JWKS
      and enforce actor_token.sub == authenticated_client_id (binding
      check — prevents replay of a leaked actor token by a different
      client).
    • Otherwise, the actor is the authenticated OAuth client.
  4. Extend the Cedar authorizer to read the act claim, so policies can
    write rules like "Alice can write, but coding-agent acting on Alice's
    behalf can only read".

  5. Add operational plumbing for in-cluster IDPs:
    caBundleConfigMapRef (private CA) and allowPrivateIP (cluster
    service IPs) on oidc-trust upstreams.

Scope

  • MCPExternalAuthConfig.spec.embeddedAuthServer.upstreamProviders gains
    a new oidc-trust type.
  • OIDCUpstreamConfig gains optional caBundleConfigMapRef and
    allowPrivateIP fields.
  • EmbeddedAuthServerConfig gains optional delegationTokenLifespan.
  • New package pkg/authserver/server/tokenexchange (handler + multi-issuer validator).
  • Cedar act claim support in pkg/authz/authorizers/cedar.

Strictly additive: deployments that don't configure delegation are
unaffected, and the existing authorization_code, refresh_token, and
client_credentials grants are unchanged.

Out of scope (deferred)

  • Multi-actor chaining (act.act.…).
  • mTLS / SPIFFE-based actor identity (kept as a follow-up; we explored it
    in spiffee-authserver and extracted the OAuth-only path first).
  • CLI-mode (thv proxy) integration.
  • Step-up auth signaling.

Impact

  • Without this, agentic workloads on ToolHive cannot get per-agent
    audit or per-agent authorization on top of per-user identity.
  • Required for the platform-authorization design to express
    agent-scoped Cedar policies.
  • Unblocks the Keycloak + sidecar-agent demo currently held back by
    the same-IDP-only single-identity JWT model.

Related

  • RFC: TBD (link forthcoming) — full design, threat model, and phased
    implementation plan.
  • Builds on: RFC-0019 (auth server), RFC-0031 (embedded AS in proxy
    runner), RFC-0052 (multi-upstream IDP).
  • Counterpart of: RFC-0007 (outbound token exchange middleware).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions