From caac403a70bab11a280050dc67ad02e148f6dd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20T=C3=B3th?= Date: Wed, 11 Nov 2026 09:53:15 +0100 Subject: [PATCH] docs(rfd): add OAuth authorization for remote Agents RFD Proposes an oauth authentication method type and an auth.oauth client capability so remote Agents can authorize Clients with OAuth 2.1 and recognise them across connections, with transport-level token presentation and a connect ticket for browser WebSockets. --- docs/rfds/remote-oauth-authorization.mdx | 191 +++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/rfds/remote-oauth-authorization.mdx diff --git a/docs/rfds/remote-oauth-authorization.mdx b/docs/rfds/remote-oauth-authorization.mdx new file mode 100644 index 000000000..94db898d0 --- /dev/null +++ b/docs/rfds/remote-oauth-authorization.mdx @@ -0,0 +1,191 @@ +--- +title: "OAuth Authorization for Remote Agents" +--- + +- Author(s): [@akos-zsolt-toth](https://github.com/akos-zsolt-toth) + +## Elevator pitch + +> What are you proposing to change? + +Remote Agents that serve many users need the Client to hold a credential that survives a reconnect. ACP has no standard way to do that today, so every reconnect to a remote Agent becomes a new interactive login. + +We propose: + +- A standard authentication method type, `oauth`, and a Client capability, `capabilities.auth.oauth`, that carries the Client's obligations. +- The method names the Agent's [OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728). The Client discovers the authorization server from it, runs OAuth 2.1 with PKCE, and stores and refreshes the token itself, the same way an [MCP Client authorizes to an MCP server](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization). +- The Client presents the token at the **transport** on every connection: an `Authorization: Bearer` header where it can set headers, and a short-lived, single-use connect ticket in the WebSocket URL where it can't, as in browsers. +- URL-mode elicitation inside `auth/login` stays as the fallback for Clients that don't advertise the capability. + +Nothing changes for local Agents or for stdio. + +## Status quo + +> How do things work today and what problems does this cause? Why would we change things? + +ACP defines two authentication method types ([Authentication](/protocol/v2/authentication#authentication-method-types)): + +- `agent`: the Agent handles login through `auth/login`, for example by opening a local browser and running a local callback. +- `terminal`: the Client runs the Agent program in a terminal, and then reconnects. + +Both assume the Agent runs on the user's machine and keeps credentials where later connections find them. The authentication docs say so: "Whether authentication outlasts the connection is up to the Agent. Credentials are often stored where other connections can use them." For a local Agent that store is the keychain or a config file, shared by every connection from the same machine. + +A remote Agent has no such store to share. To recognise a new connection as a returning user, it needs the Client to present something, and ACP defines nothing for that. In practice a remote Agent: + +1. Answers `auth/login` with a URL-mode [elicitation](/protocol/v2/elicitation) scoped to the request. The user signs in with the Agent's identity provider in a browser. +2. Has to tie the result to that elicitation and that connection, and **MUST NOT** apply it to a different connection, because there is no authenticated user yet ([Elicitation](/protocol/v2/elicitation), URL mode security). That rule is right: it is what OAuth `state` does against login-link phishing. +3. Therefore loses the login when the connection drops. The next connection starts anonymous, and the user signs in again. + +The [remote transport RFD](/rfds/streamable-http-websocket-transport) doesn't close the gap. Clients must keep cookies "for the duration of the connection" and may discard them afterwards. Cookies are also ambient credentials, browsers withhold them from cross-site WebSocket connections under `SameSite`, and every non-browser Client would need its own cookie store. + +What this costs: + +- **Every reconnect is an interactive login.** A network blip or a laptop waking from sleep sends the user back through their identity provider. +- **Agents invent their own workarounds.** Each one is custom, and none has the refresh, revocation or audience binding that a standard token flow provides. +- **Clients can't plan for login.** A generic `agent` method that only works by opening a browser looks the same as one that runs locally. A Client without a browser can't tell in advance that the method will fail. +- **Hosted Agents have no shared answer.** For example, the GitHub Copilot language server has a request to support URL elicitation or a device code for machines with no browser ([copilot-language-server-release#54](https://github.com/github/copilot-language-server-release/issues/54)). ACP gives every hosted Agent the same problem and no standard solution. + +## What we propose to do about it + +> What are you proposing to improve the situation? + +### 1. An `oauth` authentication method type + +```json +{ + "methodId": "sso", + "name": "Sign in with Example SSO", + "type": "oauth", + "resourceMetadata": "https://agent.example.com/.well-known/oauth-protected-resource/acp", + "scopes": ["acp"] +} +``` + +- `resourceMetadata` (required): the URL of the Agent's Protected Resource Metadata document ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)). +- `scopes` (optional): the scopes the Client requests. If they're omitted, the Client uses `scopes_supported` from the metadata. + +The method is meaningful only on HTTP-based transports. Agents **MUST NOT** advertise it over stdio. + +### 2. A Client capability + +```json +{ + "capabilities": { + "auth": { + "oauth": {} + } + } +} +``` + +Supplying `capabilities.auth.oauth` means the Client takes on these obligations: + +- It runs the authorization flow in §3. +- It stores the tokens per Agent resource. +- It refreshes them. +- It presents them on every connection as described in §4. + +Agents **MUST NOT** advertise an `oauth` method unless this capability is present. This mirrors `capabilities.auth.terminal`. A Client that doesn't advertise the capability keeps today's behaviour. + +### 3. The authorization flow + +The Client follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization), with the Agent as the protected resource: + +1. It fetches `resourceMetadata`, and then the authorization server's metadata ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)). +2. It registers or identifies itself the way MCP Clients do: a Client ID Metadata Document, Dynamic Client Registration, or a pre-registered client. +3. It runs the authorization code grant with PKCE ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)), and sends the Agent's resource identifier as the `resource` parameter ([RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707)). A Client without a browser **MAY** use the device authorization grant ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) if the authorization server supports it. Opening a browser follows the existing rule that Clients need the user's consent before opening a URL. +4. It stores the access token and the refresh token, keyed by the Agent's resource identifier. +5. It closes the anonymous connection, opens a new one presenting the token (§4), and resumes any sessions it wants to keep with `session/resume`. This is the same reconnect step as `terminal` authentication. + +The Client **MUST NOT** call `auth/login` for an `oauth` method. No credential travels in an ACP message. + +### 4. Presenting the token + +| Transport | How the token is presented | +| --- | --- | +| Streamable HTTP | `Authorization: Bearer ` on every request, including the GET streams | +| WebSocket, Client can set headers | `Authorization: Bearer ` on the upgrade request | +| WebSocket, Client can't set headers (browsers) | a connect ticket in the upgrade URL, described below | + +**Connect tickets.** The Agent's Protected Resource Metadata advertises `acp_connect_ticket_endpoint`. To connect: + +1. The Client `POST`s to that endpoint with `Authorization: Bearer `. +2. The Agent answers `{ "ticket": "", "expires_in": 30 }`. +3. The Client opens `wss://agent.example.com/acp?acp_ticket=`. + +The ticket rules: + +- A ticket is single-use and expires within 60 seconds. +- It is bound to the token's subject and to the Agent resource. +- The Agent **MUST** reject a reused or expired ticket, and **MUST NOT** log the full upgrade URL. +- The endpoint answers CORS preflight requests for the origins the Agent accepts. + +The ticket is the only credential that ever appears in a URL, and it is worth nothing after first use. + +### 5. Failure, expiry and logout + +- **HTTP:** a missing or invalid token gets `401` with `WWW-Authenticate: Bearer resource_metadata="…"` (RFC 9728 §5.1), as in MCP. +- **WebSocket:** a browser can't read the status code or headers of a failed upgrade. An Agent that receives an invalid or expired token or ticket **SHOULD** therefore accept the upgrade and close the socket with close code `4401`. Browsers can read close codes, so the Client can tell "authorization needed" apart from a network failure. The Client then refreshes the token, or runs §3 again, and reconnects. +- **Expiry during a connection:** the Agent **MAY** close the connection with `4401` when the token's lifetime ends. Agents **SHOULD** avoid this while a prompt is in progress. +- **`auth/logout`:** the Client discards the stored tokens and, if the authorization server supports it, revokes them ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)). The Agent ends the authenticated state of the connection, as it does today. + +### 6. Relationship to elicitation + +URL-mode elicitation is unchanged. On a connection authorized by this method there is an authenticated user, so elicitations use the stronger rule: the user who completes the external step must be the user who started it. The connection-only binding is needed only for Agents without user authentication, and for Clients that fall back to URL-mode login. + +Elicitation also keeps the job MCP gives it: an Agent obtaining access to **third-party** services, such as a source-control host the Agent calls for the user. The Client's token is for the Agent only. Agents **MUST NOT** pass it through to other services. + +## Shiny future + +> How will things will play out once this feature exists? + +- A user signs in to a hosted Agent once, through their organisation's identity provider. After that, reconnects are silent until the refresh token expires or the user logs out. +- Editors that already run MCP's OAuth flow reuse that code for ACP Agents. +- Browser Clients connect with a ticket and never see a long-lived credential in a URL. +- Machines without a browser sign in with a device code. +- Remote Agents stop inventing their own token schemes, and the connection-only elicitation binding becomes a rare fallback. + +## Implementation details and plan + +> Tell me more about your implementation. What is your detailed implementation plan? + +1. Schema: the `oauth` variant of `AuthMethod` and `capabilities.auth.oauth`. +2. Documentation: an "OAuth authorization" section in [Authentication](/protocol/v2/authentication), and a short paragraph in the remote transport RFD that points at it, covering header and ticket presentation and close code `4401`. +3. SDKs: Client helpers for discovery, token storage and refresh, and ticket exchange. The TypeScript SDK's browser WebSocket helper gains ticket support. +4. Prototype: ship the method in an example Agent and Client under a custom `_oauth` type, and standardise the type once that has been exercised. + +## Frequently asked questions + +> What questions have arisen over the course of authoring this document or during subsequent discussions? + +### What alternative approaches did you consider, and why did you settle on this one? + +- **URL elicitation plus an Agent-issued credential.** After the URL login, the Agent returns a credential that the Client stores and presents when it reconnects. This is small and works over any transport. But it invents an ACP-specific token scheme without the standard pieces: expiry, refresh, revocation, audience. It also puts a credential into ACP messages, and it would need an exception to the rule that a pre-login elicitation result never applies to another connection. The Client still has to keep a token store, so it does most of what this proposal does without the interoperability. +- **Keep cookies across connections.** The transport RFD would change "MAY discard cookies" into a requirement to keep them. This is a tiny change, and browsers already behave that way for same-site pages. But cookies are ambient credentials, browsers withhold them from cross-site WebSocket Clients, and non-browser Clients need a cookie store. +- **Pure MCP-style discovery via `401` only.** This works for HTTP, but a browser can't read the response to a failed WebSocket upgrade. Advertising the method in `authMethods` makes discovery work on every transport, from inside the protocol. + +### Why reconnect instead of sending the token on the open connection? + +It keeps credentials out of ACP messages, and the transport is then authenticated before `initialize`. It also matches `terminal` authentication, which already ends with a reconnect. + +### Doesn't MCP's URL-elicitation proposal reject OAuth-specific mechanisms? + +SEP-1036 rejects having the Client obtain **third-party** tokens on the server's behalf, because that leads to token passthrough. It keeps the Client's own authorization to the server in MCP's authorization specification, which is OAuth-based. This proposal follows the same split. + +### Why not carry the token in `Sec-WebSocket-Protocol`? + +Some systems do this to get a credential through a browser. It abuses subprotocol negotiation, the value may be echoed back or logged by intermediaries, and it is a long-lived credential. A single-use ticket avoids all three. + +### Does this conflict with the remote transport RFD? + +No. It builds on that RFD's endpoint and identity model, and could become a section of it. It covers what the transport RFD leaves open: how a remote Client signs in, and how it signs in again after a reconnect. + +### Open questions + +- The names `acp_connect_ticket_endpoint` and `acp_ticket`, and close code `4401`. +- Whether the device authorization grant belongs in the first version. +- Whether `scopes` should be on the method or only in the metadata. + +## Revision history + +- 2026-11-10: initial draft.