mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
mcpTokenStore was the only OAuth path writing straight to window.sessionStorage; useMcpOAuthFlow, useToolsOAuthFlow, the callback page and the edit-screen UI state all already go through secureStorage. Align it so the OAuth surface has one storage format instead of two. The stored payload also carried a refresh_token that nothing ever read back. All three read sites take access_token only, and nothing reads the mcp-session-token: keys directly, so the field was write-only. Drop it from the store and from the four callers that populated it. The client-forwarded modes (true_passthrough and oauth_delegate) re-authorize rather than refresh, and authorization_code is unaffected because it persists through storeMCPOAuthUserCredential on the backend, which keeps its own refresh token. Entries written before this change decode to null and are treated as absent, which surfaces the normal Authorize prompt; they are session-scoped and expire in an hour. Add two regression tests that decode the stored value before asserting, so neither can pass merely because the payload is no longer plain text.
85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
/**
|
|
* Session-storage-backed OAuth token store for MCP servers.
|
|
* Tokens are keyed by LiteLLM user id + server_id and cleared when the browser
|
|
* session ends (tab/window close). Never written to localStorage.
|
|
*/
|
|
|
|
import { getSecureItem, setSecureItem } from "./secureStorage";
|
|
|
|
const KEY_PREFIX = "mcp-session-token:";
|
|
|
|
interface StoredToken {
|
|
access_token: string;
|
|
expires_at: number;
|
|
token_type: string;
|
|
}
|
|
|
|
interface TokenInput {
|
|
access_token: string;
|
|
expires_in?: number;
|
|
token_type?: string;
|
|
}
|
|
|
|
const DEFAULT_TTL_MS = 3600 * 1000; // 1 hour
|
|
|
|
function storageKey(serverId: string, userId?: string | null): string {
|
|
const userPart = userId?.trim() || "_anonymous";
|
|
return `${KEY_PREFIX}${userPart}:${serverId}`;
|
|
}
|
|
|
|
export function setToken(serverId: string, data: TokenInput, userId?: string | null): void {
|
|
if (typeof window === "undefined") return;
|
|
const stored: StoredToken = {
|
|
access_token: data.access_token,
|
|
expires_at: Date.now() + (data.expires_in != null ? data.expires_in * 1000 : DEFAULT_TTL_MS),
|
|
token_type: data.token_type ?? "bearer",
|
|
};
|
|
try {
|
|
setSecureItem(storageKey(serverId, userId), JSON.stringify(stored));
|
|
} catch {
|
|
// Silently ignore storage errors (private browsing, quota exceeded, etc.)
|
|
}
|
|
}
|
|
|
|
export function getToken(serverId: string, userId?: string | null): StoredToken | null {
|
|
if (typeof window === "undefined") return null;
|
|
try {
|
|
const raw = getSecureItem(storageKey(serverId, userId));
|
|
if (!raw) return null;
|
|
return JSON.parse(raw) as StoredToken;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function removeToken(serverId: string, userId?: string | null): void {
|
|
if (typeof window === "undefined") return;
|
|
try {
|
|
window.sessionStorage.removeItem(storageKey(serverId, userId));
|
|
} catch {
|
|
// Silently ignore
|
|
}
|
|
}
|
|
|
|
export function isTokenValid(serverId: string, userId?: string | null): boolean {
|
|
const token = getToken(serverId, userId);
|
|
if (!token) return false;
|
|
return token.expires_at > Date.now();
|
|
}
|
|
|
|
/** Remove all MCP session tokens (e.g. on logout or user switch). */
|
|
export function clearAllMcpTokens(): void {
|
|
if (typeof window === "undefined") return;
|
|
try {
|
|
const keysToRemove: string[] = [];
|
|
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
const key = window.sessionStorage.key(i);
|
|
if (key?.startsWith(KEY_PREFIX)) {
|
|
keysToRemove.push(key);
|
|
}
|
|
}
|
|
keysToRemove.forEach((key) => window.sessionStorage.removeItem(key));
|
|
} catch {
|
|
// Silently ignore
|
|
}
|
|
}
|