MCP gateway → hidden Mint

At a glance. A gateway sits between agents and hidden downstream MCP infrastructure. Unlike client-credentials-hop, user identity IS preserved on the second hop via RFC 8693 token exchange, and the downstream sees an act-claim chain naming the gateway. Uses fronting-links — an operator-vouching declaration that bypasses the runtime-client-binding gate for gateway-fronted patterns.

Topology

flowchart LR
    User["User"]
    AS["AuthPlane"]
    Agent["Agent"]
    Gateway["Gateway<br/>(MCP)"]
    Hidden["Hidden Mint<br/>MCP<br/>(invisible to user)<br/>aud=hidden-mint<br/>sub=user<br/>act.sub=gateway"]

    User -->|consent| AS
    AS --> Agent
    AS --> Gateway
    Agent -->|"user token<br/>aud=gateway"| Gateway
    Gateway -->|"RFC 8693 ex."| Hidden

Two Mint Resources — visible gateway and hidden downstream. A fronting-link declares that gateway is authorized to front tokens to downstream. Token on the second hop preserves sub=user but adds act.sub=gateway_id.

Issued-token shape (Option β)

The exchanged token that hits the hidden Mint:

{
  "sub": "user-42",                    // preserved
  "aud": "https://hidden-mint.internal/api",
  "client_id": "<source resource slug>",   // fronted-source, not gateway
  "act": {
    "sub": "<gateway_client_id>",      // gateway is the acting party
    "actor_type": "agent"
  },
  "scope": "internal/read"
}

sub = original user (audit trail). act.sub = the gateway that made the exchange call (who to hold accountable if the second hop misbehaves). client_id = the fronted-source slug (per Option β — see Concepts: Architecture § Unified Resource model).

Flow

sequenceDiagram
    participant Agent
    participant Gateway
    participant AS as AuthPlane
    participant Hidden as Hidden Mint

    Agent->>AS: user auth-code → user token (aud=gateway)
    Agent->>Gateway: POST /mcp Bearer <user token>
    Gateway->>Gateway: validates the user token
    Gateway->>AS: POST /oauth/token<br/>grant_type=urn:ietf:params:oauth:grant-type:token-exchange<br/>subject_token=<user token><br/>resource=hidden-mint ← target<br/>client_id=$GATEWAY_ID<br/>client_secret=$GATEWAY_SECRET
    AS->>AS: checks fronting_link(gateway → hidden-mint) exists → bypass runtime-client-binding gate
    AS-->>Gateway: exchanged token: sub=user, aud=hidden-mint, act.sub=gateway
    Gateway->>Hidden: POST /api Bearer <exchanged token>
    Hidden->>Hidden: validates the exchanged token
    Hidden->>Hidden: executes with user identity + gateway act-chain visible

When to use

  • Multiple agents access the same internal infra through one boundary (the gateway).
  • You want user identity preserved on the second hop (unlike client-credentials-hop).
  • You want the audit trail to name BOTH user AND gateway (act-chain).
  • Downstream MCPs shouldn’t be directly reachable from agents.

Don’t use when:

How to configure

CLI:

# 1. Register both Resources
authserver admin resource create --slug gateway --uri https://gw.example.com/mcp --backend-kind mint --scopes 'tools/read||Read tools'
authserver admin resource create --slug hidden-mint --uri https://hidden.internal/api --backend-kind mint --scopes 'internal/read||Internal read'

# 2. Create the fronting link — gateway may front tokens to hidden-mint
authserver admin fronting create \
    --source gateway \
    --target hidden-mint \
    --scope-map "tools/read:internal/read"

# 3. Register the gateway as a confidential client with token-exchange
authserver admin client create \
    --name gateway \
    --grant-types authorization_code,refresh_token,urn:ietf:params:oauth:grant-type:token-exchange \
    --auth-method client_secret_post \
    --scopes 'tools/read||Read tools' \
    --scopes 'internal/read||Internal read'

--scope-map on the fronting-link tells AuthPlane how to map source-side scopes to target-side scopes during the exchange. Format: SOURCE:TARGET,SOURCE:TARGET. CLI caveat: the --scope-map flag splits on the FIRST :, so source-side scope names that themselves contain : (like tool:list) can’t be expressed via CLI — use REST/YAML for those.

Enable token-exchange at boot: AUTHPLANE_TOKEN_EXCHANGE_ENABLED=true.

How AuthPlane handles it

  • Standard user auth-code flow issues a token with aud=gateway.
  • On the exchange call, TokenExchangeService looks up fronting_links(source=gateway, target=hidden-mint). Found → bypass the runtime-client-binding gate (that gate lives on the broker dispatch path — see Guides: Runtime client binding).
  • MintIssuer signs the exchanged token per Option β shape: sub preserved from subject, client_id = fronted-source slug, act.sub = the exchange caller’s client_id.
  • issuances audit row records both parties + the exchange chain.

Multi-hop is supported — chained exchanges (A → B → C) nest act claims. See Concepts: Delegation & act-chain.

Verify

# Confirm the fronting link
authserver admin fronting list
# → gateway → hidden-mint, scope-map: tools/read:internal/read

# Trigger an exchange and inspect the resulting act chain
authserver admin issuance list --client gateway --limit 3
# → one row with sub=user aud=hidden-mint act.sub=gateway

echo "<exchanged_token>" | cut -d. -f2 | base64 -d | jq '{sub, aud, client_id, act}'

See also