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; | |
} | |