Spaces:
Running
Running
| import { | |
| discoverOAuthProtectedResourceMetadata, | |
| discoverAuthorizationServerMetadata, | |
| startAuthorization, | |
| exchangeAuthorization, | |
| registerClient, | |
| } from "@modelcontextprotocol/sdk/client/auth.js"; | |
| import { secureStorage } from "../utils/storage"; | |
| import { MCP_CLIENT_CONFIG, STORAGE_KEYS, DEFAULTS } from "../config/constants"; | |
| // Utility to fetch .well-known/modelcontextprotocol for OAuth endpoints | |
| export async function discoverOAuthEndpoints(serverUrl: string) { | |
| // ...existing code... | |
| let resourceMetadata, authMetadata, authorizationServerUrl; | |
| try { | |
| resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl); | |
| if (resourceMetadata?.authorization_servers?.length) { | |
| authorizationServerUrl = resourceMetadata.authorization_servers[0]; | |
| } | |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
| } catch (e) { | |
| // Fallback to direct metadata discovery if protected resource fails | |
| authMetadata = await discoverAuthorizationServerMetadata(serverUrl); | |
| authorizationServerUrl = authMetadata?.issuer || serverUrl; | |
| } | |
| if (!authorizationServerUrl) { | |
| throw new Error("No authorization server found for this MCP server"); | |
| } | |
| // Discover authorization server metadata if not already done | |
| if (!authMetadata) { | |
| authMetadata = await discoverAuthorizationServerMetadata( | |
| authorizationServerUrl | |
| ); | |
| } | |
| if ( | |
| !authMetadata || | |
| !authMetadata.authorization_endpoint || | |
| !authMetadata.token_endpoint | |
| ) { | |
| throw new Error("Missing OAuth endpoints in authorization server metadata"); | |
| } | |
| // If client_id is missing, register client dynamically | |
| if (!authMetadata.client_id && authMetadata.registration_endpoint) { | |
| // Determine token endpoint auth method | |
| let tokenEndpointAuthMethod = "none"; | |
| if ( | |
| authMetadata.token_endpoint_auth_methods_supported?.includes( | |
| "client_secret_post" | |
| ) | |
| ) { | |
| tokenEndpointAuthMethod = "client_secret_post"; | |
| } else if ( | |
| authMetadata.token_endpoint_auth_methods_supported?.includes( | |
| "client_secret_basic" | |
| ) | |
| ) { | |
| tokenEndpointAuthMethod = "client_secret_basic"; | |
| } | |
| const clientMetadata = { | |
| redirect_uris: [ | |
| String( | |
| authMetadata.redirect_uri || | |
| window.location.origin + "/#/oauth/callback" | |
| ), | |
| ], | |
| client_name: MCP_CLIENT_CONFIG.NAME, | |
| grant_types: ["authorization_code"], | |
| response_types: ["code"], | |
| token_endpoint_auth_method: tokenEndpointAuthMethod, | |
| }; | |
| const clientInfo = await registerClient(authorizationServerUrl, { | |
| metadata: authMetadata, | |
| clientMetadata, | |
| }); | |
| authMetadata.client_id = clientInfo.client_id; | |
| if (clientInfo.client_secret) { | |
| authMetadata.client_secret = clientInfo.client_secret; | |
| } | |
| // Persist client credentials for later use | |
| localStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_ID, clientInfo.client_id); | |
| if (clientInfo.client_secret) { | |
| await secureStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET, clientInfo.client_secret); | |
| } | |
| } | |
| if (!authMetadata.client_id) { | |
| throw new Error( | |
| "Missing client_id and registration not supported by authorization server" | |
| ); | |
| } | |
| // Step 3: Validate resource | |
| const resource = resourceMetadata?.resource | |
| ? new URL(resourceMetadata.resource) | |
| : undefined; | |
| // Persist endpoints, metadata, and MCP server URL for callback use | |
| localStorage.setItem( | |
| STORAGE_KEYS.OAUTH_AUTHORIZATION_ENDPOINT, | |
| authMetadata.authorization_endpoint | |
| ); | |
| localStorage.setItem(STORAGE_KEYS.OAUTH_TOKEN_ENDPOINT, authMetadata.token_endpoint); | |
| localStorage.setItem( | |
| STORAGE_KEYS.OAUTH_REDIRECT_URI, | |
| (authMetadata.redirect_uri ||window.location.origin + "/#" + DEFAULTS.OAUTH_REDIRECT_PATH).toString() | |
| ); | |
| localStorage.setItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL, serverUrl); | |
| localStorage.setItem( | |
| STORAGE_KEYS.OAUTH_AUTHORIZATION_SERVER_METADATA, | |
| JSON.stringify(authMetadata) | |
| ); | |
| if (resource) { | |
| localStorage.setItem(STORAGE_KEYS.OAUTH_RESOURCE, resource.toString()); | |
| } | |
| return { | |
| authorizationEndpoint: authMetadata.authorization_endpoint, | |
| tokenEndpoint: authMetadata.token_endpoint, | |
| clientId: authMetadata.client_id, | |
| clientSecret: authMetadata.client_secret, | |
| scopes: authMetadata.scopes || [], | |
| redirectUri: | |
| authMetadata.redirect_uri || window.location.origin + "/#/oauth/callback", | |
| resource, | |
| }; | |
| } | |
| // Start OAuth flow: redirect user to authorization endpoint | |
| export async function startOAuthFlow({ | |
| authorizationEndpoint, | |
| clientId, | |
| redirectUri, | |
| scopes, | |
| resource, | |
| }: { | |
| authorizationEndpoint: string; | |
| clientId: string; | |
| redirectUri: string; | |
| scopes?: string[]; | |
| resource?: URL; | |
| }) { | |
| // Use Proof Key for Code Exchange (PKCE) and SDK to build the authorization URL | |
| // Use persisted client_id if available | |
| const persistedClientId = localStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_ID) || clientId; | |
| const clientInformation = { client_id: persistedClientId }; | |
| // Retrieve metadata from localStorage if available | |
| let metadata; | |
| try { | |
| const stored = localStorage.getItem(STORAGE_KEYS.OAUTH_AUTHORIZATION_SERVER_METADATA); | |
| if (stored) metadata = JSON.parse(stored); | |
| } catch { | |
| console.warn("Failed to parse stored OAuth metadata, using defaults"); | |
| } | |
| // Always pass resource from localStorage if not provided | |
| let resourceParam = resource; | |
| if (!resourceParam) { | |
| const resourceStr = localStorage.getItem(STORAGE_KEYS.OAUTH_RESOURCE); | |
| if (resourceStr) resourceParam = new URL(resourceStr); | |
| } | |
| const { authorizationUrl, codeVerifier } = await startAuthorization( | |
| authorizationEndpoint, | |
| { | |
| metadata, | |
| clientInformation, | |
| redirectUrl: redirectUri, | |
| scope: scopes?.join(" ") || undefined, | |
| resource: resourceParam, | |
| } | |
| ); | |
| // Save codeVerifier in localStorage for later token exchange | |
| localStorage.setItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER, codeVerifier); | |
| window.location.href = authorizationUrl.toString(); | |
| } | |
| // Exchange code for token using MCP SDK | |
| export async function exchangeCodeForToken({ | |
| code, | |
| redirectUri, | |
| }: { | |
| serverUrl?: string; | |
| code: string; | |
| redirectUri: string; | |
| }) { | |
| // Use only persisted credentials and endpoints for token exchange | |
| const tokenEndpoint = localStorage.getItem(STORAGE_KEYS.OAUTH_TOKEN_ENDPOINT); | |
| const redirectUriPersisted = localStorage.getItem(STORAGE_KEYS.OAUTH_REDIRECT_URI); | |
| const resourceStr = localStorage.getItem(STORAGE_KEYS.OAUTH_RESOURCE); | |
| const persistedClientId = localStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_ID); | |
| const persistedClientSecret = await secureStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET); | |
| const codeVerifier = localStorage.getItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER); | |
| if (!persistedClientId || !tokenEndpoint || !codeVerifier) | |
| throw new Error( | |
| "Missing OAuth client credentials or endpoints for token exchange" | |
| ); | |
| const clientInformation: { client_id: string; client_secret?: string } = { client_id: persistedClientId }; | |
| if (persistedClientSecret) { | |
| clientInformation.client_secret = persistedClientSecret; | |
| } | |
| // Retrieve metadata from localStorage if available | |
| let metadata; | |
| try { | |
| const stored = localStorage.getItem(STORAGE_KEYS.OAUTH_AUTHORIZATION_SERVER_METADATA); | |
| if (stored) metadata = JSON.parse(stored); | |
| } catch { | |
| console.warn("Failed to parse stored OAuth metadata, using defaults"); | |
| } | |
| // Use SDK to exchange code for tokens | |
| const tokens = await exchangeAuthorization(tokenEndpoint, { | |
| metadata, | |
| clientInformation, | |
| authorizationCode: code, | |
| codeVerifier, | |
| redirectUri: redirectUriPersisted || redirectUri, | |
| resource: resourceStr ? new URL(resourceStr) : undefined, | |
| }); | |
| // Persist access token in localStorage and sync to mcp-servers | |
| if (tokens && tokens.access_token) { | |
| await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token); | |
| try { | |
| const serversStr = localStorage.getItem(STORAGE_KEYS.MCP_SERVERS); | |
| if (serversStr) { | |
| const servers = JSON.parse(serversStr); | |
| for (const server of servers) { | |
| if ( | |
| server.auth && | |
| (server.auth.type === "bearer" || server.auth.type === "oauth") | |
| ) { | |
| server.auth.token = tokens.access_token; | |
| } | |
| } | |
| localStorage.setItem(STORAGE_KEYS.MCP_SERVERS, JSON.stringify(servers)); | |
| } | |
| } catch (err) { | |
| console.warn("Failed to sync token to mcp-servers:", err); | |
| } | |
| } | |
| return tokens; | |
| } | |