How must an MCP client validate the iss parameter on an OAuth callback?
answer
- bind the request to one server
- string comparison, not URL comparison
- no case folding, no trailing slash
- stored beside the PKCE verifier
- mismatch aborts, never repairs
basics
~20 sBind each authorization request to the issuer that was resolved for it: record that issuer with the request's state, and when the callback carries an iss value, compare it to the recorded string exactly. Any mismatch aborts the flow.
solid answer
~50 sMCP revision 2026-07-28 requires clients to bind an authorization request to a specific authorization server. When the client resolves and validates which issuer it is going to talk to, it records that issuer identifier alongside the per-request material it stores — notably the PKCE verifier — so the two travel together. On the callback, if an `iss` parameter is present (RFC 9207), the client compares it to the recorded issuer by **exact string comparison**. No normalisation of any kind is permitted: no case folding of scheme or host, no eliding a default port, no adding or stripping a trailing slash, no percent-encoding canonicalisation. If the values differ as strings, the flow is aborted rather than repaired. The threat is an authorization-server mix-up, where a response produced by one server is fed into a flow the client intended for another.
code
javascript · 9 linesfunction checkIssuer(recordedIssuer, callbackParams) {
const iss = callbackParams.iss;
if (iss !== undefined && iss !== recordedIssuer) {
throw new Error("issuer mismatch - abort the flow");
}
return true;
}
checkIssuer("https://auth.example.com", { iss: "https://auth.example.com/" });go deeper
Recall the rule itself: the issuer recorded when the request was made and the iss value returned must match as exact strings, and any difference stops the flow.
Explain the mechanics — where the issuer is recorded, when the comparison happens, and which normalisations are forbidden: case folding, port elision, trailing slashes, percent-encoding.
Show you understand the threat and the failure modes: a mix-up between two authorization servers a multi-server client talks to, why library-dependent URL normalisation is itself the vulnerability, and why a mismatch is a hard failure worth alerting on.
Own it as a fleet property: how you ensure every client in your organisation records issuers verbatim, how you keep a URL type from silently rewriting them, and how you tell a genuine attack apart from a provider that changed its issuer string.
## Authorization server binding An MCP client is not a single-tenant OAuth client. It may be talking to many remote MCP servers at once, each pointing at a different authorization server, and it discovers those servers at runtime from metadata rather than from a hardcoded list. That makes it a natural target for a *mix-up*: an attack in which the client is induced to take a response that came from one authorization server and process it as though it belonged to a flow it had started with a different one. MCP's answer is binding. At the moment the client resolves which authorization server a given request will use — and validates that issuer — it records the issuer identifier as part of the state it keeps for that in-flight authorization, together with the PKCE code verifier. The pairing is the point: the verifier is already the thing that proves the callback belongs to this request, and the recorded issuer makes it also prove *which server* the request was for. ## The iss parameter RFC 9207 adds an `iss` parameter to the authorization response, carrying the issuer identifier of the server that produced it. When it is present, the client's job is mechanical: look up the issuer recorded for this request and compare. Equal, continue. Not equal, abort — do not redeem the code, do not retry against the other server, do not attempt to work out which one was 'really' meant. ## Why the comparison must be exact The rule that catches people out is that the comparison is a plain string comparison, with no URL normalisation whatsoever. Specifically prohibited: - **Case folding of scheme or host.** `HTTPS://Auth.Example.com` must not be treated as equal to `https://auth.example.com`. - **Port elision.** `https://auth.example.com:443` must not be treated as equal to `https://auth.example.com`. - **Trailing-slash handling.** `https://auth.example.com/` and `https://auth.example.com` are different strings and therefore different issuers. - **Percent-encoding normalisation.** Decoding or re-encoding octets before comparing is not allowed. This feels wrong to engineers who have internalised that URLs have equivalence rules, so it is worth being explicit about the reasoning. An issuer identifier is an *identifier that happens to look like a URL*, not a URL to be dereferenced and canonicalised. Every normalisation step widens the set of distinct strings that compare equal, and each widening is a place an attacker can try to slip a lookalike value through. Worse, normalisation is implementation-specific: two libraries disagree about edge cases, so a client and a server that both 'normalise' can still reach different conclusions about the same pair of strings. Exact comparison has no edge cases and no library-dependent behaviour, which is exactly what a security check needs. The practical corollary is that you must record the issuer string *as validated*, byte for byte, and never pass it through a URL parser and re-serialise it before storing. A round trip through a URL type is precisely the operation that silently rewrites a trailing slash or lowercases a host. ## What this does and does not protect Binding by issuer defends the client against processing a response from the wrong authorization server. It does not on its own establish that the token you eventually receive is meant for the MCP server you intend to call — audience binding and the rest of the token rules are a separate concern in MCP's OAuth profile. Nor does it substitute for the PKCE verifier, which addresses interception of the code itself. These are layered checks: PKCE ties the callback to this client's request, and the issuer binding ties that request to one specific server. ## Implementation notes Store the issuer in the same record as the verifier and the request state, keyed by whatever value you use to correlate the callback, and give the record a short lifetime. Compare before you do anything else with the response — before parsing the code, before any token request. Treat a mismatch as a hard failure with a security-relevant log entry rather than a recoverable error: a legitimate flow simply does not produce one, so a mismatch means either a serious bug in issuer handling or an actual attempt at a mix-up. Finally, remember that this is a client-side obligation. An MCP client that connects to many servers is the component with the most to lose from a mix-up, and no server-side control can compensate for a client that accepts responses from whichever authorization server happens to answer.
- Why store the issuer next to the PKCE verifier rather than in a separate table?Because they answer two halves of one question and must never drift apart. The verifier proves the callback belongs to this request; the recorded issuer proves the request was for this server. Keeping them in one short-lived record, keyed by the same correlation value, means a callback either matches both or is rejected — there is no state in which one check passes and the other was skipped.
- A trailing slash difference breaks a real login against a legitimate provider. Do you normalise to fix it?No. Fix the source of the string instead: record the issuer exactly as it appeared in the validated metadata, and stop round-tripping it through a URL type that rewrites it. Relaxing the comparison to make one provider work removes the property that makes the check meaningful, and the same laxity then applies to every attacker-supplied value.
- Does this issuer binding also prove the token you get back is for the right MCP server?No — it only proves the authorization response came from the server you intended to ask. Making sure the token is only accepted by the intended MCP server is separate work in MCP's OAuth profile, resting on audience-bound tokens and the resource server rejecting anything not minted for it.
saying these in an interview costs you the question
- Normalises both URLs before comparing them
- Skips the check when iss looks roughly right
- Stores the issuer after parsing it through a URL type
- Thinks PKCE alone rules out an authorization-server mix-up
- Retries against the other server on a mismatch