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_grantsrow for (user, agent, gateway) → JWT withsub=user,aud=gateway. - Machine flow:
client_credentials→ no consent, no user → JWT withsub=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
- Topologies: MCP gateway → hidden Mint — the alternative that PRESERVES user identity via token exchange
- Concepts: Grants & flows → Client credentials
- Topologies: Backend service + MCP — pure M2M without a preceding user hop