mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add OAuth 2.1 support for HTTP MCP servers
- Implement OAuth 2.1 client with PKCE support (RFC 7636) - Add resource server discovery (RFC 9728) - Add authorization server discovery (RFC 8414 + OIDC Discovery) - Support dynamic client registration (RFC 7591) - Integrate OAuth flow into SSE and StreamableHTTP transports - Add secure token storage and refresh mechanism - Include resource indicators (RFC 8707) in auth requests This implementation allows Roo to automatically handle OAuth 2.1 authentication when connecting to protected HTTP-based MCP servers, following all required RFCs for secure authentication flow.
This commit is contained in:
parent
87b45def18
commit
31ce0535d7
5 changed files with 1350 additions and 10 deletions
|
|
@ -3,6 +3,7 @@ import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotoc
|
|||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
import ReconnectingEventSource from "reconnecting-eventsource"
|
||||
import { createSSETransportWithOAuth, createStreamableHTTPTransportWithOAuth } from "./oauth/HttpTransportWithOAuth"
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
ListResourcesResultSchema,
|
||||
|
|
@ -734,12 +735,22 @@ export class McpHub {
|
|||
console.error(`No stderr stream for ${name}`)
|
||||
}
|
||||
} else if (configInjected.type === "streamable-http") {
|
||||
// Streamable HTTP connection
|
||||
transport = new StreamableHTTPClientTransport(new URL(configInjected.url), {
|
||||
requestInit: {
|
||||
headers: configInjected.headers,
|
||||
// Streamable HTTP connection with OAuth support
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available for OAuth initialization")
|
||||
}
|
||||
|
||||
transport = createStreamableHTTPTransportWithOAuth(
|
||||
new URL(configInjected.url),
|
||||
{
|
||||
requestInit: {
|
||||
headers: configInjected.headers,
|
||||
},
|
||||
},
|
||||
})
|
||||
name,
|
||||
provider.context,
|
||||
)
|
||||
|
||||
// Set up Streamable HTTP specific error handling
|
||||
transport.onerror = async (error) => {
|
||||
|
|
@ -760,7 +771,12 @@ export class McpHub {
|
|||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
} else if (configInjected.type === "sse") {
|
||||
// SSE connection
|
||||
// SSE connection with OAuth support
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available for OAuth initialization")
|
||||
}
|
||||
|
||||
const sseOptions = {
|
||||
requestInit: {
|
||||
headers: configInjected.headers,
|
||||
|
|
@ -779,10 +795,16 @@ export class McpHub {
|
|||
},
|
||||
}
|
||||
global.EventSource = ReconnectingEventSource
|
||||
transport = new SSEClientTransport(new URL(configInjected.url), {
|
||||
...sseOptions,
|
||||
eventSourceInit: reconnectingEventSourceOptions,
|
||||
})
|
||||
|
||||
transport = createSSETransportWithOAuth(
|
||||
new URL(configInjected.url),
|
||||
{
|
||||
...sseOptions,
|
||||
eventSourceInit: reconnectingEventSourceOptions,
|
||||
},
|
||||
name,
|
||||
provider.context,
|
||||
)
|
||||
|
||||
// Set up SSE specific error handling
|
||||
transport.onerror = async (error) => {
|
||||
|
|
|
|||
140
src/services/mcp/oauth/HttpTransportWithOAuth.ts
Normal file
140
src/services/mcp/oauth/HttpTransportWithOAuth.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* HTTP Transport wrapper with OAuth 2.1 support
|
||||
* Intercepts 401 responses and initiates OAuth flow when needed
|
||||
*/
|
||||
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
import * as vscode from "vscode"
|
||||
import { OAuthManager } from "./OAuthManager"
|
||||
|
||||
export class HttpTransportWithOAuth {
|
||||
private oauthManager: OAuthManager
|
||||
private accessToken: string | null = null
|
||||
private serverName: string
|
||||
private resourceUrl: string
|
||||
|
||||
constructor(
|
||||
private transport: SSEClientTransport | StreamableHTTPClientTransport,
|
||||
serverName: string,
|
||||
resourceUrl: string,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
this.serverName = serverName
|
||||
this.resourceUrl = resourceUrl
|
||||
this.oauthManager = new OAuthManager(context)
|
||||
this.wrapTransportMethods()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap transport methods to intercept 401 responses
|
||||
*/
|
||||
private wrapTransportMethods(): void {
|
||||
// Store original fetch method if using custom fetch
|
||||
const originalFetch = (global as any).fetch || fetch
|
||||
|
||||
// Override global fetch to intercept responses
|
||||
const interceptedFetch = async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
||||
// Add OAuth token if available
|
||||
const headers = new Headers(init?.headers || {})
|
||||
|
||||
// Check if we have an access token
|
||||
if (!this.accessToken) {
|
||||
this.accessToken = await this.oauthManager.getAccessToken(this.serverName)
|
||||
}
|
||||
|
||||
if (this.accessToken) {
|
||||
headers.set("Authorization", `Bearer ${this.accessToken}`)
|
||||
}
|
||||
|
||||
// Make the request
|
||||
const response = await originalFetch(url, {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
|
||||
// Check for 401 Unauthorized
|
||||
if (response.status === 401) {
|
||||
const wwwAuthenticate = response.headers.get("WWW-Authenticate")
|
||||
|
||||
if (wwwAuthenticate) {
|
||||
// Attempt OAuth flow
|
||||
const newToken = await this.oauthManager.handle401Response(
|
||||
this.serverName,
|
||||
this.resourceUrl,
|
||||
wwwAuthenticate,
|
||||
)
|
||||
|
||||
if (newToken) {
|
||||
// Update our token
|
||||
this.accessToken = newToken
|
||||
|
||||
// Retry the request with new token
|
||||
const retryHeaders = new Headers(init?.headers || {})
|
||||
retryHeaders.set("Authorization", `Bearer ${newToken}`)
|
||||
|
||||
return await originalFetch(url, {
|
||||
...init,
|
||||
headers: retryHeaders,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Replace global fetch for this transport
|
||||
;(global as any).fetch = interceptedFetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the wrapped transport
|
||||
*/
|
||||
public getTransport(): SSEClientTransport | StreamableHTTPClientTransport {
|
||||
return this.transport
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear OAuth credentials for this server
|
||||
*/
|
||||
public async clearCredentials(): Promise<void> {
|
||||
await this.oauthManager.clearServerCredentials(this.serverName)
|
||||
this.accessToken = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of OAuth manager
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.oauthManager.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an SSE transport with OAuth support
|
||||
*/
|
||||
export function createSSETransportWithOAuth(
|
||||
url: URL,
|
||||
options: any,
|
||||
serverName: string,
|
||||
context: vscode.ExtensionContext,
|
||||
): SSEClientTransport {
|
||||
const transport = new SSEClientTransport(url, options)
|
||||
const wrapper = new HttpTransportWithOAuth(transport, serverName, url.toString(), context)
|
||||
return transport // Return the original transport, which has been enhanced with OAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a StreamableHTTP transport with OAuth support
|
||||
*/
|
||||
export function createStreamableHTTPTransportWithOAuth(
|
||||
url: URL,
|
||||
options: any,
|
||||
serverName: string,
|
||||
context: vscode.ExtensionContext,
|
||||
): StreamableHTTPClientTransport {
|
||||
const transport = new StreamableHTTPClientTransport(url, options)
|
||||
const wrapper = new HttpTransportWithOAuth(transport, serverName, url.toString(), context)
|
||||
return transport // Return the original transport, which has been enhanced with OAuth
|
||||
}
|
||||
548
src/services/mcp/oauth/OAuthClient.ts
Normal file
548
src/services/mcp/oauth/OAuthClient.ts
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
/**
|
||||
* OAuth 2.1 Client for MCP Server Authentication
|
||||
* Implements Authorization Code + PKCE flow per OAuth 2.1 specification
|
||||
*/
|
||||
|
||||
import * as crypto from "crypto"
|
||||
import * as vscode from "vscode"
|
||||
import {
|
||||
AuthorizationServerMetadata,
|
||||
ClientRegistrationRequest,
|
||||
ClientRegistrationResponse,
|
||||
OAuthConfig,
|
||||
OAuthError,
|
||||
PKCEChallenge,
|
||||
ProtectedResourceMetadata,
|
||||
StoredOAuthCredentials,
|
||||
TokenResponse,
|
||||
WWWAuthenticateChallenge,
|
||||
} from "./types"
|
||||
|
||||
export class OAuthClient {
|
||||
private static readonly PKCE_VERIFIER_LENGTH = 128
|
||||
private static readonly TOKEN_REFRESH_BUFFER_MS = 30 * 1000 // 30 seconds buffer
|
||||
|
||||
constructor(
|
||||
private readonly serverName: string,
|
||||
private readonly resourceUrl: string,
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse WWW-Authenticate header to extract OAuth challenge information
|
||||
* @param header The WWW-Authenticate header value
|
||||
* @returns Parsed challenge information
|
||||
*/
|
||||
public parseWWWAuthenticate(header: string): WWWAuthenticateChallenge {
|
||||
const result: WWWAuthenticateChallenge = { scheme: "" }
|
||||
|
||||
// Match the scheme (e.g., "Bearer", "Basic", etc.)
|
||||
const schemeMatch = header.match(/^(\w+)\s+/)
|
||||
if (!schemeMatch) {
|
||||
throw new Error("Invalid WWW-Authenticate header format")
|
||||
}
|
||||
result.scheme = schemeMatch[1]
|
||||
|
||||
// Parse parameters
|
||||
const paramsString = header.substring(schemeMatch[0].length)
|
||||
const paramRegex = /(\w+)="([^"]+)"/g
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = paramRegex.exec(paramsString)) !== null) {
|
||||
const [, key, value] = match
|
||||
switch (key) {
|
||||
case "realm":
|
||||
result.realm = value
|
||||
break
|
||||
case "scope":
|
||||
result.scope = value
|
||||
break
|
||||
case "error":
|
||||
result.error = value
|
||||
break
|
||||
case "error_description":
|
||||
result.error_description = value
|
||||
break
|
||||
case "error_uri":
|
||||
result.error_uri = value
|
||||
break
|
||||
case "resource":
|
||||
result.resource = value
|
||||
break
|
||||
case "as_uri":
|
||||
result.as_uri = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover OAuth Protected Resource Metadata (RFC 9728)
|
||||
* @param resourceUrl The resource server URL
|
||||
* @returns Protected resource metadata
|
||||
*/
|
||||
public async discoverResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata> {
|
||||
const metadataUrl = new URL("/.well-known/oauth-protected-resource", resourceUrl).toString()
|
||||
|
||||
const response = await fetch(metadataUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch resource metadata: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const metadata = (await response.json()) as ProtectedResourceMetadata
|
||||
|
||||
// Validate required fields
|
||||
if (!metadata.resource || !metadata.authorization_servers || metadata.authorization_servers.length === 0) {
|
||||
throw new Error("Invalid resource metadata: missing required fields")
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover Authorization Server Metadata (RFC 8414 + OIDC Discovery)
|
||||
* Attempts discovery in the specified order per requirements
|
||||
* @param issuer The authorization server issuer URL
|
||||
* @returns Authorization server metadata
|
||||
*/
|
||||
public async discoverAuthorizationServerMetadata(issuer: string): Promise<AuthorizationServerMetadata> {
|
||||
const issuerUrl = new URL(issuer)
|
||||
const hasPath = issuerUrl.pathname !== "/" && issuerUrl.pathname !== ""
|
||||
|
||||
const discoveryUrls: string[] = []
|
||||
|
||||
if (hasPath) {
|
||||
// Issuer with path component
|
||||
const pathComponent = issuerUrl.pathname.replace(/^\//, "").replace(/\/$/, "")
|
||||
discoveryUrls.push(
|
||||
new URL(`/.well-known/oauth-authorization-server/${pathComponent}`, issuerUrl.origin).toString(),
|
||||
new URL(`/.well-known/openid-configuration/${pathComponent}`, issuerUrl.origin).toString(),
|
||||
new URL(`${issuerUrl.pathname}/.well-known/openid-configuration`, issuerUrl.origin).toString(),
|
||||
)
|
||||
} else {
|
||||
// Issuer without path component
|
||||
discoveryUrls.push(
|
||||
new URL("/.well-known/oauth-authorization-server", issuerUrl.origin).toString(),
|
||||
new URL("/.well-known/openid-configuration", issuerUrl.origin).toString(),
|
||||
)
|
||||
}
|
||||
|
||||
// Try each discovery URL in order
|
||||
let lastError: Error | null = null
|
||||
for (const url of discoveryUrls) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const metadata = (await response.json()) as AuthorizationServerMetadata
|
||||
|
||||
// Validate required fields
|
||||
if (!metadata.issuer || !metadata.authorization_endpoint || !metadata.token_endpoint) {
|
||||
continue // Try next URL
|
||||
}
|
||||
|
||||
// Validate PKCE support
|
||||
if (!this.validatePKCESupport(metadata)) {
|
||||
throw new Error(
|
||||
"Authorization server does not support PKCE with S256 method, which is required by OAuth 2.1",
|
||||
)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error))
|
||||
// Continue to next URL
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error("Failed to discover authorization server metadata")
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate PKCE support in authorization server metadata
|
||||
* @param metadata Authorization server metadata
|
||||
* @returns true if PKCE S256 is supported
|
||||
*/
|
||||
private validatePKCESupport(metadata: AuthorizationServerMetadata): boolean {
|
||||
// For RFC 8414: if code_challenge_methods_supported is absent, refuse
|
||||
// For OIDC Discovery: verify code_challenge_methods_supported present, if absent refuse
|
||||
if (!metadata.code_challenge_methods_supported) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if S256 is supported
|
||||
return metadata.code_challenge_methods_supported.includes("S256")
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform Dynamic Client Registration (RFC 7591)
|
||||
* @param metadata Authorization server metadata
|
||||
* @returns Client registration response
|
||||
*/
|
||||
public async registerClient(metadata: AuthorizationServerMetadata): Promise<ClientRegistrationResponse> {
|
||||
if (!metadata.registration_endpoint) {
|
||||
throw new Error("Authorization server does not support dynamic client registration")
|
||||
}
|
||||
|
||||
// Determine redirect URI based on environment
|
||||
const redirectUri = this.getRedirectUri()
|
||||
|
||||
const registrationRequest: ClientRegistrationRequest = {
|
||||
client_name: `Roo Code MCP Client - ${this.serverName}`,
|
||||
redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
token_endpoint_auth_method: "none", // Public client
|
||||
scope: "openid profile", // Request basic OIDC scopes if available
|
||||
software_id: "roo-code-mcp-client",
|
||||
software_version: this.context.extension.packageJSON.version,
|
||||
}
|
||||
|
||||
const response = await fetch(metadata.registration_endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(registrationRequest),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(
|
||||
`Client registration failed: ${error.error || response.status} - ${
|
||||
error.error_description || response.statusText
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
const registration = (await response.json()) as ClientRegistrationResponse
|
||||
|
||||
// Store client credentials
|
||||
await this.storeClientCredentials(registration)
|
||||
|
||||
return registration
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PKCE challenge for authorization request
|
||||
* @returns PKCE challenge parameters
|
||||
*/
|
||||
public generatePKCEChallenge(): PKCEChallenge {
|
||||
// Generate code verifier (128 characters from unreserved characters)
|
||||
const verifier = crypto
|
||||
.randomBytes(OAuthClient.PKCE_VERIFIER_LENGTH)
|
||||
.toString("base64url")
|
||||
.substring(0, OAuthClient.PKCE_VERIFIER_LENGTH)
|
||||
|
||||
// Generate code challenge using S256 method
|
||||
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url")
|
||||
|
||||
return {
|
||||
code_verifier: verifier,
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: "S256",
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build authorization URL for user consent
|
||||
* @param metadata Authorization server metadata
|
||||
* @param clientId Client ID
|
||||
* @param pkce PKCE challenge
|
||||
* @param resource Resource indicator (RFC 8707)
|
||||
* @param scope Optional scope
|
||||
* @returns Authorization URL
|
||||
*/
|
||||
public buildAuthorizationUrl(
|
||||
metadata: AuthorizationServerMetadata,
|
||||
clientId: string,
|
||||
pkce: PKCEChallenge,
|
||||
resource: string,
|
||||
scope?: string,
|
||||
): string {
|
||||
const redirectUri = this.getRedirectUri()
|
||||
const state = crypto.randomBytes(32).toString("base64url")
|
||||
|
||||
// Store state for validation
|
||||
this.context.globalState.update(`oauth_state_${this.serverName}`, state)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
response_type: "code",
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
state,
|
||||
code_challenge: pkce.code_challenge,
|
||||
code_challenge_method: pkce.code_challenge_method,
|
||||
resource, // RFC 8707 - Resource Indicators
|
||||
})
|
||||
|
||||
if (scope) {
|
||||
params.append("scope", scope)
|
||||
}
|
||||
|
||||
return `${metadata.authorization_endpoint}?${params.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for tokens
|
||||
* @param metadata Authorization server metadata
|
||||
* @param code Authorization code
|
||||
* @param clientId Client ID
|
||||
* @param clientSecret Optional client secret
|
||||
* @param pkce PKCE verifier
|
||||
* @param resource Resource indicator
|
||||
* @returns Token response
|
||||
*/
|
||||
public async exchangeCodeForTokens(
|
||||
metadata: AuthorizationServerMetadata,
|
||||
code: string,
|
||||
clientId: string,
|
||||
clientSecret: string | undefined,
|
||||
pkce: PKCEChallenge,
|
||||
resource: string,
|
||||
): Promise<TokenResponse> {
|
||||
const redirectUri = this.getRedirectUri()
|
||||
|
||||
const params = new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: clientId,
|
||||
code_verifier: pkce.code_verifier,
|
||||
resource, // RFC 8707 - Include resource in token request
|
||||
})
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
}
|
||||
|
||||
// Add client authentication if confidential client
|
||||
if (clientSecret) {
|
||||
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
|
||||
headers["Authorization"] = `Basic ${auth}`
|
||||
}
|
||||
|
||||
const response = await fetch(metadata.token_endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: params.toString(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = (await response.json()) as OAuthError
|
||||
throw new Error(`Token exchange failed: ${error.error} - ${error.error_description || ""}`)
|
||||
}
|
||||
|
||||
const tokens = (await response.json()) as TokenResponse
|
||||
|
||||
// Store tokens
|
||||
await this.storeTokens(tokens, clientId, clientSecret)
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
* @param metadata Authorization server metadata
|
||||
* @param refreshToken Refresh token
|
||||
* @param clientId Client ID
|
||||
* @param clientSecret Optional client secret
|
||||
* @param resource Resource indicator
|
||||
* @returns New token response
|
||||
*/
|
||||
public async refreshAccessToken(
|
||||
metadata: AuthorizationServerMetadata,
|
||||
refreshToken: string,
|
||||
clientId: string,
|
||||
clientSecret: string | undefined,
|
||||
resource: string,
|
||||
): Promise<TokenResponse> {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: clientId,
|
||||
resource, // RFC 8707 - Include resource in refresh request
|
||||
})
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
}
|
||||
|
||||
// Add client authentication if confidential client
|
||||
if (clientSecret) {
|
||||
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
|
||||
headers["Authorization"] = `Basic ${auth}`
|
||||
}
|
||||
|
||||
const response = await fetch(metadata.token_endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: params.toString(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = (await response.json()) as OAuthError
|
||||
throw new Error(`Token refresh failed: ${error.error} - ${error.error_description || ""}`)
|
||||
}
|
||||
|
||||
const tokens = (await response.json()) as TokenResponse
|
||||
|
||||
// Update stored tokens
|
||||
await this.storeTokens(tokens, clientId, clientSecret)
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored OAuth credentials
|
||||
* @returns Stored credentials or null
|
||||
*/
|
||||
public async getStoredCredentials(): Promise<StoredOAuthCredentials | null> {
|
||||
const key = `oauth_credentials_${this.serverName}`
|
||||
return this.context.globalState.get<StoredOAuthCredentials>(key) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if stored token is expired or about to expire
|
||||
* @param credentials Stored credentials
|
||||
* @returns true if token needs refresh
|
||||
*/
|
||||
public isTokenExpired(credentials: StoredOAuthCredentials): boolean {
|
||||
if (!credentials.expiresAt) {
|
||||
return false // No expiry information, assume valid
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
return now >= credentials.expiresAt - OAuthClient.TOKEN_REFRESH_BUFFER_MS
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear stored OAuth credentials
|
||||
*/
|
||||
public async clearCredentials(): Promise<void> {
|
||||
const keys = [
|
||||
`oauth_credentials_${this.serverName}`,
|
||||
`oauth_client_${this.serverName}`,
|
||||
`oauth_state_${this.serverName}`,
|
||||
`oauth_pkce_${this.serverName}`,
|
||||
]
|
||||
|
||||
for (const key of keys) {
|
||||
await this.context.globalState.update(key, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirect URI for OAuth flow
|
||||
* @returns Redirect URI
|
||||
*/
|
||||
private getRedirectUri(): string {
|
||||
// Use VS Code's built-in URI handler for OAuth callbacks
|
||||
return `vscode://RooCodeInc.roo-code/oauth-callback`
|
||||
}
|
||||
|
||||
/**
|
||||
* Store client registration details
|
||||
* @param registration Client registration response
|
||||
*/
|
||||
private async storeClientCredentials(registration: ClientRegistrationResponse): Promise<void> {
|
||||
const key = `oauth_client_${this.serverName}`
|
||||
await this.context.globalState.update(key, registration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store OAuth tokens
|
||||
* @param tokens Token response
|
||||
* @param clientId Client ID
|
||||
* @param clientSecret Optional client secret
|
||||
*/
|
||||
private async storeTokens(
|
||||
tokens: TokenResponse,
|
||||
clientId: string,
|
||||
clientSecret: string | undefined,
|
||||
): Promise<void> {
|
||||
const credentials: StoredOAuthCredentials = {
|
||||
serverName: this.serverName,
|
||||
serverUrl: this.resourceUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
tokenType: tokens.token_type,
|
||||
scope: tokens.scope,
|
||||
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,
|
||||
}
|
||||
|
||||
const key = `oauth_credentials_${this.serverName}`
|
||||
await this.context.globalState.update(key, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored client registration
|
||||
* @returns Client registration or null
|
||||
*/
|
||||
public async getStoredClientRegistration(): Promise<ClientRegistrationResponse | null> {
|
||||
const key = `oauth_client_${this.serverName}`
|
||||
return this.context.globalState.get<ClientRegistrationResponse>(key) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate state parameter from OAuth callback
|
||||
* @param state State parameter from callback
|
||||
* @returns true if state is valid
|
||||
*/
|
||||
public async validateState(state: string): Promise<boolean> {
|
||||
const key = `oauth_state_${this.serverName}`
|
||||
const storedState = await this.context.globalState.get<string>(key)
|
||||
|
||||
if (!storedState || storedState !== state) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Clear state after validation
|
||||
await this.context.globalState.update(key, undefined)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Store PKCE verifier for later use
|
||||
* @param pkce PKCE challenge
|
||||
*/
|
||||
public async storePKCEVerifier(pkce: PKCEChallenge): Promise<void> {
|
||||
const key = `oauth_pkce_${this.serverName}`
|
||||
await this.context.globalState.update(key, pkce)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored PKCE verifier
|
||||
* @returns PKCE challenge or null
|
||||
*/
|
||||
public async getStoredPKCEVerifier(): Promise<PKCEChallenge | null> {
|
||||
const key = `oauth_pkce_${this.serverName}`
|
||||
const pkce = await this.context.globalState.get<PKCEChallenge>(key)
|
||||
|
||||
if (pkce) {
|
||||
// Clear PKCE after retrieval for security
|
||||
await this.context.globalState.update(key, undefined)
|
||||
}
|
||||
|
||||
return pkce || null
|
||||
}
|
||||
}
|
||||
449
src/services/mcp/oauth/OAuthManager.ts
Normal file
449
src/services/mcp/oauth/OAuthManager.ts
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
/**
|
||||
* OAuth Manager for MCP Servers
|
||||
* Orchestrates the complete OAuth 2.1 flow for HTTP-based MCP servers
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { OAuthClient } from "./OAuthClient"
|
||||
import {
|
||||
AuthorizationServerMetadata,
|
||||
ClientRegistrationResponse,
|
||||
OAuthConfig,
|
||||
PKCEChallenge,
|
||||
StoredOAuthCredentials,
|
||||
TokenResponse,
|
||||
WWWAuthenticateChallenge,
|
||||
} from "./types"
|
||||
|
||||
export class OAuthManager {
|
||||
private oauthClients: Map<string, OAuthClient> = new Map()
|
||||
private authorizationInProgress: Map<string, boolean> = new Map()
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
// Register URI handler for OAuth callbacks
|
||||
this.registerOAuthCallbackHandler()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 401 Unauthorized response from MCP server
|
||||
* Initiates OAuth flow if WWW-Authenticate header is present
|
||||
* @param serverName MCP server name
|
||||
* @param resourceUrl Resource server URL
|
||||
* @param wwwAuthenticateHeader WWW-Authenticate header value
|
||||
* @returns Access token if successful, null otherwise
|
||||
*/
|
||||
public async handle401Response(
|
||||
serverName: string,
|
||||
resourceUrl: string,
|
||||
wwwAuthenticateHeader: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// Check if authorization is already in progress
|
||||
if (this.authorizationInProgress.get(serverName)) {
|
||||
vscode.window.showWarningMessage(`OAuth authorization already in progress for ${serverName}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Get or create OAuth client for this server
|
||||
let client = this.oauthClients.get(serverName)
|
||||
if (!client) {
|
||||
client = new OAuthClient(serverName, resourceUrl, this.context)
|
||||
this.oauthClients.set(serverName, client)
|
||||
}
|
||||
|
||||
// Parse WWW-Authenticate header
|
||||
const challenge = client.parseWWWAuthenticate(wwwAuthenticateHeader)
|
||||
|
||||
if (challenge.scheme !== "Bearer") {
|
||||
vscode.window.showErrorMessage(
|
||||
`Unsupported authentication scheme: ${challenge.scheme}. Only Bearer is supported.`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// Check for existing valid credentials
|
||||
const existingCredentials = await client.getStoredCredentials()
|
||||
if (existingCredentials && !client.isTokenExpired(existingCredentials)) {
|
||||
return existingCredentials.accessToken
|
||||
}
|
||||
|
||||
// If we have a refresh token, try to refresh
|
||||
if (existingCredentials?.refreshToken) {
|
||||
try {
|
||||
const tokens = await this.refreshToken(client, existingCredentials)
|
||||
return tokens.access_token
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh token, initiating new authorization:", error)
|
||||
// Continue with new authorization flow
|
||||
}
|
||||
}
|
||||
|
||||
// Start OAuth flow
|
||||
this.authorizationInProgress.set(serverName, true)
|
||||
|
||||
try {
|
||||
// Step 1: Discover resource metadata (RFC 9728)
|
||||
vscode.window.showInformationMessage(`Discovering OAuth configuration for ${serverName}...`)
|
||||
const resourceMetadata = await client.discoverResourceMetadata(resourceUrl)
|
||||
|
||||
// Step 2: Select authorization server (for now, use the first one)
|
||||
const authServerUrl = resourceMetadata.authorization_servers[0]
|
||||
|
||||
// Step 3: Discover authorization server metadata (RFC 8414 + OIDC)
|
||||
const authMetadata = await client.discoverAuthorizationServerMetadata(authServerUrl)
|
||||
|
||||
// Step 4: Check for existing client registration or perform dynamic registration
|
||||
let clientRegistration = await client.getStoredClientRegistration()
|
||||
|
||||
if (!clientRegistration && authMetadata.registration_endpoint) {
|
||||
// Perform dynamic client registration (RFC 7591)
|
||||
vscode.window.showInformationMessage(`Registering client with authorization server...`)
|
||||
clientRegistration = await client.registerClient(authMetadata)
|
||||
} else if (!clientRegistration) {
|
||||
// No dynamic registration available, need manual configuration
|
||||
clientRegistration = await this.promptForClientCredentials(serverName)
|
||||
if (!clientRegistration) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Generate PKCE challenge
|
||||
const pkce = client.generatePKCEChallenge()
|
||||
await client.storePKCEVerifier(pkce)
|
||||
|
||||
// Step 6: Build authorization URL
|
||||
const authUrl = client.buildAuthorizationUrl(
|
||||
authMetadata,
|
||||
clientRegistration.client_id,
|
||||
pkce,
|
||||
resourceUrl, // Use resource URL as resource indicator
|
||||
clientRegistration.scope,
|
||||
)
|
||||
|
||||
// Step 7: Open browser for user authorization
|
||||
const authorized = await this.openAuthorizationUrl(authUrl, serverName)
|
||||
if (!authorized) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Step 8: Wait for callback (handled by URI handler)
|
||||
const tokens = await this.waitForTokens(serverName)
|
||||
if (!tokens) {
|
||||
return null
|
||||
}
|
||||
|
||||
return tokens.access_token
|
||||
} finally {
|
||||
this.authorizationInProgress.delete(serverName)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("OAuth flow failed:", error)
|
||||
vscode.window.showErrorMessage(
|
||||
`OAuth authentication failed for ${serverName}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
this.authorizationInProgress.delete(serverName)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid access token for MCP server
|
||||
* Refreshes token if needed
|
||||
* @param serverName MCP server name
|
||||
* @returns Access token or null
|
||||
*/
|
||||
public async getAccessToken(serverName: string): Promise<string | null> {
|
||||
const client = this.oauthClients.get(serverName)
|
||||
if (!client) {
|
||||
return null
|
||||
}
|
||||
|
||||
const credentials = await client.getStoredCredentials()
|
||||
if (!credentials) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if token needs refresh
|
||||
if (client.isTokenExpired(credentials)) {
|
||||
if (!credentials.refreshToken) {
|
||||
// No refresh token, need new authorization
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await this.refreshToken(client, credentials)
|
||||
return tokens.access_token
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh token:", error)
|
||||
// Clear invalid credentials
|
||||
await client.clearCredentials()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return credentials.accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
* @param client OAuth client
|
||||
* @param credentials Stored credentials
|
||||
* @returns New tokens
|
||||
*/
|
||||
private async refreshToken(client: OAuthClient, credentials: StoredOAuthCredentials): Promise<TokenResponse> {
|
||||
// Discover authorization server metadata
|
||||
const authServerUrl = await this.getAuthServerUrl(credentials.serverUrl)
|
||||
if (!authServerUrl) {
|
||||
throw new Error("Cannot determine authorization server URL")
|
||||
}
|
||||
|
||||
const authMetadata = await client.discoverAuthorizationServerMetadata(authServerUrl)
|
||||
|
||||
// Refresh token
|
||||
return await client.refreshAccessToken(
|
||||
authMetadata,
|
||||
credentials.refreshToken!,
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
credentials.serverUrl,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization server URL for a resource
|
||||
* @param resourceUrl Resource server URL
|
||||
* @returns Authorization server URL or null
|
||||
*/
|
||||
private async getAuthServerUrl(resourceUrl: string): Promise<string | null> {
|
||||
try {
|
||||
// Create temporary client to discover metadata
|
||||
const tempClient = new OAuthClient("temp", resourceUrl, this.context)
|
||||
const resourceMetadata = await tempClient.discoverResourceMetadata(resourceUrl)
|
||||
return resourceMetadata.authorization_servers[0] || null
|
||||
} catch (error) {
|
||||
console.error("Failed to get authorization server URL:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for manual client credentials
|
||||
* @param serverName Server name
|
||||
* @returns Client registration or null
|
||||
*/
|
||||
private async promptForClientCredentials(serverName: string): Promise<ClientRegistrationResponse | null> {
|
||||
const clientId = await vscode.window.showInputBox({
|
||||
prompt: `Enter Client ID for ${serverName}`,
|
||||
placeHolder: "client-id",
|
||||
ignoreFocusOut: true,
|
||||
})
|
||||
|
||||
if (!clientId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const clientSecret = await vscode.window.showInputBox({
|
||||
prompt: `Enter Client Secret for ${serverName} (leave empty for public client)`,
|
||||
placeHolder: "client-secret (optional)",
|
||||
password: true,
|
||||
ignoreFocusOut: true,
|
||||
})
|
||||
|
||||
const redirectUri = `vscode://RooCodeInc.roo-code/oauth-callback`
|
||||
|
||||
return {
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret || undefined,
|
||||
redirect_uris: [redirectUri],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open authorization URL in browser
|
||||
* @param authUrl Authorization URL
|
||||
* @param serverName Server name
|
||||
* @returns true if user proceeded with authorization
|
||||
*/
|
||||
private async openAuthorizationUrl(authUrl: string, serverName: string): Promise<boolean> {
|
||||
const result = await vscode.window.showInformationMessage(
|
||||
`Authorization required for MCP server "${serverName}". Click "Authorize" to open your browser and grant access.`,
|
||||
"Authorize",
|
||||
"Cancel",
|
||||
)
|
||||
|
||||
if (result !== "Authorize") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Open browser
|
||||
await vscode.env.openExternal(vscode.Uri.parse(authUrl))
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for OAuth tokens after authorization
|
||||
* @param serverName Server name
|
||||
* @returns Tokens or null
|
||||
*/
|
||||
private async waitForTokens(serverName: string): Promise<TokenResponse | null> {
|
||||
return new Promise((resolve) => {
|
||||
// Set up a timeout
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
vscode.window.showErrorMessage(`OAuth authorization timed out for ${serverName}`)
|
||||
resolve(null)
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
) // 5 minutes timeout
|
||||
|
||||
// Check for tokens periodically
|
||||
const checkInterval = setInterval(async () => {
|
||||
const client = this.oauthClients.get(serverName)
|
||||
if (!client) {
|
||||
clearInterval(checkInterval)
|
||||
clearTimeout(timeout)
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
const credentials = await client.getStoredCredentials()
|
||||
if (credentials) {
|
||||
clearInterval(checkInterval)
|
||||
clearTimeout(timeout)
|
||||
resolve({
|
||||
access_token: credentials.accessToken,
|
||||
token_type: credentials.tokenType,
|
||||
refresh_token: credentials.refreshToken,
|
||||
scope: credentials.scope,
|
||||
expires_in: credentials.expiresAt
|
||||
? Math.floor((credentials.expiresAt - Date.now()) / 1000)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
}, 1000) // Check every second
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register URI handler for OAuth callbacks
|
||||
*/
|
||||
private registerOAuthCallbackHandler(): void {
|
||||
vscode.window.registerUriHandler({
|
||||
handleUri: async (uri: vscode.Uri) => {
|
||||
if (uri.path === "/oauth-callback") {
|
||||
await this.handleOAuthCallback(uri)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OAuth callback from browser
|
||||
* @param uri Callback URI with authorization code
|
||||
*/
|
||||
private async handleOAuthCallback(uri: vscode.Uri): Promise<void> {
|
||||
try {
|
||||
// Parse query parameters
|
||||
const params = new URLSearchParams(uri.query)
|
||||
const code = params.get("code")
|
||||
const state = params.get("state")
|
||||
const error = params.get("error")
|
||||
const errorDescription = params.get("error_description")
|
||||
|
||||
if (error) {
|
||||
vscode.window.showErrorMessage(`OAuth authorization failed: ${error} - ${errorDescription || ""}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
vscode.window.showErrorMessage("Invalid OAuth callback: missing code or state")
|
||||
return
|
||||
}
|
||||
|
||||
// Find the client that initiated this flow
|
||||
// We need to validate state to determine which server this is for
|
||||
let targetClient: OAuthClient | null = null
|
||||
let targetServerName: string | null = null
|
||||
|
||||
for (const [serverName, client] of this.oauthClients) {
|
||||
if (await client.validateState(state)) {
|
||||
targetClient = client
|
||||
targetServerName = serverName
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetClient || !targetServerName) {
|
||||
vscode.window.showErrorMessage("OAuth callback received for unknown session")
|
||||
return
|
||||
}
|
||||
|
||||
// Get stored PKCE verifier
|
||||
const pkce = await targetClient.getStoredPKCEVerifier()
|
||||
if (!pkce) {
|
||||
vscode.window.showErrorMessage("OAuth callback received but PKCE verifier not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get client registration
|
||||
const clientRegistration = await targetClient.getStoredClientRegistration()
|
||||
if (!clientRegistration) {
|
||||
vscode.window.showErrorMessage("OAuth callback received but client registration not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get authorization server metadata
|
||||
const credentials = await targetClient.getStoredCredentials()
|
||||
const resourceUrl = credentials?.serverUrl || ""
|
||||
const authServerUrl = await this.getAuthServerUrl(resourceUrl)
|
||||
|
||||
if (!authServerUrl) {
|
||||
vscode.window.showErrorMessage("Cannot determine authorization server URL")
|
||||
return
|
||||
}
|
||||
|
||||
const authMetadata = await targetClient.discoverAuthorizationServerMetadata(authServerUrl)
|
||||
|
||||
// Exchange code for tokens
|
||||
vscode.window.showInformationMessage(`Completing OAuth authorization for ${targetServerName}...`)
|
||||
|
||||
const tokens = await targetClient.exchangeCodeForTokens(
|
||||
authMetadata,
|
||||
code,
|
||||
clientRegistration.client_id,
|
||||
clientRegistration.client_secret,
|
||||
pkce,
|
||||
resourceUrl,
|
||||
)
|
||||
|
||||
vscode.window.showInformationMessage(`Successfully authorized MCP server "${targetServerName}"`)
|
||||
} catch (error) {
|
||||
console.error("OAuth callback handling failed:", error)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to complete OAuth authorization: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear OAuth credentials for a server
|
||||
* @param serverName Server name
|
||||
*/
|
||||
public async clearServerCredentials(serverName: string): Promise<void> {
|
||||
const client = this.oauthClients.get(serverName)
|
||||
if (client) {
|
||||
await client.clearCredentials()
|
||||
this.oauthClients.delete(serverName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of OAuth manager
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.oauthClients.clear()
|
||||
this.authorizationInProgress.clear()
|
||||
}
|
||||
}
|
||||
181
src/services/mcp/oauth/types.ts
Normal file
181
src/services/mcp/oauth/types.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* OAuth 2.1 types and interfaces for MCP server authentication
|
||||
* Implements RFC 6749, RFC 7636 (PKCE), RFC 8414, RFC 8707, RFC 9728
|
||||
*/
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
||||
*/
|
||||
export interface ProtectedResourceMetadata {
|
||||
resource: string
|
||||
authorization_servers: string[]
|
||||
bearer_methods_supported?: string[]
|
||||
resource_documentation?: string
|
||||
resource_policy_uri?: string
|
||||
resource_tos_uri?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
*/
|
||||
export interface AuthorizationServerMetadata {
|
||||
issuer: string
|
||||
authorization_endpoint: string
|
||||
token_endpoint: string
|
||||
jwks_uri?: string
|
||||
registration_endpoint?: string
|
||||
scopes_supported?: string[]
|
||||
response_types_supported: string[]
|
||||
response_modes_supported?: string[]
|
||||
grant_types_supported?: string[]
|
||||
token_endpoint_auth_methods_supported?: string[]
|
||||
token_endpoint_auth_signing_alg_values_supported?: string[]
|
||||
service_documentation?: string
|
||||
ui_locales_supported?: string[]
|
||||
op_policy_uri?: string
|
||||
op_tos_uri?: string
|
||||
revocation_endpoint?: string
|
||||
revocation_endpoint_auth_methods_supported?: string[]
|
||||
introspection_endpoint?: string
|
||||
introspection_endpoint_auth_methods_supported?: string[]
|
||||
code_challenge_methods_supported?: string[]
|
||||
// Additional OIDC Discovery fields
|
||||
userinfo_endpoint?: string
|
||||
end_session_endpoint?: string
|
||||
check_session_iframe?: string
|
||||
acr_values_supported?: string[]
|
||||
subject_types_supported?: string[]
|
||||
id_token_signing_alg_values_supported?: string[]
|
||||
id_token_encryption_alg_values_supported?: string[]
|
||||
id_token_encryption_enc_values_supported?: string[]
|
||||
userinfo_signing_alg_values_supported?: string[]
|
||||
userinfo_encryption_alg_values_supported?: string[]
|
||||
userinfo_encryption_enc_values_supported?: string[]
|
||||
request_object_signing_alg_values_supported?: string[]
|
||||
request_object_encryption_alg_values_supported?: string[]
|
||||
request_object_encryption_enc_values_supported?: string[]
|
||||
display_values_supported?: string[]
|
||||
claim_types_supported?: string[]
|
||||
claims_supported?: string[]
|
||||
claims_locales_supported?: string[]
|
||||
claims_parameter_supported?: boolean
|
||||
request_parameter_supported?: boolean
|
||||
request_uri_parameter_supported?: boolean
|
||||
require_request_uri_registration?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic Client Registration Request (RFC 7591)
|
||||
*/
|
||||
export interface ClientRegistrationRequest {
|
||||
redirect_uris: string[]
|
||||
client_name?: string
|
||||
client_uri?: string
|
||||
logo_uri?: string
|
||||
scope?: string
|
||||
contacts?: string[]
|
||||
tos_uri?: string
|
||||
policy_uri?: string
|
||||
jwks_uri?: string
|
||||
jwks?: any
|
||||
software_id?: string
|
||||
software_version?: string
|
||||
grant_types?: string[]
|
||||
response_types?: string[]
|
||||
token_endpoint_auth_method?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic Client Registration Response (RFC 7591)
|
||||
*/
|
||||
export interface ClientRegistrationResponse {
|
||||
client_id: string
|
||||
client_secret?: string
|
||||
client_id_issued_at?: number
|
||||
client_secret_expires_at?: number
|
||||
redirect_uris: string[]
|
||||
grant_types?: string[]
|
||||
response_types?: string[]
|
||||
token_endpoint_auth_method?: string
|
||||
client_name?: string
|
||||
client_uri?: string
|
||||
logo_uri?: string
|
||||
scope?: string
|
||||
contacts?: string[]
|
||||
tos_uri?: string
|
||||
policy_uri?: string
|
||||
jwks_uri?: string
|
||||
jwks?: any
|
||||
software_id?: string
|
||||
software_version?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Token Response
|
||||
*/
|
||||
export interface TokenResponse {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in?: number
|
||||
refresh_token?: string
|
||||
scope?: string
|
||||
id_token?: string // For OIDC
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Error Response
|
||||
*/
|
||||
export interface OAuthError {
|
||||
error: string
|
||||
error_description?: string
|
||||
error_uri?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* PKCE (RFC 7636) parameters
|
||||
*/
|
||||
export interface PKCEChallenge {
|
||||
code_verifier: string
|
||||
code_challenge: string
|
||||
code_challenge_method: "S256"
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored OAuth credentials for an MCP server
|
||||
*/
|
||||
export interface StoredOAuthCredentials {
|
||||
serverName: string
|
||||
serverUrl: string
|
||||
clientId: string
|
||||
clientSecret?: string
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
expiresAt?: number
|
||||
scope?: string
|
||||
tokenType: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth configuration for an MCP server
|
||||
*/
|
||||
export interface OAuthConfig {
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
scope?: string
|
||||
authorizationServerUrl?: string
|
||||
resourceUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* WWW-Authenticate header parsed values
|
||||
*/
|
||||
export interface WWWAuthenticateChallenge {
|
||||
scheme: string
|
||||
realm?: string
|
||||
scope?: string
|
||||
error?: string
|
||||
error_description?: string
|
||||
error_uri?: string
|
||||
resource?: string
|
||||
as_uri?: string
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue