Third-Party Authorization with URL Elicitation
Two different authorizations
Section titled “Two different authorizations”Everything earlier in this module was about the client proving itself to the server. Many real servers have a second problem: to do their job they must call another API on the user’s behalf, and that API has its own login.
| MCP authorization | External (third-party) authorization | |
|---|---|---|
| Who authorizes whom | MCP client → MCP server | MCP server → third-party API |
| Specified by | The MCP authorization spec | The third party’s OAuth, plus MCP’s URL-mode elicitation |
| The server’s role | OAuth resource server | OAuth client of the third party |
| Token used | Issued by the MCP server’s AS, audience = MCP server | Issued by the third-party AS, audience = third-party API |
| Passes through the MCP client? | Yes, that is its purpose | Never |
The temptation is to collapse the two: take the token the client sent and forward it to the third party. That is token passthrough, and it is forbidden. The MCP server MUST obtain its own credentials for the third party, and the mechanism the spec provides is URL-mode elicitation.
What URL-mode elicitation is for
Section titled “What URL-mode elicitation is for”Form-mode elicitation collects structured data through the client; the client sees every field. That is fine for a reservation date and unacceptable for a password. Servers MUST NOT use form mode for secrets, API keys, tokens, or payment details, and MUST use URL mode for them.
URL mode sends the user to a page the server controls. Whatever happens on that page (typing an API key, completing an OAuth flow with a third party, paying) is invisible to the MCP client and to the LLM. The client’s only jobs are to show the user which server is asking and the full URL, obtain consent, and open it safely.
Under MRTR, the request arrives inside an InputRequiredResult:
{ "jsonrpc": "2.0", "id": 1, "result": { "resultType": "input_required", "inputRequests": { "connect_calendar": { "method": "elicitation/create", "params": { "mode": "url", "url": "https://mcp.example.com/connect?flow=calendar", "message": "Connect your calendar account so I can read your events." } } }, "requestState": "v1.AEAD-protected-blob" }}The client answers by retrying the original request with inputResponses and the untouched requestState:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "list_events", "arguments": { "day": "2026-09-10" }, "inputResponses": { "connect_calendar": { "action": "accept" } }, "requestState": "v1.AEAD-protected-blob" }}Note what accept means here: the user consented to open the URL. It says nothing about whether the out-of-band interaction has finished. The server decides that from its own stored state (or the echoed requestState) and either returns the final result or another InputRequiredResult. Clients SHOULD give the user manual retry and cancel controls for exactly this case.
The full flow
Section titled “The full flow”sequenceDiagram participant U as User participant B as Browser participant C as MCP Client participant S as MCP Server participant TA as Third-party AS participant TR as Third-party API C->>S: tools/call list_events (Bearer MCP token) Note over S: no calendar token for this user S-->>C: InputRequiredResult (mode url, /connect, requestState) C->>U: show server name and full URL, ask consent U-->>C: consent C->>B: open https://mcp.example.com/connect C->>S: tools/call retry (accept, requestState) B->>S: GET /connect Note over S: verify session user equals MCP user (sub) S-->>B: redirect to third-party authorize B->>TA: user consents to calendar access for S TA-->>B: redirect to S callback with code B->>S: GET /callback S->>TA: exchange code (S's own client_id) TA-->>S: third-party tokens, stored bound to user S->>TR: read events with third-party token S-->>C: final tools/call result
The implementation pattern, step by step:
- The server (acting as an OAuth client of the third party) prepares an authorization request, but does not hand that third-party URL to the MCP client directly.
- The server stores state binding this elicitation to the authenticated MCP user, and builds a
requestStatethat encodes the original request and, if needed, the user. - The server returns a URL-mode elicitation pointing at its own connect page (
https://mcp.example.com/connect?…). - The user consents in the client, which opens the URL in a secure browser context.
- The connect page verifies the person who opened it is the same user who triggered the elicitation (next section), then redirects to the third-party authorization endpoint.
- The third-party AS redirects back to the server’s registered
redirect_uri; the server confirms that URI belongs to it and exchanges the code using its own staticclient_id. - The server stores the third-party tokens bound to the MCP user’s identity. It is now necessarily stateful for this purpose.
- Later MCP requests from that user use the stored tokens. The MCP client’s own token was never involved.
Three rules the spec makes absolute:
- Third-party credentials MUST NOT transit through the MCP client.
- The server MUST NOT use the client’s MCP token to call the third party (token passthrough).
- The server MUST NOT transmit credentials obtained via URL elicitation back to the MCP client.
And one more that trips people up: URL-mode elicitation is not for authorizing the MCP client to the MCP server. That is the ordinary flow from earlier in this module. Servers MUST NOT rely on URL elicitation to authenticate their own callers.
The phishing attack you must defeat
Section titled “The phishing attack you must defeat”A URL is just a string; whoever receives it can forward it. Without a same-user check, the following works against a perfectly benign server:
- Alice, connected to the benign server, triggers a calendar elicitation.
- The server generates the connect URL for Alice’s pending request.
- Alice’s client shows her the URL. Instead of opening it, she sends it to Bob, another user of the same server, dressed up as “click to connect your calendar”.
- Bob opens it and completes the third-party authorization, believing he is connecting his own account.
- The third-party AS calls back; the server matches the callback to Alice’s pending request.
- Bob’s calendar tokens are stored under Alice’s identity. Alice now reads Bob’s calendar.
Mitigation. The server MUST ensure the user who started the elicitation is the user who completes the authorization. The spec’s non-normative recipe for a web-reachable server:
- Elicit to a server-owned connect page, not straight to the third party’s authorize endpoint.
- On that page, require a session with the MCP server (or its AS), for instance a login cookie.
- Compare the authoritative subject (
sub) from the MCP access token that created the elicitation with the subject of the browser session. Mismatch means stop. - Only then redirect onward to the third-party AS, and bind the eventual tokens to that verified subject.
Servers that cannot use a browser session must find another way to establish that the same person is on both ends, and whatever the mechanism, it MUST be resilient to an attacker who can edit the elicitation URL.
The same identity principle applies everywhere in elicitation: servers MUST NOT trust client-provided identity claims. “I am [email protected]” typed into a form is not authentication; the verified MCP token is.
Safe URL rules
Section titled “Safe URL rules”Because URL elicitation is a legitimate channel for sending users to links, both sides carry obligations that keep it from becoming a phishing primitive.
Servers requesting elicitation:
- MUST NOT put sensitive user information (credentials, PII) in the URL.
- MUST NOT hand out a URL that is pre-authenticated to a protected resource; a malicious client could use it to impersonate the user.
- SHOULD NOT include clickable-looking URLs in any field of a form-mode request.
- SHOULD use HTTPS outside development.
Clients handling URL-mode requests:
- MUST NOT prefetch the URL or its metadata.
- MUST NOT open it without explicit user consent, and MUST show the full URL for inspection first.
- MUST open it in a way the client and the LLM cannot inspect (a system browser or an out-of-process web view such as SFSafariViewController; not an embedded WKWebView the app can read).
- SHOULD highlight the domain to expose subdomain tricks, and warn on suspicious or Punycode hosts.
- SHOULD NOT render URLs as clickable anywhere except the
urlfield of a URL-mode request.
State and storage obligations
Section titled “State and storage obligations”Elicitations do not require server state thanks to MRTR, but third-party authorization does, because someone has to hold the upstream tokens. When you store that state:
- Protect it against unauthorized access.
- Derive the user identity from the MCP authorization credentials (the
subclaim), never from anything the client typed. - Bind every stored third-party token to that identity and look it up only for requests carrying a token with the same subject.
Read together with Threats and Mitigations, this is the confused-deputy story from the server’s side: your server is the deputy, the third-party API is what it acts on, and per-user binding plus the same-user check are what keep it from being confused.