Client-credentials hop

At a glance. A gateway sits between the user’s agent and hidden downstream infrastructure. The first hop is user-authenticated. The second hop is the gateway acting as itself via client_credentials — deliberately dropping user context. Use when the downstream service doesn’t need to know which user triggered the call.

Topology

flowchart LR
    Agent["Agent"]
    Gateway["Gateway<br/>(MCP, M2M client)"]
    Backend["Hidden backend<br/>(Mint)"]
    AS["AuthPlane"]

    Agent -->|"Bearer<br/>aud=gw<br/>(user)"| Gateway
    Gateway -->|"Bearer<br/>aud=backend<br/>(gateway as itself)"| Backend
    Gateway -->|JWKS + PRM| AS
    Backend -->|JWKS + PRM| AS

Two Resources: gateway (agents authenticate against this) + hidden backend (only the gateway reaches this). Gateway is BOTH a Resource AND an OAuth client with client_credentials.

Flow

sequenceDiagram
    participant Agent
    participant Gateway
    participant AS as AuthPlane
    participant Backend

    Agent->>AS: user auth-code flow → token with aud=gateway
    Agent->>Gateway: POST /mcp Authorization: Bearer <user-token, aud=gateway>
    Gateway->>Gateway: validates the user token (JWKS cached)
    Gateway->>AS: POST /oauth/token<br/>grant_type=client_credentials<br/>client_id=$GATEWAY_ID<br/>client_secret=$GATEWAY_SECRET<br/>resource=hidden-backend<br/>scope=internal/write
    AS-->>Gateway: machine token (aud=hidden-backend, sub=gateway_id)
    Gateway->>Backend: POST /internal Authorization: Bearer <machine token>
    Backend->>Backend: executes as authenticated caller = the gateway
    Gateway-->>Agent: response

User context is intentionally lost on step 4 — the downstream sub claim is the gateway’s client_id, not the original user. If the backend needs the user identity, use mcp-gateway-mint with token exchange instead.

When to use

  • Downstream infrastructure only cares “is this call from the gateway”, not which user triggered it.
  • Gateway is a trusted boundary and hides the user identity from downstream by design.
  • Downstream systems can’t handle per-user tokens (legacy internal APIs, aggregators).
  • Compliance says user identity shouldn’t traverse the internal boundary.

Don’t use when:

  • Downstream needs the user identity (for per-user auth, quotas, or audit) → mcp-gateway-mint with token exchange preserves sub.
  • Downstream is a Broker resource vending upstream tokens → mcp-gateway-broker.
  • You want the second hop audited per-user in AuthPlane → mcp-gateway-mint.

How to configure

CLI:

# 1. Enable client_credentials at boot
export AUTHPLANE_CLIENT_CREDENTIALS_ENABLED=true

# 2. Register the hidden backend as a Mint Resource
authserver admin resource create \
    --slug hidden-backend \
    --uri https://backend.internal/api \
    --backend-kind mint \
    --scopes 'internal/write||Internal write'

# 3. Register the gateway (Resource AND confidential client)
authserver admin resource create \
    --slug gateway \
    --uri https://gateway.example.com/mcp \
    --backend-kind mint \
    --scopes 'tools/read||Read tools' \
    --scopes 'tools/write||Write tools'

authserver admin client create \
    --name gateway \
    --grant-types authorization_code,refresh_token,client_credentials \
    --auth-method client_secret_post \
    --scopes 'tools/read||Read tools' \
    --scopes 'tools/write||Write tools' \
    --scopes 'internal/write||Internal write'

The gateway client has BOTH grant types because it acts as an OAuth server for agents (authorization_code) AND as a client against the hidden backend (client_credentials).

How AuthPlane handles it

Two independent token flows share no state:

  • User flow: standard authorization_code → consent_grants row for (user, agent, gateway) → JWT with sub=user, aud=gateway.
  • Machine flow: client_credentials → no consent, no user → JWT with sub=gateway_id, aud=hidden-backend.

The backend audit shows the gateway as the caller. To attribute a specific request to a user, correlate via trace_id (OTEL) or request_id — both hops carry them.

Verify

# Two separate issuances per agent tool call — user token and machine token
authserver admin issuance list --client gateway --limit 5
# → one with sub=user-42 aud=gateway (from step 1)
# → one with sub=gateway aud=hidden-backend (from step 4-5)

# Decode both — user identity is on the first, gateway identity is on the second

See also