Skip to content

Commit 7c80b7d

Browse files
maximus12793spencer426
authored andcommitted
fix: use discoverOAuthFromWWWAuthenticate for reactive OAuth flow (#18760) (#19038)
1 parent 899eff3 commit 7c80b7d

File tree

2 files changed

+94
-13
lines changed

2 files changed

+94
-13
lines changed

packages/core/src/tools/mcp-client.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,90 @@ describe('connectToMcpServer with OAuth', () => {
20562056
capturedTransport._requestInit?.headers?.['Authorization'];
20572057
expect(authHeader).toBe('Bearer test-access-token-from-discovery');
20582058
});
2059+
2060+
it('should use discoverOAuthFromWWWAuthenticate when it succeeds and skip discoverOAuthConfig', async () => {
2061+
const serverUrl = 'http://test-server.com/mcp';
2062+
const authUrl = 'http://auth.example.com/auth';
2063+
const tokenUrl = 'http://auth.example.com/token';
2064+
const wwwAuthHeader = `Bearer realm="test", resource_metadata="http://test-server.com/.well-known/oauth-protected-resource"`;
2065+
2066+
vi.mocked(mockedClient.connect).mockRejectedValueOnce(
2067+
new StreamableHTTPError(
2068+
401,
2069+
`Unauthorized\nwww-authenticate: ${wwwAuthHeader}`,
2070+
),
2071+
);
2072+
2073+
vi.mocked(OAuthUtils.discoverOAuthFromWWWAuthenticate).mockResolvedValue({
2074+
authorizationUrl: authUrl,
2075+
tokenUrl,
2076+
scopes: ['read'],
2077+
});
2078+
2079+
vi.mocked(mockedClient.connect).mockResolvedValueOnce(undefined);
2080+
2081+
const client = await connectToMcpServer(
2082+
'0.0.1',
2083+
'test-server',
2084+
{ httpUrl: serverUrl, oauth: { enabled: true } },
2085+
false,
2086+
workspaceContext,
2087+
EMPTY_CONFIG,
2088+
);
2089+
2090+
expect(client).toBe(mockedClient);
2091+
expect(OAuthUtils.discoverOAuthFromWWWAuthenticate).toHaveBeenCalledWith(
2092+
wwwAuthHeader,
2093+
serverUrl,
2094+
);
2095+
expect(OAuthUtils.discoverOAuthConfig).not.toHaveBeenCalled();
2096+
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
2097+
});
2098+
2099+
it('should fall back to extractBaseUrl + discoverOAuthConfig when discoverOAuthFromWWWAuthenticate returns null', async () => {
2100+
const serverUrl = 'http://test-server.com/mcp';
2101+
const baseUrl = 'http://test-server.com';
2102+
const authUrl = 'http://auth.example.com/auth';
2103+
const tokenUrl = 'http://auth.example.com/token';
2104+
const wwwAuthHeader = `Bearer realm="test"`;
2105+
2106+
vi.mocked(mockedClient.connect).mockRejectedValueOnce(
2107+
new StreamableHTTPError(
2108+
401,
2109+
`Unauthorized\nwww-authenticate: ${wwwAuthHeader}`,
2110+
),
2111+
);
2112+
2113+
vi.mocked(OAuthUtils.discoverOAuthFromWWWAuthenticate).mockResolvedValue(
2114+
null,
2115+
);
2116+
vi.mocked(OAuthUtils.extractBaseUrl).mockReturnValue(baseUrl);
2117+
vi.mocked(OAuthUtils.discoverOAuthConfig).mockResolvedValue({
2118+
authorizationUrl: authUrl,
2119+
tokenUrl,
2120+
scopes: ['read'],
2121+
});
2122+
2123+
vi.mocked(mockedClient.connect).mockResolvedValueOnce(undefined);
2124+
2125+
const client = await connectToMcpServer(
2126+
'0.0.1',
2127+
'test-server',
2128+
{ httpUrl: serverUrl, oauth: { enabled: true } },
2129+
false,
2130+
workspaceContext,
2131+
EMPTY_CONFIG,
2132+
);
2133+
2134+
expect(client).toBe(mockedClient);
2135+
expect(OAuthUtils.discoverOAuthFromWWWAuthenticate).toHaveBeenCalledWith(
2136+
wwwAuthHeader,
2137+
serverUrl,
2138+
);
2139+
expect(OAuthUtils.extractBaseUrl).toHaveBeenCalledWith(serverUrl);
2140+
expect(OAuthUtils.discoverOAuthConfig).toHaveBeenCalledWith(baseUrl);
2141+
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
2142+
});
20592143
});
20602144

20612145
describe('connectToMcpServer - HTTP→SSE fallback', () => {

packages/core/src/tools/mcp-client.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -719,18 +719,17 @@ async function handleAutomaticOAuth(
719719
try {
720720
debugLogger.log(`🔐 '${mcpServerName}' requires OAuth authentication`);
721721

722-
// Always try to parse the resource metadata URI from the www-authenticate header
723-
let oauthConfig;
724-
const resourceMetadataUri =
725-
OAuthUtils.parseWWWAuthenticateHeader(wwwAuthenticate);
726-
if (resourceMetadataUri) {
727-
oauthConfig = await OAuthUtils.discoverOAuthConfig(resourceMetadataUri);
728-
} else if (hasNetworkTransport(mcpServerConfig)) {
722+
const serverUrl = mcpServerConfig.httpUrl || mcpServerConfig.url;
723+
724+
// Try to discover OAuth config from the WWW-Authenticate header first
725+
let oauthConfig = await OAuthUtils.discoverOAuthFromWWWAuthenticate(
726+
wwwAuthenticate,
727+
serverUrl,
728+
);
729+
730+
if (!oauthConfig && hasNetworkTransport(mcpServerConfig)) {
729731
// Fallback: try to discover OAuth config from the base URL
730-
const serverUrl = new URL(
731-
mcpServerConfig.httpUrl || mcpServerConfig.url!,
732-
);
733-
const baseUrl = `${serverUrl.protocol}//${serverUrl.host}`;
732+
const baseUrl = OAuthUtils.extractBaseUrl(serverUrl!);
734733
oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl);
735734
}
736735

@@ -754,8 +753,6 @@ async function handleAutomaticOAuth(
754753
};
755754

756755
// Perform OAuth authentication
757-
// Pass the server URL for proper discovery
758-
const serverUrl = mcpServerConfig.httpUrl || mcpServerConfig.url;
759756
debugLogger.log(
760757
`Starting OAuth authentication for server '${mcpServerName}'...`,
761758
);

0 commit comments

Comments
 (0)