Extend unmanaged OAuth flow to drive code exchange in-process#2896
Draft
trungutt wants to merge 1 commit into
Draft
Extend unmanaged OAuth flow to drive code exchange in-process#2896trungutt wants to merge 1 commit into
trungutt wants to merge 1 commit into
Conversation
1612def to
7bd8d5f
Compare
Adds a new sub-behavior to the unmanaged OAuth flow that lets the
runtime own PKCE / state / DCR / token exchange while the connected
client acts as a thin courier (open browser, receive deeplink,
forward {code, state}).
Activated by a new server-level flag --mcp-oauth-redirect-uri (also
exposed on the runtime as WithUnmanagedOAuthRedirectURI and on the
RuntimeConfig as MCPOAuthRedirectURI). When set, the runtime:
- generates state + PKCE in-process
- runs DCR if needed (or uses per-toolset explicit credentials)
- builds the full authorize URL with the configured redirect_uri
- emits an elicitation whose Meta carries cagent/authorize_url +
cagent/state alongside the existing auth_server_metadata
- accepts {code, state} as a ResumeElicitation payload, verifies
state in constant time, and exchanges the code at the token
endpoint using the same redirect_uri (RFC 6749 §4.1.3 binding)
- stores the resulting token in the keychain with client_id /
client_secret stamped on for later silent refresh
When the flag is empty, the existing client-driven contract is
preserved verbatim: the elicitation carries only metadata and the
client is expected to reply with {access_token, refresh_token, …}.
The {access_token, …} reply shape is also accepted on the new path
so a client that prefers to do the exchange itself is still free to.
A per-toolset RemoteOAuthConfig.CallbackRedirectURL overrides the
runtime-wide flag for that toolset.
Docker-agent never learns anything about the URL it advertises: it
is opaque, and what happens at the URL (HTML bouncer, OS deeplink,
universal-link claim, …) is the host's concern.
Plumbing:
- cmd/root/flags.go: add --mcp-oauth-redirect-uri
- pkg/config/runtime.go: MCPOAuthRedirectURI field
- pkg/runtime/runtime.go: unmanagedOAuthRedirectURI field +
WithUnmanagedOAuthRedirectURI Opt
- pkg/runtime/loop.go: pass through to ConfigureHandlers
- pkg/server/session_manager.go: forward from runtime config
- pkg/tools/capabilities.go: extend OAuthCapable interface +
ConfigureHandlers
- pkg/tools/mcp/mcp.go, remote.go, session_client.go,
builtin/mcpcatalog: implement the new SetUnmanagedOAuthRedirectURI
- pkg/tools/mcp/oauth.go: rework handleUnmanagedOAuthFlow,
factor out resolveClientCredentials helper and
consumeUnmanagedElicitationReply
- docs/features/remote-mcp/index.md: new 'Unmanaged OAuth flow'
section documenting the meta keys and behavior
Tests: six new oauth_test.go tests covering the drive-flow happy
path (incl. asserting RFC 6749 §4.1.3 redirect_uri parity at /token),
state-mismatch rejection, legacy access-token reply on the new path,
legacy mode shape (no authorize_url emitted), legacy mode rejection
of {code, state}, and the per-toolset > runtime-wide precedence.
Signed-off-by: Trung Nguyen <trung.nguyen@docker.com>
7bd8d5f to
c97124d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The current unmanaged OAuth flow (
WithManagedOAuth(false), added in #830 by @rumpl) emits an elicitation carrying only authorization-server metadata and expects the connected client to:state{access_token, refresh_token, …}viaResumeElicitationThat contract works fine when the client is a Go process (the CLI mirror at
pkg/runtime/remote_runtime.go:handleOAuthElicitationreuses the same helpers). It is a poor fit for hosts that want to be thin couriers, such as an Electron app delivering the OAuth callback via a custom URI scheme deeplink: every such host ends up re-implementing PKCE / DCR / token exchange in the host language.This PR extends the unmanaged flow with an opt-in sub-behavior where the runtime drives everything itself and the client just relays the deeplink payload back.
What changes
A new server-level flag —
--mcp-oauth-redirect-uri=<URL>(alsoMCPOAuthRedirectURIonRuntimeConfigandWithUnmanagedOAuthRedirectURIon the runtime). When set,handleUnmanagedOAuthFlow:state+ PKCE in-processclientId/clientSecretfromRemoteOAuthConfig)redirect_uriMetaincludes:cagent/authorize_url— URL the client should open in the browsercagent/state— state value the client must echo backcagent/server_url,auth_server,auth_server_metadata,resource_metadata{code, state}as aResumeElicitationpayload, verifiesstatein constant time, exchanges the code at the token endpoint (sameredirect_uriper RFC 6749 §4.1.3), and stores the resulting token in the keychain with client credentials stamped on for silent refreshWhen the flag is not set, the elicitation shape stays identical to today and the runtime expects the legacy
{access_token, …}reply. CLI-mirror clients continue to work unchanged.The new path also accepts the legacy
{access_token, …}reply, so a client that prefers to do the exchange itself (despite docker-agent offering anauthorize_url) is still free to.A per-toolset
RemoteOAuthConfig.CallbackRedirectURLoverrides the runtime-wide flag for that toolset.What docker-agent does NOT know
Per the OSS/proprietary boundary discussion, none of the host-specific bits leak into the runtime:
Companion changes (other repos)
docker-desktop://...deeplink in Docker Desktop's custom URL handler.Tests
pkg/tools/mcp/oauth_test.gogets six new tests:TestUnmanagedOAuthFlow_DriveFlow_ExchangesCodeForToken— happy path: docker-agent emitsauthorize_url+state, client returns{code, state}, docker-agent exchanges the code (asserts RFC 6749 §4.1.3redirect_uriparity at the token endpoint), token stored with client credentials.TestUnmanagedOAuthFlow_DriveFlow_RejectsStateMismatch— CSRF check: token endpoint must not be hit ifstatedoesn't match.TestUnmanagedOAuthFlow_DriveFlow_AcceptsLegacyAccessTokenReply— back-compat: client may still return{access_token, …}on the new path.TestUnmanagedOAuthFlow_LegacyMode_NoAuthorizeURLInElicitation— when the flag is empty, the elicitation does not carrycagent/authorize_url/cagent/state.TestUnmanagedOAuthFlow_LegacyMode_RejectsCodeStateReply— defensive: a{code, state}reply with no flag set is an error (no stored verifier to exchange with).TestUnmanagedRedirectURI_PerToolsetTakesPrecedence—RemoteOAuthConfig.CallbackRedirectURLoverrides the runtime-wide flag.Existing OAuth tests untouched and still pass.
Backwards compatibility
{access_token, …}reply) is preserved verbatim when the flag is empty.OAuthCapablegains a new methodSetUnmanagedOAuthRedirectURI. All in-tree implementations updated (pkg/tools/mcp/{mcp,remote,session_client}.go,pkg/tools/builtin/mcpcatalog). ExternalOAuthCapableimplementations (if any) will need the same one-line addition; for stdio-style toolsets the implementation is a no-op.pkg/runtime/remote_runtime.go:handleOAuthElicitationis untouched and continues to readcagent/server_urlonly; the new meta keys are additive and harmless to legacy readers.Docs
docs/features/remote-mcp/index.mdgets a new "Unmanaged OAuth flow (server mode)" section describing both sub-behaviors and the elicitation meta keys.