# How do I do OAuth token exchange / OBO so an MCP server never uses the user's long-lived token upstream?

**Short answer:** Do not pass the client's bearer token through to GitHub, Slack, your API, or any other upstream. MCP's security best practices call that "token passthrough" and forbid it: an MCP server must only accept tokens issued for that server, then exchange them (RFC 8693 token exchange / on-behalf-of) for a new, short-lived token whose audience is the downstream API. Keep the user's subject, narrow the scopes, put the MCP server (and the agent client) in the act delegation chain, cache the exchanged token until near expiry, and log user id + agent/client id on every tools/call. The user's long-lived refresh or personal access token never leaves the identity provider or the MCP server's own vault.

## Checklist
1. Stop token passthrough: reject inbound tokens whose aud/resource is not this MCP server, and never put that inbound token on an Authorization header to an upstream host (MCP Security Best Practices).
2. Register the MCP server as an OAuth client that can perform grant_type=urn:ietf:params:oauth:grant-type:token-exchange (RFC 8693) or your IdP's documented on-behalf-of / token-exchange profile (Auth0 documents on_behalf_of_token_exchange for MCP → first-party API).
3. On each authenticated tools/call (or on cache miss): exchange subject_token=<user access token for this MCP>, subject_token_type=access_token, audience=<downstream API>, scope=<minimum for this tool>. Prefer resource indicators (RFC 8707) when your AS supports them.
4. Require short expires_in on the exchanged token; cache by (user, agent/client, audience, scope) until shortly before expiry — Auth0's OBO guidance warns that re-exchanging every call burns rate limits.
5. Preserve identity in the issued JWT: same sub as the user, aud set to the downstream API, and an act claim that records the MCP server (and nested client) as actors so audit logs show who acted for whom.
6. Log every upstream call with stable user id, agent or MCP client id, tool name, audience, scopes used, and exchange/cache hit — not the raw tokens.
7. Never store the user's long-lived refresh token in the agent session, AGENTS.md, or a workspace file the model can read; keep refresh material in the MCP server's secret store and rotate on revoke.
8. Fail closed: if exchange fails, audience mismatches, or scopes were reduced below what the tool needs, return an auth error to the agent instead of falling back to a shared service account or the original user token.

## Why is forwarding the user token the wrong pattern?
- MCP Security Best Practices define token passthrough as accepting a client token without proving it was issued for the MCP server and sending that same token to a downstream API. The spec says MCP servers MUST NOT accept tokens that were not explicitly issued for them.
- Passthrough breaks audience separation: rate limits, audit trails and confused-deputy controls that key off aud no longer see the MCP server as the caller. A stolen upstream-shaped token can be replayed against services that never meant to trust the MCP client.
- Client-credentials-only calls as the MCP server (no user subject) are a different anti-pattern: the downstream API loses the end user and cannot authorize per-user. OBO keeps sub=user while minting a new aud for the API.

## What does a correct OBO exchange look like?
- RFC 8693: POST to the token endpoint with grant_type urn:ietf:params:oauth:grant-type:token-exchange, a required subject_token + subject_token_type, and optional audience, resource, and scope. The AS returns access_token, issued_token_type, token_type, and usually expires_in.
- Auth0's MCP example: Token A is issued with aud=MCP server and sub=user; the MCP server exchanges it for Token B with aud=first-party API, same sub, and an act chain naming the MCP server (and nested SPA client). Downstream APIs authorize on the user while logging the actor chain.
- Other IdPs ship their own OBO profiles with different grant names; keep the product rule the same wherever you land: new audience for the upstream API, short lifetime, user subject preserved, no passthrough of the inbound MCP token.
- Per-user, per-agent: key the cache and the audit row on both the human subject and the agent/client id that initiated the tools/call so one compromised agent session does not silently reuse another user's exchanged token.

## What can go wrong?
- The MCP server forwards the browser or IDE token unchanged because "the API already accepts it" — classic passthrough.
- A single long-lived GitHub PAT or Slack bot token is shared by every user of the MCP server, so you cannot revoke one user without killing everyone.
- Exchanged tokens are minted with broad scopes for convenience and live for hours; a leak from logs or memory becomes a wide standing grant.
- Exchange errors are swallowed and the server falls back to a service-account client-credentials token, silently dropping user context.

## Ask Rightshold (free, no key)
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code (one-click setup: https://www.rightshold.com/start?ref=seo), then ask: "Record a signed receipt for this MCP upstream call: hashes of the user id, agent/client id, tool name, downstream audience and scopes I exchanged for — without storing the access tokens."

```json
{
  "tool": "record_ai_run",
  "arguments": {
    "model": "<mcp-server-or-agent-id>",
    "provider": "mcp-obo",
    "prompt_sha256": "<sha256 of user_id|agent_id|tool|audience|scope>",
    "output_sha256": "<sha256 of allow/deny + expires_at>",
    "params": {
      "user_id": "auth0|user123",
      "agent_id": "spa_client_id",
      "tool": "create_issue",
      "audience": "https://api.example.com",
      "scope": "issues:write",
      "decision": "exchanged",
      "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange"
    },
    "ran_at": "2026-11-06T20:00:00Z"
  }
}
```
- Needs an API key. Send hashes only if you prefer; Rightshold stores SHA-256 of prompt/output text. The receipt is Ed25519-signed and ledger-backed so you can prove which user+agent pair drove an upstream call.
- precall_check (free) probes an MCP server's live tools/list fingerprint before you trust it with any OAuth client that can mint OBO tokens.
- set_spend_policy / authorize_spend add a second gate when the exchanged call can spend money; request_human_approval binds a human yes to the exact tool arguments.
- How to scope OAuth tokens per task for AI agents and MCP OAuth wrong-audience tokens cover task-scoped tokens and audience mistakes.

_Rightshold records signed run receipts and spend/approval decisions for agents that call it. It does not run your IdP, perform RFC 8693 exchanges for you, or stop an MCP server that still forwards tokens. Wire exchange in your authorization server and MCP process. Not legal advice._

## Sources
- [RFC 8693: OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693.html)
- [Model Context Protocol: Security Best Practices (token passthrough)](https://modelcontextprotocol.io/docs/2025-11-25/tutorials/security/security_best_practices)
- [Auth0: On-Behalf-Of Token Exchange (MCP server → first-party API)](https://auth0.com/docs/secure/call-apis-on-users-behalf/on-behalf-of-token-exchange)
- [RFC 8707: Resource Indicators for OAuth 2.0](https://www.rfc-editor.org/rfc/rfc8707.html)
- [How to scope OAuth tokens per task for AI agents (Rightshold)](https://www.rightshold.com/answers/how-to-scope-oauth-tokens-per-task-for-ai-agents)

## Related
- [How to scope OAuth tokens per task for AI agents](https://www.rightshold.com/answers/how-to-scope-oauth-tokens-per-task-for-ai-agents)
- [MCP OAuth: wrong-audience tokens](https://www.rightshold.com/answers/mcp-oauth-wrong-audience-tokens)
- [How to give an AI agent a scoped, expiring credential instead of a long-lived API key](https://www.rightshold.com/answers/how-to-give-an-ai-agent-a-scoped-expiring-credential-instead-of-a-long-lived-api-key)
- [How to share one MCP server across a company with per-user auth](https://www.rightshold.com/answers/how-to-share-one-mcp-server-across-a-company-with-per-user-auth)

_Not legal advice. Updated 2026-11-06. JSON: https://www.rightshold.com/answers/how-to-do-oauth-token-exchange-so-an-mcp-server-never-forwards-user-tokens.json_