Backend service + MCP (no user)
At a glance. No human, no browser, no consent screen. A backend service authenticates with its own client_id + client_secret and gets a machine token. Standard OAuth 2.1 client_credentials grant (RFC 6749 §4.4). Same audience binding as the user flow — token is bound to the target MCP’s resource URI.
Topology
flowchart LR
Backend["Backend service<br/>(worker, CI, bot)"]
AS["AuthPlane"]
MCP["MCP Server"]
Backend -->|M2M auth| AS
AS -->|JWKS + PRM| MCP
Backend -->|"Bearer(aud=mcp)"| MCP
Two components on the AS side: backend service — a confidential OAuth client with client_credentials in its grant types. MCP server — the Mint Resource. No user. No consent grants stored.
Flow
sequenceDiagram
participant Backend
participant AS as AuthPlane
participant MCP as MCP Server
Backend->>AS: POST /oauth/token<br/>grant_type=client_credentials<br/>client_id=$BACKEND_CLIENT_ID<br/>client_secret=$BACKEND_SECRET<br/>scope=tools/query<br/>resource=https://mcp.example.com/mcp
AS-->>Backend: { access_token, token_type=Bearer, expires_in=3600 }
Backend->>MCP: POST /mcp Authorization: Bearer <token>
MCP->>AS: GET /.well-known/jwks.json (cached)
AS-->>MCP: JWKS
MCP-->>Backend: response
No refresh token — machines re-authenticate every hour (or whatever client_credentials.token_expiry is).
Token claim shape: sub = the backend’s client_id (not a user), client_id = same, aud = target resource URI.
When to use
- A CI/CD pipeline calling MCP tools to trigger builds, run analysis, deploy.
- A backend worker processing a queue, calling MCP tools for summarization / DB queries / notifications.
- An automated agent with no human in the loop.
- A monitoring service calling health-check tools.
Don’t use when:
- A user is in the loop and should authorize → single-mcp with authorization_code.
- The service needs to act on behalf of a user (use the user’s GitHub token) → broker-mcp with token exchange, or the service-account-user pattern (see Concepts: Grants & flows).
How to configure
Enable the grant (disabled by default):
client_credentials:
enabled: true
token_expiry: 1h
Or AUTHPLANE_CLIENT_CREDENTIALS_ENABLED=true.
Register the resource (same as single-mcp):
authserver admin resource create \
--slug my-mcp \
--uri https://mcp.example.com/mcp \
--backend-kind mint \
--scopes 'tools/query||Query tools'
Register the backend as a confidential client:
authserver admin client create \
--name backend-worker \
--grant-types client_credentials \
--auth-method client_secret_post \
--scopes 'tools/query||Query tools'
# → save client_id and client_secret; secret is bcrypt-hashed after this call
Request a token from the backend:
curl -X POST http://localhost:9000/oauth/token \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=tools/query" \
-d "resource=https://mcp.example.com/mcp"
How AuthPlane handles it
TokenService.ClientCredentialsverifiesclient_id+client_secret(bcrypt compare against stored hash).- Scope resolution: if request includes
scope, token getsintersection(requested, client.registered_scopes); if request omitsscope, token gets all client’s registered scopes. - No
consent_grantswrite — machines don’t consent. MintIssuersigns the JWT withsub = client_id, writes anissuancesaudit row.- Optional DPoP binding: if the request has a
DPoPheader, token getscnf.jkt.
Verify
# Confirm the issuance
authserver admin issuance list --client backend-worker --limit 3
# Decode the token — sub should equal client_id
echo "<access_token>" | cut -d. -f2 | base64 -d | jq '{sub, client_id, aud, scope}'
See also
- Concepts: Grants & flows → Client credentials
- Guides: Admin API → Register a client
- Reference: Configuration → Optional grants —
client_credentials.enabled - Topologies: Client-credentials hop — variant where a gateway drops user context on the second hop