ULTRATHINK
Solutions
← Back to The Signal
Architecture February 25, 2026

MCP Authentication in Production: Token Exchange, User Impersonation, and the Pattern Nobody Is Talking About

The MCP spec says each server needs its own audience-scoped token. Every blog post explains the requirement. None of them show how to actually build it. We did—with RFC 8693 token exchange, Okta, and Snowflake—and landed on a two-layer model that matches the emerging MCP ext-auth Enterprise-Managed Authorization draft.

Updated April 20, 2026

Nick Amabile
Nick Amabile
Founder & CEO

In our last post, we open-sourced fastmcp-gateway and solved MCP’s context collapse problem—replacing 150+ tool schemas with 3 meta-tools for progressive discovery. That post ended with a “what’s next” section that mentioned access control.

This is that post. And it turned out to be a much harder problem than tool discovery.

Here’s the uncomfortable truth about MCP authentication: the spec tells you what to do, but not how to do it. Section 7.3 of the MCP authorization specification is clear—each MCP server acts as a separate Resource Server with its own audience. Clients must not forward tokens issued for one resource server to another. That’s the requirement.

Now go look at every MCP authentication guide on the internet. Auth0, Stytch, Permit.io, AWS—they all explain the spec requirement. They show you how OAuth 2.1 fits into MCP. They draw architecture diagrams with boxes and arrows. What none of them show is what happens when your MCP server needs to talk to a platform like Snowflake that requires its own OAuth token scoped to the individual user making the request. That’s the gap we’re filling today.

The identity problem nobody is solving

Most MCP servers in the wild use one of two authentication patterns. Both break in production.

Pattern 1: Shared API keys

The MCP server holds a single API key or service account credential. Every request—regardless of which user initiated it—executes as the same identity. A 2025 security report found that over 53% of MCP servers rely on static secrets like API keys and personal access tokens.

This means you can’t enforce row-level security, can’t create per-user audit trails, and can’t revoke access for a single user without rotating the key for everyone. For a Snowflake MCP server, CURRENT_USER() returns the service account for every query. Your CISO will love that.

Pattern 2: Token passthrough

Pass the user’s token straight through from the client to the MCP server to the downstream platform. Simple. And it violates the MCP spec.

