fix(ui): separate OAuth result keys per flow, sessionStorage-only, surface revoke errors

This commit is contained in:
Ishaan Jaffer 2026-03-11 08:42:33 -07:00
parent f47cce70f1
commit e6f948facb
3 changed files with 17 additions and 12 deletions

View file

@ -3,7 +3,11 @@
import { Suspense, useEffect, useMemo } from "react";
import { useSearchParams } from "next/navigation";
const RESULT_STORAGE_KEY = "litellm-mcp-oauth-result";
// Written to sessionStorage so both the admin hook (useMcpOAuthFlow) and the
// user hook (useUserMcpOAuthFlow) can pick up the result. Each hook reads
// its own namespace to avoid cross-flow collisions.
const ADMIN_RESULT_KEY = "litellm-mcp-oauth-result";
const USER_RESULT_KEY = "litellm-user-mcp-oauth-result";
const RETURN_URL_STORAGE_KEY = "litellm-mcp-oauth-return-url";
const resolveDefaultRedirect = () => {
@ -41,16 +45,16 @@ const McpOAuthCallbackContent = () => {
}
try {
// Store in both sessionStorage and localStorage for redundancy
window.sessionStorage.setItem(RESULT_STORAGE_KEY, JSON.stringify(payload));
window.localStorage.setItem(RESULT_STORAGE_KEY, JSON.stringify(payload));
// Write to both namespace keys (admin and user) so whichever hook is
// active can consume the result. sessionStorage only — no localStorage.
const serialized = JSON.stringify(payload);
window.sessionStorage.setItem(ADMIN_RESULT_KEY, serialized);
window.sessionStorage.setItem(USER_RESULT_KEY, serialized);
} catch (err) {
// Silently ignore storage errors
}
// Check both sessionStorage and localStorage for return URL
const returnUrl = window.sessionStorage.getItem(RETURN_URL_STORAGE_KEY) ||
window.localStorage.getItem(RETURN_URL_STORAGE_KEY);
const returnUrl = window.sessionStorage.getItem(RETURN_URL_STORAGE_KEY);
const destination = returnUrl || resolveDefaultRedirect();
window.location.replace(destination);
}, [payload]);

View file

@ -8,7 +8,7 @@
*/
import React, { useCallback, useEffect, useState } from "react";
import { Spin } from "antd";
import { Spin, message } from "antd";
import { CheckCircleOutlined, DeleteOutlined, LinkOutlined } from "@ant-design/icons";
import {
deleteMCPOAuthUserCredential,
@ -76,7 +76,7 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
await deleteMCPOAuthUserCredential(accessToken, serverId);
setCredentials((prev) => prev.filter((c) => c.server_id !== serverId));
} catch {
// silently fail — user can retry
message.error("Failed to revoke connection. Please try again.");
} finally {
setRevoking((prev) => { const n = new Set(prev); n.delete(serverId); return n; });
}

View file

@ -44,7 +44,9 @@ interface UseUserMcpOAuthFlowResult {
}
const FLOW_STATE_KEY = "litellm-user-mcp-oauth-flow-state";
const RESULT_KEY = "litellm-mcp-oauth-result";
// Use a user-flow-specific key to avoid collisions with the admin OAuth flow
// (useMcpOAuthFlow) which uses "litellm-mcp-oauth-result".
const RESULT_KEY = "litellm-user-mcp-oauth-result";
const RETURN_URL_KEY = "litellm-mcp-oauth-return-url";
type StoredFlowState = {
@ -88,7 +90,7 @@ const setStorage = (key: string, value: string) => {
const getStorage = (key: string): string | null => {
try {
return window.sessionStorage.getItem(key) || window.localStorage.getItem(key);
return window.sessionStorage.getItem(key);
} catch (_) {
return null;
}
@ -98,7 +100,6 @@ const clearStorage = (...keys: string[]) => {
keys.forEach((k) => {
try {
window.sessionStorage.removeItem(k);
window.localStorage.removeItem(k);
} catch (_) {}
});
};