Skip to content

fix: consistently use consumer-provided fetch function #767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions src/client/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,35 @@ describe("OAuth Authorization", () => {
const [url] = calls[0];
expect(url.toString()).toBe("https://custom.example.com/metadata");
});

it("supports overriding the fetch function used for requests", async () => {
const validMetadata = {
resource: "https://resource.example.com",
authorization_servers: ["https://auth.example.com"],
};

const customFetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => validMetadata,
});

const metadata = await discoverOAuthProtectedResourceMetadata(
"https://resource.example.com",
undefined,
customFetch
);

expect(metadata).toEqual(validMetadata);
expect(customFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).not.toHaveBeenCalled();

const [url, options] = customFetch.mock.calls[0];
expect(url.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
expect(options.headers).toEqual({
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
});
});
});

describe("discoverOAuthMetadata", () => {
Expand Down Expand Up @@ -619,6 +648,39 @@ describe("OAuth Authorization", () => {
discoverOAuthMetadata("https://auth.example.com")
).rejects.toThrow();
});

it("supports overriding the fetch function used for requests", async () => {
const validMetadata = {
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
registration_endpoint: "https://auth.example.com/register",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
};

const customFetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => validMetadata,
});

const metadata = await discoverOAuthMetadata(
"https://auth.example.com",
{},
customFetch
);

expect(metadata).toEqual(validMetadata);
expect(customFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).not.toHaveBeenCalled();

const [url, options] = customFetch.mock.calls[0];
expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
expect(options.headers).toEqual({
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
});
});
});

describe("startAuthorization", () => {
Expand Down Expand Up @@ -917,6 +979,46 @@ describe("OAuth Authorization", () => {
})
).rejects.toThrow("Token exchange failed");
});

it("supports overriding the fetch function used for requests", async () => {
const customFetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => validTokens,
});

const tokens = await exchangeAuthorization("https://auth.example.com", {
clientInformation: validClientInfo,
authorizationCode: "code123",
codeVerifier: "verifier123",
redirectUri: "http://localhost:3000/callback",
resource: new URL("https://api.example.com/mcp-server"),
fetchFn: customFetch,
});

expect(tokens).toEqual(validTokens);
expect(customFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).not.toHaveBeenCalled();

const [url, options] = customFetch.mock.calls[0];
expect(url.toString()).toBe("https://auth.example.com/token");
expect(options).toEqual(
expect.objectContaining({
method: "POST",
headers: expect.any(Headers),
body: expect.any(URLSearchParams),
})
);

const body = options.body as URLSearchParams;
expect(body.get("grant_type")).toBe("authorization_code");
expect(body.get("code")).toBe("code123");
expect(body.get("code_verifier")).toBe("verifier123");
expect(body.get("client_id")).toBe("client123");
expect(body.get("client_secret")).toBe("secret123");
expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback");
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
});
});

describe("refreshAuthorization", () => {
Expand Down Expand Up @@ -1824,6 +1926,68 @@ describe("OAuth Authorization", () => {
// Second call should be to AS metadata with the path from authorization server
expect(calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/oauth");
});

it("supports overriding the fetch function used for requests", async () => {
const customFetch = jest.fn();

// Mock PRM discovery
customFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
resource: "https://resource.example.com",
authorization_servers: ["https://auth.example.com"],
}),
});

// Mock AS metadata discovery
customFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
registration_endpoint: "https://auth.example.com/register",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});

const mockProvider: OAuthClientProvider = {
get redirectUrl() { return "http://localhost:3000/callback"; },
get clientMetadata() {
return {
client_name: "Test Client",
redirect_uris: ["http://localhost:3000/callback"],
};
},
clientInformation: jest.fn().mockResolvedValue({
client_id: "client123",
client_secret: "secret123",
}),
tokens: jest.fn().mockResolvedValue(undefined),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn().mockResolvedValue("verifier123"),
};

const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
fetchFn: customFetch,
});

expect(result).toBe("REDIRECT");
expect(customFetch).toHaveBeenCalledTimes(2);
expect(mockFetch).not.toHaveBeenCalled();

// Verify custom fetch was called for PRM discovery
expect(customFetch.mock.calls[0][0].toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");

// Verify custom fetch was called for AS metadata discovery
expect(customFetch.mock.calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
});
});

describe("exchangeAuthorization with multiple client authentication methods", () => {
Expand Down
Loading