fix(ui): keep the browser-authorized token out of form.credentials for the pass-through modes

The create form wrote the upstream token obtained by Authorize & Fetch into
form.credentials for every mode, so for true_passthrough / oauth_delegate the
browser-held token leaked into the OAuth flow's getCredentials (preview requests)
and the redirect-persist cache, and was a step away from server-level credential
persistence. onTokenReceived now early-returns for the client-forwarded modes,
holding the token only in local state for preview (mirroring the edit form),
instead of writing it into form.credentials.
This commit is contained in:
Tin 2026-07-08 23:09:19 -07:00
parent a199bf975d
commit e29e24e628
2 changed files with 54 additions and 16 deletions

View file

@ -30,6 +30,7 @@ const oauthHook = vi.hoisted(() => ({
onTokenReceived: null as
| ((token: Record<string, unknown> | null, registeredClient?: { clientId?: string; clientSecret?: string }) => void)
| null,
getCredentials: null as (() => Record<string, unknown> | undefined) | null,
}));
vi.mock("@/hooks/useMcpOAuthFlow", () => ({
useMcpOAuthFlow: (opts: {
@ -37,8 +38,10 @@ vi.mock("@/hooks/useMcpOAuthFlow", () => ({
token: Record<string, unknown> | null,
registeredClient?: { clientId?: string; clientSecret?: string },
) => void;
getCredentials?: () => Record<string, unknown> | undefined;
}) => {
oauthHook.onTokenReceived = opts.onTokenReceived;
oauthHook.getCredentials = opts.getCredentials ?? null;
return {
startOAuthFlow: vi.fn(),
status: "idle",
@ -349,6 +352,28 @@ describe("CreateMCPServer", () => {
expect(payload.credentials).toEqual({ auth_value: "my-secret-key" });
});
it("does not write the browser-authorized token into form.credentials for true_passthrough", async () => {
await selectHttpTransport();
const user = userEvent.setup({ delay: null });
await user.type(getServerNameInput(), "PT_Server");
await user.type(screen.getByPlaceholderText("https://your-mcp-server.com"), "https://example.com/mcp");
await selectAntOption("Authentication", "True Passthrough (no LiteLLM auth)");
// Simulate the browser Authorize & Fetch flow handing back an upstream token.
await waitFor(() => expect(oauthHook.onTokenReceived).toBeTruthy());
await act(async () => {
oauthHook.onTokenReceived!({ access_token: "upstream-tok", token_type: "Bearer" }, undefined);
});
// For a browser-only mode the token must never land in form.credentials, which the OAuth flow's
// getCredentials reads for preview requests and the redirect-persist cache serializes. Without
// the guard, onTokenReceived writes it here and this returns { access_token: "upstream-tok" }.
const credentials = oauthHook.getCredentials?.() ?? {};
expect(credentials.access_token).toBeUndefined();
});
it("should not show auth value field when None auth type is selected", async () => {
await selectHttpTransport();

View file

@ -200,23 +200,36 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
onTokenReceived: (token, registeredClient) => {
setOauthAccessToken(token?.access_token ?? null);
if (token?.access_token) {
const credentials = {
access_token: token.access_token,
...(token.refresh_token && { refresh_token: token.refresh_token }),
...(token.expires_in && { expires_in: token.expires_in }),
...(token.scope && { scope: token.scope }),
...(registeredClient?.clientId && { client_id: registeredClient.clientId }),
...(registeredClient?.clientSecret && { client_secret: registeredClient.clientSecret }),
};
form.setFieldsValue({ credentials });
setAuthorizedUrl(getOAuthAuthorizationTarget(form.getFieldsValue(true)));
NotificationsManager.success(
"OAuth authorization successful! Please click 'Create MCP Server' to save the configuration.",
);
if (!token?.access_token) {
return;
}
if (isClientForwardedTokenMode(form.getFieldValue("auth_type"))) {
// Browser-only modes: the token is held in local state (oauthAccessToken) for tool preview
// and committed to sessionStorage on submit; it must never be written into form.credentials,
// which would persist it as server-level credentials on the created server row. Mirrors the
// edit form's onTokenReceived early return.
NotificationsManager.success(
"Token held for this browser session. Tools can now be previewed and configured; nothing will be saved to LiteLLM.",
);
return;
}
const credentials = {
access_token: token.access_token,
...(token.refresh_token && { refresh_token: token.refresh_token }),
...(token.expires_in && { expires_in: token.expires_in }),
...(token.scope && { scope: token.scope }),
...(registeredClient?.clientId && { client_id: registeredClient.clientId }),
...(registeredClient?.clientSecret && { client_secret: registeredClient.clientSecret }),
};
form.setFieldsValue({ credentials });
setAuthorizedUrl(getOAuthAuthorizationTarget(form.getFieldsValue(true)));
NotificationsManager.success(
"OAuth authorization successful! Please click 'Create MCP Server' to save the configuration.",
);
},
onBeforeRedirect: persistCreateUiState,
flowSource: "create",