|
|
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"; |
|
|
|
|
|
export async function discoverOAuthEndpoints(serverUrl: string) { |
|
|
|
|
|
let resourceMetadata, authMetadata, authorizationServerUrl; |
|
|
try { |
|
|
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl); |
|
|
if (resourceMetadata?.authorization_servers?.length) { |
|
|
authorizationServerUrl = resourceMetadata.authorization_servers[0]; |
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
authMetadata = await discoverAuthorizationServerMetadata(serverUrl); |
|
|
authorizationServerUrl = authMetadata?.issuer || serverUrl; |
|
|
} |
|
|
|
|
|
if (!authorizationServerUrl) { |
|
|
throw new Error("No authorization server found for this MCP server"); |
|
|
} |
|
|
|
|
|
|
|
|
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 (!authMetadata.client_id && authMetadata.registration_endpoint) { |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
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" |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
const resource = resourceMetadata?.resource |
|
|
? new URL(resourceMetadata.resource) |
|
|
: undefined; |
|
|
|
|
|
|
|
|
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, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
export async function startOAuthFlow({ |
|
|
authorizationEndpoint, |
|
|
clientId, |
|
|
redirectUri, |
|
|
scopes, |
|
|
resource, |
|
|
}: { |
|
|
authorizationEndpoint: string; |
|
|
clientId: string; |
|
|
redirectUri: string; |
|
|
scopes?: string[]; |
|
|
resource?: URL; |
|
|
}) { |
|
|
|
|
|
|
|
|
const persistedClientId = localStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_ID) || clientId; |
|
|
const clientInformation = { client_id: persistedClientId }; |
|
|
|
|
|
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"); |
|
|
} |
|
|
|
|
|
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, |
|
|
} |
|
|
); |
|
|
|
|
|
localStorage.setItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER, codeVerifier); |
|
|
window.location.href = authorizationUrl.toString(); |
|
|
} |
|
|
|
|
|
|
|
|
export async function exchangeCodeForToken({ |
|
|
code, |
|
|
redirectUri, |
|
|
}: { |
|
|
serverUrl?: string; |
|
|
code: string; |
|
|
redirectUri: string; |
|
|
}) { |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
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"); |
|
|
} |
|
|
|
|
|
const tokens = await exchangeAuthorization(tokenEndpoint, { |
|
|
metadata, |
|
|
clientInformation, |
|
|
authorizationCode: code, |
|
|
codeVerifier, |
|
|
redirectUri: redirectUriPersisted || redirectUri, |
|
|
resource: resourceStr ? new URL(resourceStr) : undefined, |
|
|
}); |
|
|
|
|
|
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; |
|
|
} |
|
|
|