MP-301h · Module 2
Token Exchange & Impersonation
3 min read
Token exchange (RFC 8693) allows one service to obtain a token for another service on behalf of a user. In MCP architectures, this arises when an MCP server needs to call a downstream API that requires its own authentication. The MCP server exchanges the user's access token for a downstream-scoped token at the authorization server's token exchange endpoint. The downstream token may have narrower scopes (principle of least privilege) and a shorter lifetime. This prevents the MCP server from reusing the user's full-privilege token for downstream calls.
Impersonation vs delegation is a critical distinction. Impersonation means the MCP server acts as the user — the downstream service sees the user's identity directly. Delegation means the MCP server acts on behalf of the user — the downstream service sees both the MCP server's identity and the user's identity. Delegation is almost always the right choice for MCP because it preserves the audit trail: the downstream service knows both who is acting (the MCP server) and on whose behalf (the user). Impersonation erases the MCP server from the audit trail, which violates the principle of attribution.
Token exchange chains can get deep. The user authenticates to the MCP client, which gets token A. The MCP client calls MCP server X, which exchanges token A for token B to call downstream API Y. API Y exchanges token B for token C to access storage service Z. Each hop narrows the scope and shortens the lifetime, but adds latency (each exchange is an HTTP round-trip to the authorization server). Cache exchanged tokens until just before expiry to avoid repeated exchanges. But never cache across users — a token cache key must include both the user identity and the target service.
// RFC 8693 token exchange for downstream API calls
async function exchangeToken(
tokenEndpoint: string,
subjectToken: string,
targetAudience: string,
targetScopes: string[]
): Promise<{ access_token: string; expires_in: number }> {
const res = await fetch(tokenEndpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
subject_token: subjectToken,
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
audience: targetAudience,
scope: targetScopes.join(" "),
requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Token exchange failed: ${err.error_description}`);
}
return res.json();
}