Skip to content

feat(ui): connect-flow interlude banner on the MCP apps grid for gateway DCR sign-in - #33192

Merged
tin-berri merged 3 commits into
litellm_lit3637_session_admissionfrom
litellm_lit3637_grid_interlude
Jul 19, 2026
Merged

feat(ui): connect-flow interlude banner on the MCP apps grid for gateway DCR sign-in#33192
tin-berri merged 3 commits into
litellm_lit3637_session_admissionfrom
litellm_lit3637_grid_interlude

Conversation

@tin-berri

@tin-berri tin-berri commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Stacked on #33191 (multi-team union) -> #33190 -> #33189 -> #33188 -> #33182 -> #33174. The base of this PR is litellm_lit3637_team_union; review and merge after those

Linear ticket

Part of LIT-3637 (PR 5 of the stack: the magic-URL interlude that turns the apps grid into the finish step of the gateway DCR sign-in)

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

This is a UI-only change. To see it end to end against a live proxy (aggregate gateway DCR is on by default, no flag):

  1. Start the proxy and the dashboard dev server (npm run dev in ui/litellm-dashboard, served on port 3000)
  2. Drive a DCR client (or the curl walk from PR feat(mcp): aggregate DCR register, authorize, complete, and token flow for the gateway front door #33189) up to the authorize step, which 303-redirects the browser to http://localhost:4000/ui/chat/integrations?connect_flow=<handle>&connect_client=https://claude.ai
  3. Open that URL in a browser where you are signed into LiteLLM; confirm the blue interlude banner appears above the apps grid, reading "Connect your MCP servers to https://claude.ai", and that the grid shows only the server cards with no Beta badge, chat subtitle, or tool-count summary
  4. Confirm each already-authorized OAuth2 server shows Connected immediately without first flashing a Connect button, and servers still needing authorization show their Connect button
  5. Authorize any servers you need with the per-card Connect buttons (they vault tokens per user); confirm authorizing one navigates you out and back without the banner auto-finishing behind you
  6. Either click "Finish connecting" or just close the tab; confirm the browser reaches /authorize/complete (a form POST for the button, a sendBeacon for the close) and the client receives the gateway authorization code at its redirect URI, completing sign-in

The unit tests assert the finish action is a full-page form POST carrying only the flow handle (no token, code, or secret), that closing the tab fires a sendBeacon to the same endpoint, that the beacon is suppressed while a per-server authorize is in flight and after the button was pressed, that the client origin is shown, and that it falls back to a generic label when the origin is absent

Type

🆕 New Feature

Changes

The gateway DCR authorize (PR #33189) sends a signed-in user to /ui/chat/integrations?connect_flow=<handle> to authorize servers before finishing sign-in. This adds the interlude that page needs: when connect_flow is present, a ConnectFlowBanner renders above the apps grid, naming the client the user is connecting to and offering a single finish action

In this connect context the grid drops its chat-oriented chrome so it reads as "authorize your servers" rather than a chat feature: the connectMode prop hides the Beta badge, the "browse tools, authenticate once, use in chat" subtitle, and the tool-count summary, leaving just the server cards you click into to see tools and connect. With no connect_flow param the page is byte-identical to today, so the normal browse-and-connect experience is untouched

Finish happens two ways. The explicit "Finish connecting" button is a native HTML form POST to the proxy's /authorize/complete, not a fetch: that endpoint 303-redirects the browser back to the DCR client's own redirect URI with the gateway authorization code, and only a full-page navigation carries the HttpOnly per-flow cookie and follows that redirect. Closing or navigating away from the tab now also best-effort finishes, via navigator.sendBeacon to the same endpoint, so the code still reaches the client's loopback without an explicit click and the browser follows the 303 the same way. This is a convenience, not a consent gate, since consent already happened at sign-in. It is skipped while a per-server authorize is navigating the page away (tracked by a short-lived sessionStorage flag) and after the button was pressed, so it never fires mid-authorize or double-delivers the code. The form and the beacon both carry only the opaque flow handle; everything sensitive lives in the sealed cookie the authorize step set

Authorized servers used to flash their Connect button for about a second before flipping to Connected, because the per-user credential checks only ran after the whole tool-count fetch had finished. The credential checks now fire in parallel with the tool-count load, and each OAuth card shows a skeleton in its button slot until its own status resolves, so the connect/connected state never flips under the user. Batching the status into the /v1/mcp/server response is a separate backend/schema optimization still left out to keep this change UI-only

QA runbook

Follow the numbered steps above. Also confirm that visiting /ui/chat/integrations with no connect_flow param shows no banner, keeps the Beta badge and tool-count chrome, and behaves exactly as before

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a ConnectFlowBanner that renders above the MCP apps grid when the connect_flow query parameter is present, forming the finish step of the gateway DCR sign-in flow. With no connect_flow param the page is byte-identical to the previous behaviour.

  • The "Finish connecting" action is a native HTML form POST (not a fetch) to the proxy's /authorize/complete, carrying only the opaque flow handle; the sealed HttpOnly per-flow cookie is the security boundary, not the URL.
  • connect_client is intentionally read from the URL as a display-only label; the actual client identity is enforced by the backend cookie validation at /authorize/complete.
  • Three unit tests cover the form shape, client origin display, and null-origin fallback.

Confidence Score: 4/5

Safe to merge for the normal integrations path; the banner is only shown when connect_flow is present and the form POST is guarded by the backend cookie validation.

The change is narrow and well-isolated. The banner renders without any backend round-trip, so a crafted URL with an arbitrary connect_client value will display misleading text until the user clicks Finish connecting and the backend rejects the mismatched cookie — that gap between display and enforcement is the main thing worth a second look before merge.

ConnectFlowBanner.tsx — the connect_client display path; ConnectFlowBanner.test.tsx — the sensitive-field assertion.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/chat/ConnectFlowBanner.tsx New component: renders the DCR sign-in interlude banner; uses a native form POST to /authorize/complete with only the flow handle as a field — design is intentional and well-commented. The connect_client URL param is passed straight to display without any origin-validation against the flow's registered client.
ui/litellm-dashboard/src/components/chat/ConnectFlowBanner.test.tsx Three unit tests cover the form POST shape, the client origin display, and the null-origin fallback. Coverage is reasonable for the surface area.
ui/litellm-dashboard/src/app/chat/integrations/page.tsx Reads connect_flow and connect_client search params and conditionally renders ConnectFlowBanner above the existing MCPAppsPanel; existing oauthReturn cleanup logic is untouched.
ui/litellm-dashboard/tsconfig.tsbuildinfo TypeScript incremental build artifact; no code change to review.

Reviews (1): Last reviewed commit: "feat(ui): connect-flow interlude banner ..." | Re-trigger Greptile

*/
const ConnectFlowBanner: React.FC<Props> = ({ flowHandle, clientOrigin }) => {
const action = `${getProxyBaseUrl()}/authorize/complete`;
const clientLabel = clientOrigin ?? "the application";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 clientOrigin displayed without cross-checking the registered client

The connect_client URL parameter is shown verbatim as the identity of the connecting service ("Connect your MCP servers to {clientLabel}"). Because connect_flow is intentionally left in the URL, anyone who knows or guesses an opaque flow handle can craft a link with an arbitrary connect_client value and present a misleading "Connect to YourBank" banner to a signed-in user. The POST itself will fail at /authorize/complete once the backend validates the cookie, but the banner still renders before that check fires.

If the /authorize step already stores the client origin in the sealed cookie, the backend could echo it back (e.g., as a short-lived signed display hint) so the UI can show a value that matches the actual DCR registration rather than trusting the URL param.

Comment on lines +20 to +21
expect(form.innerHTML).not.toContain("token");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Sensitive-field check searches substring rather than enumerating inputs

expect(form.innerHTML).not.toContain("token") passes as long as the literal string "token" never appears anywhere in the rendered HTML — including class names, aria labels, or comments. A hidden field named "auth_token" would be caught, but "access_code" or "secret" would not. Enumerating all <input> elements by querySelectorAll('input') and asserting exactly one name="flow" field would give the same intent with a tighter, name-based assertion.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tin-berri
tin-berri force-pushed the litellm_lit3637_team_union branch from 5daf091 to 8e5d0a6 Compare July 14, 2026 17:01
@tin-berri
tin-berri force-pushed the litellm_lit3637_grid_interlude branch from 1707d3e to 08a9cb0 Compare July 14, 2026 17:02
@devin-ai-integration

Copy link
Copy Markdown
Contributor
Claude Code gateway DCR runtime

Ran PR head 08a9cb0f45 with Claude Code 2.1.209 against http://localhost:4142/mcp/, PostgreSQL, Redis-backed auth cache, and the real DeepWiki MCP endpoint

  • Passed: Claude registered litellm-gateway and offered Authenticate
  • Passed: the signed-in retry rendered the connect-flow banner and Finish connecting
  • Passed: DeepWiki connected with read_wiki_structure, read_wiki_contents, and ask_question
  • Passed: /authorize/complete returned 303, /token returned 200, and Claude showed connected · 3 tools
  • Passed: Claude called deepwiki-ask_question and returned the tool-grounded answer
  • Caveat: the local no-SSO username/password login established the UI session but redirected to /ui/?login=success, so the user had to select Authenticate a second time
  • Runtime prerequisite: enable_chat_ui=true; when disabled, the authenticated integrations route redirects to the normal dashboard

Connect-flow banner and connected DeepWiki

Claude DeepWiki tool result

Full runtime session

@tin-berri
tin-berri force-pushed the litellm_lit3637_grid_interlude branch from 08a9cb0 to 4a04e6c Compare July 15, 2026 00:42
*/
const ConnectFlowBanner: React.FC<Props> = ({ flowHandle, clientOrigin }) => {
const action = `${getProxyBaseUrl()}/authorize/complete`;
const clientLabel = clientOrigin ?? "the application";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Misleading OAuth client identity

A malicious DCR client can register a redirect such as https://claude.ai@evil.example/callback. The banner displays that raw authority as the client identity, while the browser sends the authorization code to evil.example, allowing the attacker to trick a user into issuing gateway access and refresh tokens to the malicious client. Parse the value with the browser URL implementation and display its normalized origin instead.

Suggested change
const clientLabel = clientOrigin ?? "the application";
const clientLabel = (() => {
if (!clientOrigin) return "the application";
try {
const parsed = new URL(clientOrigin);
return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.origin : "the application";
} catch {
return "the application";
}
})();

@veria-ai

veria-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR adds a connect-flow interlude banner to the LiteLLM dashboard for gateway DCR sign-in from the MCP apps grid. The banner presents the requesting application context and participates in completing or cancelling the authorization flow.

There are two open security issues in the OAuth/DCR interlude flow, with none addressed yet. The most significant issue is that the authorization flow can complete without an explicit user confirmation, allowing a malicious registered client to obtain user tokens if it drives an already-authenticated user through the flow. A second issue can make the requesting client identity misleading by displaying an unnormalized redirect authority. The PR should be considered high risk until completion is gated on an explicit user action and the client identity display is normalized.

Open issues (2)

Fixed/addressed: 0 · PR risk: 8/10

…nnect-status flash

In the gateway DCR connect flow the apps grid now reads as "authorize your servers"
rather than a chat feature; the connectMode prop drops the Beta badge, the "use in
chat" subtitle, and the tool-count chrome

Closing the connect tab now best-effort finishes the flow via navigator.sendBeacon to
/authorize/complete, so the gateway authorization code still reaches the client's
loopback without an explicit click; the explicit "Finish connecting" button stays as
the reliable path. The beacon is skipped while a per-server authorize is navigating
away and after the button was pressed, so it never double-delivers or fires mid-authorize

Authorized servers previously flashed "Connect" for a second before flipping to
"Connected" because the per-user credential checks ran only after the whole tool-count
fetch finished. They now fire in parallel with the tool-count load, and each card shows
a skeleton in the button slot until its status resolves, so the state never flips under
the user
if (finishedRef.current) return;
if (sessionStorage.getItem(PERSERVER_CONNECTING_KEY) === "1") return;
if (typeof navigator.sendBeacon === "function") {
navigator.sendBeacon(action, new URLSearchParams({ flow: flowHandle }));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: OAuth flow completes without user confirmation

A malicious DCR client can send an already-authenticated user through its authorization URL and then receive a redeemable code when the user merely closes or navigates away from this page. Because the client controls the PKCE verifier, it can exchange that code for access and refresh tokens representing the user; only an explicit finish action should call /authorize/complete.

…y connect flow

On the aggregate gateway connect flow the client holds only an identity-only session
bearer, and upstream credentials are resolved server-side from the per-user vault, which
is only populated by interactive authorization_code (oauth2). The client-forwarded modes
(true_passthrough, oauth_delegate) need the caller to present the upstream Authorization
per call, and oauth2_token_exchange (OBO) needs the caller's own IdP token as the exchange
subject; the session bearer is neither, so a tool call to those servers can never complete
on this connection.

Rather than let them look connectable and then 401, the grid greys those servers and
labels them "Not supported on this connection" when rendered in connect mode. Outside the
connect flow the normal integrations page is unchanged, since the client forwards its own
token there and those modes work. The classification lives in a shared
isUnsupportedOnGatewayConnect helper next to isClientForwardedTokenMode so the UI gate and
the auth-mode taxonomy cannot drift.
Base automatically changed from litellm_lit3637_team_union to litellm_lit3637_session_admission July 17, 2026 21:52
@tin-berri
tin-berri merged commit 1dedeaa into litellm_lit3637_session_admission Jul 19, 2026
97 of 98 checks passed
@tin-berri
tin-berri deleted the litellm_lit3637_grid_interlude branch July 19, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant