In this article
July 20, 2026
July 20, 2026

OAuth mix-up attacks and RFC 9207: The issuer check that never made it to token exchange

A four-year-old fix for a classic OAuth attack closed one door. Token exchange left a nearly identical one open, and CVE-2026-59208 walked right through it.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

In our roundup of recent OAuth vulnerabilities, one bug stood out as more structurally interesting than the rest: n8n's token exchange flaw, where a valid, correctly signed token from one issuer could be used to log in as a same-named user under a completely different issuer. We called it a confused deputy problem wearing OAuth clothing. It's worth going deeper, because this exact shape of bug has a name, a history, and a standard that was supposed to have fixed it years ago. That standard just never got extended to cover the flow n8n was using.

What a mix-up attack actually is

Mix-up attacks target OAuth clients that talk to more than one authorization server. That's an increasingly normal setup: an app supporting login through several identity providers, a platform integrating with per-tenant IdPs, or an AI agent that connects to a handful of MCP servers each backed by their own authorization server.

The attack works like this. A client kicks off two authorization flows at once, one against a legitimate authorization server, one against a server the attacker controls (maybe a compromised IdP, maybe one the attacker registered themselves, since some clients let users add arbitrary providers). The client gets confused about which response came from which server. It ends up sending an authorization code, or in some variants a full access token, to the wrong destination. If it sends the legitimate server's code to the attacker, the attacker exchanges it themselves. If it accepts the attacker's code while believing it came from the legitimate server, the attacker can potentially get a token issued under someone else's identity.

The root problem, as the OAuth working group described it when standardizing the fix, is that the standard authorization response never says who sent it. A client gets back a code and a state parameter, both of which look identical no matter which authorization server produced them. Nothing in the response format lets the client verify it's talking to the server it thinks it's talking to.

RFC 9207: The fix, and its two-sided contract

