diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index bb3b56f2e95..57cab5a271a 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -1,5 +1,5 @@ import { Log } from "../util/log" -import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider" +import { OAUTH_CALLBACK_PATH, OAUTH_CALLBACK_PORT } from "./oauth-provider" const log = Log.create({ service: "mcp.oauth-callback" }) @@ -161,7 +161,7 @@ export namespace McpOAuthCallback { export async function isPortInUse(): Promise { return new Promise((resolve) => { Bun.connect({ - hostname: "127.0.0.1", + hostname: "localhost", port: OAUTH_CALLBACK_PORT, socket: { open(socket) { diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 35ead25e8be..c7961a164d8 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -1,12 +1,12 @@ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js" import type { - OAuthClientMetadata, - OAuthTokens, OAuthClientInformation, OAuthClientInformationFull, + OAuthClientMetadata, + OAuthTokens, } from "@modelcontextprotocol/sdk/shared/auth.js" -import { McpAuth } from "./auth" import { Log } from "../util/log" +import { McpAuth } from "./auth" const log = Log.create({ service: "mcp.oauth" }) @@ -32,7 +32,7 @@ export class McpOAuthProvider implements OAuthClientProvider { ) {} get redirectUrl(): string { - return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}` + return `http://localhost:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}` } get clientMetadata(): OAuthClientMetadata { @@ -121,9 +121,45 @@ export class McpOAuthProvider implements OAuthClientProvider { log.info("saved oauth tokens", { mcpName: this.mcpName }) } + async validateResourceURL(serverUrl: string | URL, resource?: string): Promise { + // For Azure AD, disable resource parameter to use scope-based authentication (v2.0) + // Azure AD v2.0 doesn't allow both resource and scope parameters + const url = new URL(typeof serverUrl === "string" ? serverUrl : serverUrl.toString()) + if (url.hostname === "login.microsoftonline.com") { + log.info("azure ad detected, disabling resource parameter", { mcpName: this.mcpName }) + return undefined + } + // For other providers, use the resource if provided + return resource ? new URL(resource) : undefined + } + + addClientAuthentication = (_headers: Headers, params: URLSearchParams, url: string | URL, _metadata?: any): Promise => { + // For Azure AD token endpoint, ensure client_id is in the request body + // and remove resource parameter if present + return (async () => { + const endpoint = new URL(typeof url === "string" ? url : url.toString()) + if (endpoint.hostname === "login.microsoftonline.com") { + // Ensure client_id is in the request body for public clients + if (this.config.clientId && !params.has("client_id")) { + params.set("client_id", this.config.clientId) + log.info("added client_id to token request", { mcpName: this.mcpName }) + } + // Remove resource parameter - v2.0 uses scope only + params.delete("resource") + log.info("removed resource parameter from token request", { mcpName: this.mcpName }) + } + })() + } + async redirectToAuthorization(authorizationUrl: URL): Promise { - log.info("redirecting to authorization", { mcpName: this.mcpName, url: authorizationUrl.toString() }) - await this.callbacks.onRedirect(authorizationUrl) + const url = new URL(authorizationUrl.toString()) + + if (url.hostname === "login.microsoftonline.com") { + url.searchParams.delete("resource") + } + + log.info("redirecting to authorization", { mcpName: this.mcpName, url: url.toString() }) + await this.callbacks.onRedirect(url) } async saveCodeVerifier(codeVerifier: string): Promise { @@ -151,4 +187,5 @@ export class McpOAuthProvider implements OAuthClientProvider { } } -export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } +export { OAUTH_CALLBACK_PATH, OAUTH_CALLBACK_PORT } + diff --git a/packages/opencode/test/mcp/oauth-resource.test.ts b/packages/opencode/test/mcp/oauth-resource.test.ts new file mode 100644 index 00000000000..ab6e93c1c4b --- /dev/null +++ b/packages/opencode/test/mcp/oauth-resource.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "bun:test" + +import { McpOAuthProvider } from "../../src/mcp/oauth-provider" + +test("McpOAuthProvider omits resource for login.microsoftonline.com", async () => { + let captured: URL | undefined + + const provider = new McpOAuthProvider( + "test", + "http://localhost:3000", + {}, + { + onRedirect: async (url) => { + captured = url + }, + }, + ) + + const url = new URL( + "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=x&resource=http%3A%2F%2Flocalhost%3A5533%2F&scope=openid", + ) + + await provider.redirectToAuthorization(url) + + expect(captured).toBeDefined() + expect(captured!.hostname).toBe("login.microsoftonline.com") + expect(captured!.searchParams.get("resource")).toBeNull() + expect(captured!.searchParams.get("client_id")).toBe("x") + expect(captured!.searchParams.get("scope")).toBe("openid") +})