Skip to content

Correctly pass redirect_uri to tokens call #222

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 1 commit into from
Mar 28, 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
18 changes: 11 additions & 7 deletions src/client/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ describe("OAuth Authorization", () => {
it("returns metadata when first fetch fails but second without MCP header succeeds", async () => {
// Set up a counter to control behavior
let callCount = 0;

// Mock implementation that changes behavior based on call count
mockFetch.mockImplementation((_url, _options) => {
callCount++;

if (callCount === 1) {
// First call with MCP header - fail with TypeError (simulating CORS error)
// We need to use TypeError specifically because that's what the implementation checks for
Expand All @@ -68,22 +68,22 @@ describe("OAuth Authorization", () => {
// Should succeed with the second call
const metadata = await discoverOAuthMetadata("https://auth.example.com");
expect(metadata).toEqual(validMetadata);

// Verify both calls were made
expect(mockFetch).toHaveBeenCalledTimes(2);

// Verify first call had MCP header
expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version");
});

it("throws an error when all fetch attempts fail", async () => {
// Set up a counter to control behavior
let callCount = 0;

// Mock implementation that changes behavior based on call count
mockFetch.mockImplementation((_url, _options) => {
callCount++;

if (callCount === 1) {
// First call - fail with TypeError
return Promise.reject(new TypeError("First failure"));
Expand All @@ -96,7 +96,7 @@ describe("OAuth Authorization", () => {
// Should fail with the second error
await expect(discoverOAuthMetadata("https://auth.example.com"))
.rejects.toThrow("Second failure");

// Verify both calls were made
expect(mockFetch).toHaveBeenCalledTimes(2);
});
Expand Down Expand Up @@ -250,6 +250,7 @@ describe("OAuth Authorization", () => {
clientInformation: validClientInfo,
authorizationCode: "code123",
codeVerifier: "verifier123",
redirectUri: "http://localhost:3000/callback",
});

expect(tokens).toEqual(validTokens);
Expand All @@ -271,6 +272,7 @@ describe("OAuth Authorization", () => {
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");
});

it("validates token response schema", async () => {
Expand All @@ -288,6 +290,7 @@ describe("OAuth Authorization", () => {
clientInformation: validClientInfo,
authorizationCode: "code123",
codeVerifier: "verifier123",
redirectUri: "http://localhost:3000/callback",
})
).rejects.toThrow();
});
Expand All @@ -303,6 +306,7 @@ describe("OAuth Authorization", () => {
clientInformation: validClientInfo,
authorizationCode: "code123",
codeVerifier: "verifier123",
redirectUri: "http://localhost:3000/callback",
})
).rejects.toThrow("Token exchange failed");
});
Expand Down
4 changes: 4 additions & 0 deletions src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export async function auth(
clientInformation,
authorizationCode,
codeVerifier,
redirectUri: provider.redirectUrl,
});

await provider.saveTokens(tokens);
Expand Down Expand Up @@ -259,11 +260,13 @@ export async function exchangeAuthorization(
clientInformation,
authorizationCode,
codeVerifier,
redirectUri,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be optional? I think it's possible for the authorization request to not have ?redirect_uri=xxx defined (i.e. if the client only has a single configured redirect the server should just send them there).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and thus by oauth 2.0, it wouldn't be required

redirect_uri
REQUIRED, if the "redirect_uri" parameter was included in the
authorization request as described in Section 4.1.1, and their
values MUST be identical.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh I think it's okay as is

}: {
metadata?: OAuthMetadata;
clientInformation: OAuthClientInformation;
authorizationCode: string;
codeVerifier: string;
redirectUri: string | URL;
},
): Promise<OAuthTokens> {
const grantType = "authorization_code";
Expand All @@ -290,6 +293,7 @@ export async function exchangeAuthorization(
client_id: clientInformation.client_id,
code: authorizationCode,
code_verifier: codeVerifier,
redirect_uri: String(redirectUri),
});

if (clientInformation.client_secret) {
Expand Down