Published in 2022, RFC 9207 adds one new parameter to the authorization response: iss, carrying the authorization server's issuer identifier. When the client gets a response back, it's supposed to compare the iss value against the issuer it expected for that specific flow (something it already knows from the server's discovery metadata) using plain string comparison, and reject the response outright on any mismatch.

The part that gets skipped in practice is the second half of that contract. The RFC doesn't just say "check iss if it's there." It says clients must reject a response that's missing iss when the client already knows the authorization server supports it. Otherwise an attacker can simply strip the parameter and force the client back into the exact ambiguity RFC 9207 exists to remove. Emitting iss on the server side does nothing if the client never enforces the check, or enforces it only when convenient.

It's also worth noting what RFC 9207 deliberately doesn't try to do. The iss parameter isn't cryptographically signed on its own, it's just a query parameter alongside code and state. The RFC's authors point out this is fine given the actual threat model: if an attacker could tamper with the redirect response in transit, they'd already have direct access to the authorization code and wouldn't need a mix-up attack in the first place. The fix targets confusion between servers, not tampering with an honest one.

Four years on, RFC 9207 is still under-deployed. It solves a real problem, cleanly, and plenty of OAuth client libraries still don't check it by default.

Where the fix doesn't reach

RFC 9207 patches the authorization response, which only exists in redirect-based flows: authorization code, implicit, the flows where a browser bounces the user between the client and the authorization server. Token exchange (RFC 8693) doesn't have an authorization response. It's a back-channel grant: one party hands a subject_token straight to a token endpoint and gets a new token back, no browser, no redirect, no iss parameter to check because there's no equivalent message to attach one to.

That doesn't mean the underlying problem goes away. It means the responsibility for solving it moves somewhere else, into the token exchange endpoint's own verification logic, and nothing in the spec forces an implementation to get that part right.

Here's the shape of it. A subject_token is typically a JWT, and JWTs already carry an iss claim identifying who signed them. In principle, an implementation verifying a subject_token should do two things together: confirm the signature validates against a key belonging to a specific, expected issuer, and confirm the token's own iss claim names that same issuer. Do both, and a token from Issuer A can never be mistaken for one from Issuer B, even if both issuers are generally trusted by the system.

What actually tends to get built is a flatter trust model: a pool of trusted signing keys, checked against incoming tokens without partitioning by issuer first. A signature that validates against any key in the pool passes. The sub claim then gets used to look up an account, often across a namespace that isn't scoped to the issuer the token came from. That's precisely the gap CVE-2026-59208 exploited: n8n confirmed a token's signature matched a trusted key, but never confirmed that key belonged to the issuer the sub claim was supposed to come from. A token meant for one tenant matched an account in a different one, because nothing in the verification path made "which issuer" and "which account namespace" the same question.

It's the same failure RFC 9207 fixes in the redirect flow, expressed in a flow the RFC never touches: a party accepting an assertion of identity without confirming which of several trusted parties actually vouched for it.

Diagram comparing two OAuth flows. Top row, 'redirect flow, RFC 9207 added a checkpoint': client starts an auth flow with authorization server AS-1, the redirect response carries both a code and an iss parameter per RFC 9207, the client compares iss to the issuer it expected, and proceeds only on a match, rejecting on mismatch. Bottom row, 'token exchange, no equivalent message to check': a party sends a subject_token to the token endpoint over a back channel with no browser involved, the endpoint verifies the signature against a trusted key, often from a flat pool not partitioned by issuer, but there is no iss parameter in the response to compare, and nothing enforces that check, so a new token gets minted with a possible issuer collision, which is what happened in CVE-2026-59208. A callout below states that RFC 9207 fixes confusion in messages that carry an iss parameter, but back-channel grants like token exchange never had one, so the same issuer binding has to be enforced manually by checking the subject_token's own iss claim.

RFC 8693 isn't unique here either. The JWT bearer grant (RFC 7523) and the SAML 2.0 bearer assertion grant (RFC 7522) have the identical shape: an external assertion comes in, gets verified against a set of trusted issuers, and gets converted into a token. Any system accepting assertions from more than one issuer through any of these grants needs the same issuer-to-key binding that RFC 9207 formalized for the redirect flow, and none of them get it for free.

Why this matters more now, not less

Mix-up attacks were historically a niche concern, most consumer OAuth clients only ever talk to one authorization server. That's changing. MCP-based agent architectures are exactly the multi-authorization-server clients RFC 9207 was written for: an agent routinely connects to several MCP servers, each with its own authorization server, sometimes discovered dynamically rather than configured up front. The client-side half of RFC 9207, checking iss on every authorization response against what was expected for that specific connection, matters more with every MCP server an agent adds, not less.

The back-channel half of the problem scales the same way. Token exchange and delegation patterns for agents (the kind we've covered in our multi-hop delegation and on-behalf-of pieces) depend on exactly the issuer-binding discipline that RFC 9207 never had to formalize for redirects, because back-channel exchange never had an equivalent parameter to check in the first place. More issuers, more tenants, more agents trading tokens on each other's behalf all raise the odds that a verification path checks "is this signed by someone I trust" without also checking "is this signed by the specific someone this claim says it's from."

What to actually check

  • If your client talks to more than one authorization server, confirm your OAuth library enforces RFC 9207. Check the authorization server's metadata for issuer support, and make sure a missing iss parameter gets rejected, not silently accepted, when the server is known to support it. Many libraries still leave this off by default.
  • If you accept assertions from more than one issuer through any back-channel grant (token exchange, JWT bearer, SAML bearer), don't verify against a single flat pool of trusted keys. Partition your key sets per issuer, and check that the key which validated the signature actually belongs to the issuer named in the token's own iss claim.
  • Scope account lookups to the issuer, not just the claim. A sub value should only ever be matched against accounts within the namespace of the issuer that was actually confirmed, never globally. This is the single check that would have stopped CVE-2026-59208.
  • Treat this as a first-class check in agent and MCP architectures, not an edge case. Multi-authorization-server clients are becoming the default topology for agentic systems, and that's exactly the population RFC 9207 was written to protect.

Mix-up attacks got a real fix in 2022. It just didn't travel with the protocol as OAuth grew new back-channel grants for a world of agents, tenants, and token exchange the original authors weren't building for. The lesson generalizes past any one RFC: whenever a system starts trusting more than one issuer, "signed by someone I trust" and "signed by the specific issuer this claim is from" stop being the same question, and the code has to ask both.

If you're managing SSO across dozens of customer identity providers, this is the exact class of problem WorkOS's SSO and AuthKit implementations handle so you don't have to build the issuer-binding logic yourself, for every grant type, for every customer. See how it works.