| |
| |
| |
| |
| |
| |
|
|
| import { discoverOAuthProtectedResourceMetadata } from '@modelcontextprotocol/sdk/client/auth.js'; |
| import { mcpConfig } from '../mcpConfig'; |
|
|
| export interface OAuthDetectionResult { |
| requiresOAuth: boolean; |
| method: 'protected-resource-metadata' | '401-challenge-metadata' | 'no-metadata-found'; |
| metadata?: Record<string, unknown> | null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function detectOAuthRequirement(serverUrl: string): Promise<OAuthDetectionResult> { |
| const protectedResourceResult = await checkProtectedResourceMetadata(serverUrl); |
| if (protectedResourceResult) return protectedResourceResult; |
|
|
| const challengeResult = await check401ChallengeMetadata(serverUrl); |
| if (challengeResult) return challengeResult; |
|
|
| const fallbackResult = await checkAuthErrorFallback(serverUrl); |
| if (fallbackResult) return fallbackResult; |
|
|
| |
| return { |
| requiresOAuth: false, |
| method: 'no-metadata-found', |
| metadata: null, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| |
| async function checkProtectedResourceMetadata( |
| serverUrl: string, |
| ): Promise<OAuthDetectionResult | null> { |
| try { |
| const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl); |
|
|
| if (!resourceMetadata?.authorization_servers?.length) return null; |
|
|
| return { |
| requiresOAuth: true, |
| method: 'protected-resource-metadata', |
| metadata: resourceMetadata, |
| }; |
| } catch { |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async function check401ChallengeMetadata(serverUrl: string): Promise<OAuthDetectionResult | null> { |
| |
| const headResult = await check401WithMethod(serverUrl, 'HEAD'); |
| if (headResult) return headResult; |
|
|
| |
| const postResult = await check401WithMethod(serverUrl, 'POST'); |
| if (postResult) return postResult; |
|
|
| return null; |
| } |
|
|
| async function check401WithMethod( |
| serverUrl: string, |
| method: 'HEAD' | 'POST', |
| ): Promise<OAuthDetectionResult | null> { |
| try { |
| const fetchOptions: RequestInit = { |
| method, |
| signal: AbortSignal.timeout(mcpConfig.OAUTH_DETECTION_TIMEOUT), |
| }; |
|
|
| |
| if (method === 'POST') { |
| fetchOptions.headers = { 'Content-Type': 'application/json' }; |
| fetchOptions.body = JSON.stringify({}); |
| } |
|
|
| const response = await fetch(serverUrl, fetchOptions); |
|
|
| if (response.status !== 401) return null; |
|
|
| const wwwAuth = response.headers.get('www-authenticate'); |
| const metadataUrl = wwwAuth?.match(/resource_metadata="([^"]+)"/)?.[1]; |
|
|
| if (metadataUrl) { |
| try { |
| |
| const metadataResponse = await fetch(metadataUrl, { |
| signal: AbortSignal.timeout(mcpConfig.OAUTH_DETECTION_TIMEOUT), |
| }); |
| const metadata = await metadataResponse.json(); |
|
|
| if (metadata?.authorization_servers?.length) { |
| return { |
| requiresOAuth: true, |
| method: '401-challenge-metadata', |
| metadata, |
| }; |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| if (wwwAuth && /bearer/i.test(wwwAuth)) { |
| return { |
| requiresOAuth: true, |
| method: '401-challenge-metadata', |
| metadata: null, |
| }; |
| } |
|
|
| return null; |
| } catch { |
| return null; |
| } |
| } |
|
|
| |
| async function checkAuthErrorFallback(serverUrl: string): Promise<OAuthDetectionResult | null> { |
| try { |
| if (!mcpConfig.OAUTH_ON_AUTH_ERROR) return null; |
|
|
| const response = await fetch(serverUrl, { |
| method: 'HEAD', |
| signal: AbortSignal.timeout(mcpConfig.OAUTH_DETECTION_TIMEOUT), |
| }); |
|
|
| if (response.status !== 401 && response.status !== 403) return null; |
|
|
| return { |
| requiresOAuth: true, |
| method: 'no-metadata-found', |
| metadata: null, |
| }; |
| } catch { |
| return null; |
| } |
| } |
|
|