feat(oauth): optional post-login redirect after the proxy callback#978
Merged
Conversation
When oauth.mcpClient.postLoginRedirectUrl is set, a successful OAuth proxy callback redirects the browser to that URL with the connected server's name appended as a server query parameter, instead of rendering the static success page. This lets a front-end (e.g. a chat gateway) observe connector login completion and update its own UI. The target is operator config, never request input, and must be an absolute http(s) URL; an invalid value is ignored with a warning. Failed callbacks still render the error page.
Auth-challenge login URLs now point at a muster-hosted start endpoint (/oauth/proxy/start) that redirects to the upstream authorization URL stored with the flow's state. The initiating caller may append a redirect query parameter to the start URL; when it matches an entry of the new oauth.mcpClient.postLoginRedirectAllowlist (exact scheme and host, path prefix), a successful callback redirects the browser there with the connected server's name appended as a server query parameter, instead of rendering the static success page. The redirect is opt-in per flow, so front-ends like klaus-gateway can observe connector login completion for the flows they initiated while other clients of the same muster (Claude Code, Backstage AI) keep the static success page. A rejected or absent redirect target never blocks the login; failed callbacks always render the error page.
The simulated browser flow parsed OAuth parameters straight off the core_auth_login URL, which is now the muster-hosted start endpoint carrying only the state. Dereference it one hop, like a browser, and continue with the upstream authorization URL from the Location header.
The OAuth browser callback connects the backend outside any MCP request, so a client that already listed and cached its tools never learns the backend became available and has to re-run core_auth_login manually. Push the same subject-wide list_changed notifications the SSO connect path sends.
Reject dot segments (plain and %2e-encoded) and require the allowlist entry's path to be extended at a segment boundary, matching on the escaped path so percent-encoded slashes cannot fake a boundary. Browsers normalize dot segments before navigation, so the previous plain prefix match effectively allowed any path on an allowlisted host.
SET on update now uses XX so it only overwrites a key that still exists. Without it, an update racing the callback's consuming GETDEL recreated the key, and KEEPTTL on a fresh key means no expiry.
GenerateState now takes a build callback invoked with the encoded state, so the upstream authorization URL lands in the same store write instead of a follow-up Update (one round trip instead of two on valkey, and no state can exist without its authorization URL).
QuentinBisson
marked this pull request as ready for review
July 17, 2026 14:02
paurosello
approved these changes
Jul 22, 2026
QuentinBisson
enabled auto-merge (squash)
July 22, 2026 07:52
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.
Lets the front-end that initiated a connector login observe its completion, and makes login completion visible to live MCP sessions, without affecting other clients of the same muster.
What changed
/oauth/proxy/start):core_auth_loginchallenges now return a muster-hosted URL that 302s to the upstream authorization URL (stored server-side with the flow's state). One extra transparent hop for every client; the login flow is otherwise unchanged.oauth.mcpClient.postLoginRedirectAllowlist: the initiating caller may appendredirect=<url>to the start URL. When the target matches an allowlist entry (exact scheme and host, path extended at a segment boundary; query unconstrained), it is recorded on the flow's state, and a successful callback 303s the browser there withserver=<name>appended, instead of rendering the static success page.list_changedafter browser-consent connect: the OAuth callback connects the backend outside any MCP request, so a client that had already listed and cached its tools never learned the backend became available and had to re-runcore_auth_loginmanually. The callback-connect path now pushes the same subject-widetools/resources/prompts list_changednotifications the SSO connect path already sends, so clients that honorlist_changed(e.g. Claude Code) see the new tools appear on their own.Why per-flow instead of a global config URL
The first revision of this PR used a single configured redirect URL. That breaks on shared musters: klaus-gateway, Claude Code, and Backstage AI all drive connector logins through the same instance, and a global redirect would send every client's consent flow to one front-end's landing page. With the start-endpoint parameter, only flows whose login URL was decorated by the initiating front-end redirect; everyone else keeps the static success page.
The redirect target's query is preserved, so a front-end can carry its own correlation state (e.g. klaus-gateway mapping the completion back to a Slack user and prompt) without cookies.
Security notes
Test coverage
Update(non-consuming, TTL preserved), callback redirect vs. success page.Consumer
klaus-gateway's Slack connector prompt (giantswarm/klaus-gateway#139): the Connect button gets
redirect=<gateway>/connectors/complete?s=<state>appended, and the gateway landing endpoint replaces the ephemeral prompt with "signed in" via the storedresponse_url.