mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
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:
parent
a199bf975d
commit
e29e24e628
2 changed files with 54 additions and 16 deletions
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue