diff --git a/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx b/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx index 24881e669f9..9157fedbe21 100644 --- a/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx +++ b/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx @@ -11,6 +11,7 @@ import { serverRootPath, } from "@/components/networking"; import { extractErrorMessage } from "@/utils/errorUtils"; +import { generateCodeChallenge, generateCodeVerifier } from "@/utils/pkce"; import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; export type McpOAuthStatus = "idle" | "authorizing" | "exchanging" | "success" | "error"; @@ -34,25 +35,6 @@ interface UseMcpOAuthFlowResult { tokenResponse: Record | null; } -const base64UrlEncode = (buffer: ArrayBuffer) => { - const bytes = new Uint8Array(buffer); - let binary = ""; - bytes.forEach((b) => (binary += String.fromCharCode(b))); - return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -}; - -const generateCodeVerifier = () => { - const array = new Uint8Array(32); - window.crypto.getRandomValues(array); - return base64UrlEncode(array.buffer); -}; - -const generateCodeChallenge = async (verifier: string) => { - const data = new TextEncoder().encode(verifier); - const digest = await window.crypto.subtle.digest("SHA-256", data); - return base64UrlEncode(digest); -}; - export const useMcpOAuthFlow = ({ accessToken, getCredentials, diff --git a/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx b/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx index e032c503dc7..aa7ce84de5e 100644 --- a/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx +++ b/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx @@ -23,6 +23,7 @@ import { } from "@/components/networking"; import NotificationsManager from "@/components/molecules/notifications_manager"; import { extractErrorMessage } from "@/utils/errorUtils"; +import { generateCodeChallenge, generateCodeVerifier } from "@/utils/pkce"; import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; export type UserMcpOAuthStatus = "idle" | "authorizing" | "exchanging" | "success" | "error"; @@ -60,25 +61,6 @@ type StoredFlowState = { scopes?: string[]; }; -const b64url = (buf: ArrayBuffer) => { - const bytes = new Uint8Array(buf); - let s = ""; - bytes.forEach((b) => (s += String.fromCharCode(b))); - return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -}; - -const genVerifier = () => { - const arr = new Uint8Array(32); - window.crypto.getRandomValues(arr); - return b64url(arr.buffer); -}; - -const genChallenge = async (verifier: string) => { - const data = new TextEncoder().encode(verifier); - const digest = await window.crypto.subtle.digest("SHA-256", data); - return b64url(digest); -}; - const setStorage = (key: string, value: string) => { setSecureItem(key, value); }; @@ -144,8 +126,8 @@ export const useUserMcpOAuthFlow = ({ } } - const verifier = genVerifier(); - const challenge = await genChallenge(verifier); + const verifier = generateCodeVerifier(); + const challenge = await generateCodeChallenge(verifier); const state = crypto.randomUUID(); const redirectUri = buildCallbackUrl(); const scopeString = scopes?.filter((s) => s.trim()).join(" "); diff --git a/ui/litellm-dashboard/src/utils/pkce.ts b/ui/litellm-dashboard/src/utils/pkce.ts new file mode 100644 index 00000000000..2b960c34f33 --- /dev/null +++ b/ui/litellm-dashboard/src/utils/pkce.ts @@ -0,0 +1,18 @@ +const base64UrlEncode = (buffer: ArrayBuffer) => { + const bytes = new Uint8Array(buffer); + let binary = ""; + bytes.forEach((b) => (binary += String.fromCharCode(b))); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +}; + +export const generateCodeVerifier = () => { + const array = new Uint8Array(32); + window.crypto.getRandomValues(array); + return base64UrlEncode(array.buffer); +}; + +export const generateCodeChallenge = async (verifier: string) => { + const data = new TextEncoder().encode(verifier); + const digest = await window.crypto.subtle.digest("SHA-256", data); + return base64UrlEncode(digest); +};