The spec is explicit: tokens must be audience-scoped per server. A token issued for your MCP gateway (audience: axon-gateway) should not be accepted by Snowflake (audience: https://myaccount.snowflakecomputing.com). Token passthrough conflates authentication boundaries. One compromised MCP server can replay tokens against every downstream platform. This isn’t theoretical—it’s the exact attack vector the spec was designed to prevent.

What’s needed is a third pattern: the gateway validates the user’s identity, checks their permissions, then exchanges their token for a platform-specific token scoped to the correct audience—on every request. The user’s identity flows through. The token boundaries stay clean. The downstream platform sees the real user, not a service account.

The pattern: two layers, where each belongs

Authorization in the Axon MCP platform lives in exactly two places. Coarse “can this user call this tool” lives at the identity provider—enforced by scopes and app assignments in Okta, Entra, or Google Workspace. Fine-grained “can this user see row X, column Y, record Z” lives in the system of record—Snowflake RLS, Dynamics 365 security roles, Tableau row-level filters, Google Workspace sharing rules. Nothing lives in between. Not in our MCP layer, not in a parallel relationship graph, not anywhere.

This is not an accident. It matches how Doyensec’s 2026 MCP security research and the draft MCP Enterprise-Managed Authorization profile both converge: the IdP is the policy-enforcement point for cross-app authorization; the system of record stays the authoritative place for its own data. An earlier version of this post described a three-layer architecture with a relationship- based access-control (ReBAC) tier in the middle. We removed that tier in April 2026—it was duplicating permissions the backing systems already enforce better. The resulting architecture is simpler, more defensible, and exactly what the MCP spec is converging toward.

fastmcp-gateway (our open-source progressive-discovery gateway for MCP) exposes a lifecycle-hook interface at the request boundary. Authentication, scope enforcement, and downstream token exchange compose there as independent, stackable layers — the exact method names and header contract live in the OSS source and are out of scope for this post. The shape we deploy on the Ultrathink Axon™ platform:

Authentication Call Flow

Client Request

Bearer JWT — aud bound to the gateway’s canonical URI (RFC 8707)

↓

fastmcp-gateway

1

OIDC Authentication

→ IdP JWKS

Validate JWT signature · Verify audience binding · Extract identity claims

↓

2

Scope Enforcement

→ JWT claims

Each tool declares a required scope · IdP grants scopes via app assignment · gateway enforces on every call

↓

3

RFC 8693 Token Exchange

→ IdP token endpoint

Subject token → downstream-audience access token · user identity preserved for the backing system

↓

Downstream-audience access token forwarded

↓

Snowflake MCP Server

OAuth-connect to Snowflake using the exchanged token

↓

Snowflake

CURRENT_USER() resolves to the authenticated user

Lifecycle, at the stage level

Client Request (Bearer JWT)
  ↓
Gateway
  ↓
Stage 1 — Authenticate
  • Validate JWT signature against the IdP's JWKS
  • Enforce audience binding to the gateway's canonical URI (RFC 8707)
  • Reject alg=none; accept asymmetric signatures only (stricter than RFC 9068)
  ↓
Stage 2 — Authorize at the tool level
  • Each tool declares a required scope in its metadata
  • Verify the required scope is present in the JWT
  • 403 insufficient_scope challenge on mismatch (RFC 7235 + RFC 6750)
  ↓
Stage 3 (when applicable) — Exchange for downstream identity
  • RFC 8693 token exchange at the IdP: subject token → downstream-audience access token
  • Downstream-audience access token forwarded to the upstream MCP server
  ↓
Upstream MCP Server
  • Use the exchanged token to OAuth-connect to the backing system as the user
  • CURRENT_USER() / downstream identity resolves to the real human
  • Native row-level security, masking, and role grants all apply

Each layer is independent. Layer 1 (OIDC validation with IdP-granted tool-scope enforcement) is the coarse tool-level authorization tier — the gateway validates the JWT and checks that the required scope for the called tool is present. Stage 3 (RFC 8693 token exchange) is only needed for platforms that support user impersonation; for org-wide app-key downstreams it is bypassed at deployment time. The upstream MCP server itself implements no auth logic — it consumes the exchanged downstream token from the gateway and OAuth-connects to the backing system as the user.

Mode is a deployment-config decision, not a runtime inference. Each tool is declared in its capability manifest as one of three modes: user-impersonation, service-account, or app-key. That declaration is static, versioned, and reviewable — security teams can audit it in a single pass. A user-impersonation tool fails closed if an invocation arrives without a valid exchanged user token; a service-account tool runs on the configured service account regardless of whether any user context came with the request; an app-key tool runs on the shared credential the backing system requires. The presence or absence of a user-identity payload on an individual request is not a mode switch — header presence is evidence the caller is carrying a user token, not permission for the server to pick an auth mode on the fly. The pattern we’re ruling out is the silent-fallback anti-pattern where stripping an identity header causes a user-mode tool to downgrade into a service-account call; an attacker who can strip headers should not also get to choose the auth mode that applies.

Decouple identity propagation from authentication

This is the MCP security pattern that emerged from our production deployments: decouple identity propagation from authentication. The gateway does the hard part — OIDC validation, JWKS caching, scope enforcement, RFC 8693 exchange — and hands the upstream MCP server a verified, downstream-audience-bound token. Upstream servers become thin execution layers: no auth library dependencies, no duplicated validation code, no inference about who the caller is from headers they could forge. The exact header contract and configuration conventions live in the private deployment documentation we share under customer security review, not in public marketing — the architectural principle is what matters here, not the specific wiring.

RFC 8693: The token exchange nobody implements

RFC 8693 defines a standard OAuth grant type for exchanging one token for another. It’s been around since 2020. Identity providers like Okta, Azure AD, and Auth0 all support it. Yet in the MCP ecosystem, almost nobody is using it. The June 2025 MCP spec update even references Resource Indicators (RFC 8707) to prevent token misuse across servers. The November 2025 MCP update (spec 2025-11-25) made audience binding and Protected Resource Metadata (RFC 9728) first-class, with JWT Profile for OAuth Access Tokens (RFC 9068) requiring signed JWT access tokens (the alg: none case is forbidden), mandating support for RS256, and recommending asymmetric signing. Our own implementation goes a step further and allows only RS256 / ES256 — a deliberate strict subset that keeps HS256 out of the chain — but RFC 9068 itself doesn’t outright forbid symmetric algorithms. Token exchange is the production mechanism that glues all of it together.

The RFC 8693 shape itself is straightforward and public — below is the spec-defined request and response, with placeholders instead of our deployment’s specific identifiers. The interesting part isn’t the HTTP wire format; it’s the set of properties the protocol guarantees, and how those line up with what a downstream data platform needs to accept an externally-minted identity token.

The exchange request (from RFC 8693)

POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<user JWT, audience-bound to the gateway>
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&audience=<downstream platform canonical URI>
&requested_token_type=urn:ietf:params:oauth:token-type:access_token

What comes back

{
  "access_token": "<new JWT, downstream-audience-bound>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

// Decoded payload of the new token:
{
  "iss": "<IdP issuer URI>",
  "aud": "<downstream platform canonical URI>",   // re-scoped
  "sub": "<user identifier preserved>",           // identity carries through
  "exp": <expiry>
}

The three properties that make this work as a production identity-passthrough mechanism: (1) the input token is scoped to the gateway, the output to the downstream platform — no ambient cross-scope validity; (2) the user’s sub claim is preserved across the exchange — the downstream platform sees the real human, not a service account; and (3) the IdP is the thing doing the minting, so it can enforce admin-configured policy (which users are allowed to reach which downstream platforms) at the exchange boundary.

The observable result: CURRENT_USER() at the data warehouse resolves to the actual human who invoked the agent, not a shared service account. Row-level security policies, masking rules, role grants, and audit trails all behave exactly as they would for that user running the same query through the warehouse’s native UI. The AI agent inherits the user’s permissions — it doesn’t establish its own, and there is no service-account escalation path.

What implementing this actually takes

RFC 8693 is elegant on paper. Taking it from spec to production — with a real enterprise IdP, a real data warehouse, and a real security team signing off — has sharp edges. The specifics of those edges are what we share under customer security review, not in marketing copy. But the categories of work are worth describing, because if you’re sizing the effort they’re what you’ll budget for. These are the three that consistently surprise teams the first time through.

1. IdP actor/subject app modeling

RFC 8693’s on-behalf-of pattern requires the identity provider to distinguish between the application the user logs into (the subject) and the machine identity that performs the token exchange (the actor). Both Okta and Microsoft Entra enforce this distinction at the app-registration level, and the mapping is non-obvious: the wrong combination of app types or grant types produces confusing invalid_grant errors that look like client-secret problems but aren’t. Expect to spend real time on the IdP admin console modeling this correctly the first time through; after that, it’s one-time tenant setup and the pattern is identical across customers on the same IdP vendor.

2. Downstream platform trust configuration

Each downstream data platform accepts externally-minted identity tokens through its own integration surface: Snowflake exposes External OAuth, Tableau has Connected Apps, BigQuery uses Workforce Identity Federation, and Dynamics 365 uses Azure AD on-behalf-of. Each of these maps an IdP-issued claim (usually sub) onto the platform’s native user identifier so that existing row-level security, role grants, and masking policies resolve to the actual human. The vendors document the SQL, CLI, or API configuration well; what they don’t document is that every platform’s claim-mapping assumptions are slightly different, and the order of debugging matters — IdP misconfiguration and downstream-platform misconfiguration fail in very similar-looking ways at the token-validation boundary.

3. Per-platform audience binding (RFC 8707)

Every downstream platform has to receive a token that is audience-bound specifically to it, not to your gateway. RFC 8707 Resource Indicators is the mechanism; both Okta and Entra support it, but the IdP-side app configuration has to expose each downstream’s canonical URI as a legitimate audience the client is allowed to request. Missing an audience binding means the downstream accepts tokens that weren’t meant for it — that is the confused-deputy vulnerability MCP spec §7.3 specifically calls out. We validate audience binding explicitly on every token we mint and every token we accept; so should every production MCP deployment.

On specifics. Our own configuration — the exact IdP app types and names, the exact claim mappings, the exact downstream audience identifiers — is part of our customer deployment documentation, not marketing material. Publishing a full deployment recipe helps attackers reconnoiter tenants that deploy Axon. The pattern above is the shape every production implementation of the MCP spec will take; the specifics belong in security reviews under NDA.

 

For readers who want to dig into the vendor-side specifics: Okta’s token-exchange guide, Microsoft Entra’s on-behalf-of flow, and Snowflake External OAuth are the authoritative references. The work of composing those pieces into a coherent production deployment — the thing that actually lets a CIO sign off — is what a Pathfinder or Build engagement produces, and it’s what our Ultrathink Axon™ platform automates away so new tenants don’t rediscover these edges from scratch.

Explicit mode gating, not silent fallback

Not every call needs user impersonation. Scheduled batch jobs, health probes, and admin maintenance tasks legitimately run as a service account — that’s what a service account is for. Org-wide app-key downstreams (public-data services, enrichment APIs, anything without a per-user identity model on the other side) have no user to impersonate at all. And user-invoked agent tool calls against identity-aware platforms absolutely need the user’s own credentials flowing through. Three different legitimate modes, and a production system has to handle all three — without the edges between them becoming ambiguous.

The rule we settled on, which we recommend for any MCP production deployment: mode is declared per-tool at deployment configuration time, not inferred at call time from whether an identity header happens to be present. A user-impersonation tool fails closed if the expected downstream identity isn’t available; a service-account tool runs on the configured service account; an app-key tool runs on the shared app credential. The mode choice is static, auditable, and visible to security review. Silent fallback between modes is the anti-pattern we’re ruling out — it’s how an attacker who can strip a header ends up running queries against the service account instead of the user they were pretending to be.

Authorization: at the system of record, not in the middle

Authentication tells you who the user is. Authorization tells you what they can do. The question is: where should the “what they can do” decision actually live? We used to answer that with a relationship-based access-control tier in the gateway itself. We no longer do. The decision lives in two places, and neither is our code.

  • 01 Coarse “can call this tool” lives at the identity provider. Each tool declares a required scope; the IdP (Okta, Entra, Google Workspace) grants that scope to the user via app assignment or group membership; the user’s JWT carries the granted scopes; the gateway verifies the scope is present on every call and returns 403 insufficient_scope if not. No runtime round-trip to a separate policy engine, no drift-prone permission copy, no mid-tier cache to invalidate when an employee changes roles. The IdP is already the source of truth for employment status and group membership; making it the source of truth for tool authorization too removes the second copy.
  • 02 Fine-grained “can see row X, column Y, record Z” lives in the system of record. Snowflake row-level security, Dynamics 365 security roles, Tableau row-level filters, Google Workspace sharing rules—these are already the authoritative place for that decision. RFC 8693 token exchange puts the user’s identity in front of that system, and the system applies the same policy it would for a human analyst logging in directly. We do not replicate, cache, or second-guess it.

This matters for production AI posture in two ways. First, there is no parallel permission graph to keep in sync with the backing system—no drift, no stale grants, no silent divergence when a user is offboarded in HR but the mid-tier still says they can see the North region. Second, when a customer’s security team asks “what happens if your MCP layer is compromised?” the answer is: the attacker still only sees what the authenticated user’s Snowflake/D365/Tableau permissions would have shown them. The MCP server cannot widen what the downstream already enforces.

Most MCP servers in the wild do the opposite: shared service accounts, blanket API keys, or a bolted-on RBAC layer that drifts from the backing system within weeks. The OWASP MCP Top 10 lists MCP07 Insufficient Authentication & Authorization and MCP10 Context Injection & Over-Sharing as the two most common enterprise failure modes. Delegating to the system of record makes both structurally harder to trigger.

Why this is a platform, not a per-team rebuild

The whole pattern above — IdP audience binding, scope enforcement, RFC 8693 exchange per downstream, explicit per-tool mode gating, token caching to keep the exchange out of the hot path — is configuration-driven in the Ultrathink Axon™ platform. Adding support for a new downstream data system in a customer deployment is a capability-catalog entry and a configuration bundle, not an engineering project. The upstream MCP servers stay thin; the identity-handling work that would otherwise live in every server lives in the platform layer exactly once.

That’s the practical reason this pattern — as opposed to the pattern most teams end up with on their first attempt (shared API keys in Kubernetes Secrets, bespoke middleware copy-pasted across four MCP servers, identity inference from whichever header happens to be populated) — is sustainable at enterprise scale. A company building AI agents against ten downstream systems does not want ten different token-exchange implementations, ten different mode-gating decisions, ten different fallback behaviors to audit. It wants one platform that gets this right once, and ten thin adapters sitting on top.

Token caching is the one implementation detail worth calling out because it’s the difference between “token exchange is a great pattern” and “token exchange adds 100ms to every agent tool call.” Exchanged tokens cache by the tuple of user identity and downstream platform, with TTL bounded by the IdP-issued token’s exp claim. For a user making ten queries against the same warehouse in a session, only the first triggers an IdP round-trip; the other nine reuse the cached downstream token. Per-request auth overhead amortizes to near-zero after the first call.

Worth being precise about invalidation because it’s often overclaimed in vendor marketing: cache invalidation is expiry-driven. A cached exchanged token remains valid until it expires — grant changes made at the IdP (a user losing a group membership, a scope being revoked, a role reassignment) do not retroactively invalidate already-cached tokens. The operational control is the TTL: short TTLs bound the staleness window (we default to minutes, not hours). Deployments that need tighter propagation layer on either push-based revocation from the IdP or periodic token introspection against the IdP’s revocation endpoint — both are industry-standard patterns, and both are on our roadmap beyond the short-TTL default. We call this out here because claiming “cache invalidates instantly on grant change” is a common security overclaim we want no part of.

Why this matters for enterprise AI

AI agents are getting access to production data platforms. Snowflake, BigQuery, Databricks—these aren’t sandboxes. When an AI agent runs a SQL query against your data warehouse, the question isn’t whether it needs authentication. The question is whether you can trace that query back to the specific human who asked for it.

Shared service accounts make that impossible. Token passthrough violates the MCP spec and creates replay attack vectors. The only pattern that satisfies the spec, enables per-user audit trails, and preserves token boundary security is token exchange. An emerging IETF standard—the Identity Assertion Authorization Grant (ID-JAG)—is now formalizing this exact pattern with enterprise IDP governance built in.

This is the same pattern we see across the AI Execution Gap. The technology exists—RFC 8693, OIDC, RFC 8707 audience binding, Snowflake External OAuth. The spec defines the requirement. But the production implementation—the wiring, the gotchas, the dual-app Okta configuration, the header contract between gateway and upstream—is where the real work lives.

Production AI needs production identity. Not API keys duct-taped to MCP servers. Not tokens passed through without audience validation. Real identity propagation—from the human asking the question, through the AI agent, through the gateway, to the data platform executing the query. Every step auditable. Every token scoped. Every permission checked.

fastmcp-gateway handles the discovery layer. The hook system handles authentication, authorization, and token exchange. If you need the full stack—strategy, platform, and the Outcome Partnership to run it over time—start the conversation.

This is Part 2 of our MCP gateway series. See Part 1: Why Your AI Application Needs an MCP Gateway. For more on building production-grade AI systems, see The Modern AI Application Stack and Production Observability with Logfire.

Where the spec is heading: XAA and ID-JAG

Everything above describes the production pattern we shipped. But the standards world is catching up. The IETF’s Identity Assertion Authorization Grant (ID-JAG) draft (revision 01 at time of writing—check the datatracker for the latest) formalizes what we built—RFC 8693 exchange at the IDP—but adds an enterprise governance layer on top. The IDP doesn’t just issue tokens. It decides whether the exchange is allowed, based on app assignments and enterprise policy. The broader framework around this is called Cross-App Access (XAA).

The ID-JAG flow has two client-visible token endpoints and three parties: an identity provider, a resource authorization server, and a resource server. First, the client performs an RFC 8693 token exchange at the IdP — exactly what our platform’s exchange layer does today — producing an ID-JAG (Identity Assertion Authorization Grant). The IdP evaluates whether the requesting app is authorized to act on behalf of the user for the target resource and embeds that determination in the grant. Second, the client presents the ID-JAG to the resource authorization server’s token endpoint as an RFC 7523 JWT-bearer authorization grant; the resource authorization server validates the grant’s signature, issuer, audience, and policy scope and issues a resource-scoped access token that the client then uses to call the resource server itself. Two token endpoints, one continuous policy decision — and the IdP becomes the anchor point for “is this app allowed to act as this user against this resource at all?” rather than just a token issuer.

Our implementation is already shaped like ID-JAG. The RFC 8693 exchange we perform at the gateway is the first half of the ID-JAG flow. Coarse authorization—whether a user can call a given tool—already lives at the IdP via scopes granted through app assignment. That is precisely the governance role ID-JAG formalizes. When ID-JAG reaches GA in identity providers, the migration path is a cleaner JWT-bearer grant on the outbound leg, not a rewrite of where the authorization decision lives. The decision has lived at the IdP the whole time.

The MCP specification itself incorporated XAA as “Enterprise-Managed Authorization” in its November 2025 extension. Okta’s XAA Phase 1 is in Early Access (check Okta’s XAA page for current availability). No MCP client SDKs implement it yet. The spec is still early, but the direction is clear: enterprise identity providers will govern cross-application token exchange, and RFC 8693 is the mechanism underneath.

Track the spec at the IETF datatracker and explore the reference playground at xaa.dev.

Further reading

The canonical specs and independent research behind this architecture, in the order we think is most useful to read them.

Specs and drafts

Independent security research

More from The Signal

Ready to Close the Execution Gap?

Take the next step from insight to action.

No sales pitches. No buzzwords. Just a straightforward discussion about your challenges.