Guide · Agents
Authentication between MCP servers
When one MCP server calls another, the identity should not disappear at the door. Authfu carries it through the whole chain.
The problem
A single MCP server sitting behind Authfu is straightforward: the caller presents a token, the server verifies it, and it knows who is calling. The moment that server needs to call another MCP server on the caller’s behalf, the question becomes harder. If it forwards the caller’s token, it becomes a confused deputy — a token meant for one server used against another. If it uses its own machine credentials, the person is erased and the downstream server can only see “server A called me,” with no idea for whom or with what authority.
How it works
Authfu implements OAuth 2.0 Token Exchange (RFC 8693). Server A takes the token that was issued to it and exchanges it at the Authfu token endpoint for a new token scoped to server B. The original subject is preserved, the audience becomes server B, and the acting party is recorded in the token’s act claim — which nests, so the full delegation chain is visible.
Server A receives a token issued to it
A caller — a user’s agent, or another server up the chain — presents an Authfu token whose audience is server A.Server A exchanges it for a token scoped to server B
One call to the token endpoint. Authfu preserves the originalsub, sets the new audience to server B, narrows the scopes if asked, and pushes server A onto theactchain.Server B verifies offline and sees the whole chain
Server B validates the RS256 signature against Authfu’s JWKS — no network round-trip to a server — and readssub(the person) plus the nestedactchain (agent → server A) to make its decision.
Call another MCP server
The @authfu/mcp library does the exchange and the downstream request in one call with callMcp. It sets the exchanged token as the outgoing Authorization header and never lets the inbound token leak downstream.
import { callMcp } from "@authfu/mcp";
// Inside server A, handling a request that arrived with the caller's Authfu token.
// server A calls server B on the caller's behalf — one call does the exchange and the request.
const { response, token } = await callMcp({
issuer: "https://authfu.app",
subjectToken: incomingToken, // the token that was issued TO server A
clientId: process.env.AUTHFU_CLIENT_ID!, // server A's own client id
clientSecret: process.env.AUTHFU_CLIENT_SECRET!,
resource: "server-b", // the downstream server's Authfu client id
url: "https://server-b.example.com/mcp",
init: { method: "POST", body: JSON.stringify(rpc) },
// scope: "tools:read", // optional: narrow the token further
});If you want to run the downstream request yourself, exchangeToken returns just the new token:
import { exchangeToken } from "@authfu/mcp";
// Just the exchange, if you want to make the downstream call yourself.
const exchanged = await exchangeToken({
issuer: "https://authfu.app",
subjectToken: incomingToken,
clientId: process.env.AUTHFU_CLIENT_ID!,
clientSecret: process.env.AUTHFU_CLIENT_SECRET!,
resource: "server-b",
});
// exchanged.accessToken is scoped to server-b, on behalf of the original person.What the downstream server sees
Server B protects itself exactly as any Authfu-guarded MCP server does — it verifies the bearer against Authfu’s JWKS. What it gains is the delegation chain: the person on whose behalf the request is ultimately made, and every actor between.
import { verifyAuthfuToken } from "@authfu/mcp";
const identity = await verifyAuthfuToken(bearer, {
issuer: "https://authfu.app",
audience: "server-b",
});
// The person the whole chain acts for:
identity.sub; // e.g. the human's user id
// The delegation chain, outermost actor first:
identity.act; // { sub: "server-a", act: { sub: "agent-…", … } }A working two-server demo — server A calling server B with a real delegation chain — ships in the examples/delegation directory of the @authfu/mcp package.
The guarantees
A server can only re-delegate what was handed to it
The exchange refuses a subject token whose audience is not server A. A server cannot take a token it merely observed and trade it for reach it was never given — the confused-deputy problem, closed at the door.Delegation can only narrow, never widen
The exchanged token carries at most the scopes and roles the original already had. Passing ascopenarrows it further; nothing can add authority a caller lacked.The chain is bounded
Delegation nests, but only so far — a hard depth limit stops an unbounded chain from forming, and every exchange is written to the audit log as person → actor → target.
FAQ
Is this a proprietary extension? No. It is RFC 8693 token exchange over the standard Authfu token endpoint, and the tokens are ordinary RS256 JWTs any compliant verifier can read. The @authfu/mcp helper is convenience, not a requirement.
Do I need agents to use this? No — any two Authfu apps can delegate. It is most useful for agent and MCP-server chains, which is where the extra hops appear.
What does server B have to trust? Only Authfu’s public keys. It never calls server A back, never holds a shared secret with it, and verifies everything offline.