Merge pull request #830 from kentcdodds/auth-fix
fix: enhance OAuth token handling in useConnection hook to prevent infinite auth loops
This commit is contained in:
commit
901d10c080
3 changed files with 68 additions and 5 deletions
|
|
@ -164,12 +164,12 @@ const App = () => {
|
||||||
return migrateFromLegacyAuth(legacyToken, legacyHeaderName);
|
return migrateFromLegacyAuth(legacyToken, legacyHeaderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to Authorization: Bearer as the most common case
|
// Default to empty array
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "Authorization",
|
name: "Authorization",
|
||||||
value: "Bearer ",
|
value: "Bearer ",
|
||||||
enabled: true,
|
enabled: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,10 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the toast hook
|
// Mock the toast hook
|
||||||
|
const mockToast = jest.fn();
|
||||||
jest.mock("@/lib/hooks/useToast", () => ({
|
jest.mock("@/lib/hooks/useToast", () => ({
|
||||||
useToast: () => ({
|
useToast: () => ({
|
||||||
toast: jest.fn(),
|
toast: mockToast,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -913,6 +914,42 @@ describe("useConnection", () => {
|
||||||
expect(headers).toHaveProperty("Authorization", "Bearer mock-token");
|
expect(headers).toHaveProperty("Authorization", "Bearer mock-token");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("warns of enabled empty Bearer token", async () => {
|
||||||
|
// This test prevents regression of the bug where default "Bearer " header
|
||||||
|
// prevented OAuth token injection, causing infinite auth loops
|
||||||
|
const customHeaders: CustomHeaders = [
|
||||||
|
{
|
||||||
|
name: "Authorization",
|
||||||
|
value: "Bearer ", // Empty Bearer token placeholder
|
||||||
|
enabled: true, // enabled
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const propsWithEmptyBearer = {
|
||||||
|
...defaultProps,
|
||||||
|
customHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConnection(propsWithEmptyBearer));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = mockSSETransport.options?.requestInit?.headers;
|
||||||
|
|
||||||
|
expect(headers).toHaveProperty("Authorization", "Bearer");
|
||||||
|
// Should not have the x-custom-auth-headers since Authorization is standard
|
||||||
|
expect(headers).not.toHaveProperty("x-custom-auth-headers");
|
||||||
|
|
||||||
|
// Should show toast notification for empty Authorization header
|
||||||
|
expect(mockToast).toHaveBeenCalledWith({
|
||||||
|
title: "Invalid Authorization Header",
|
||||||
|
description: expect.any(String),
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("prioritizes custom headers over legacy auth", async () => {
|
test("prioritizes custom headers over legacy auth", async () => {
|
||||||
const customHeaders: CustomHeaders = [
|
const customHeaders: CustomHeaders = [
|
||||||
{ name: "Authorization", value: "Bearer custom-token", enabled: true },
|
{ name: "Authorization", value: "Bearer custom-token", enabled: true },
|
||||||
|
|
|
||||||
|
|
@ -400,11 +400,37 @@ export function useConnection({
|
||||||
// Use custom headers (migration is handled in App.tsx)
|
// Use custom headers (migration is handled in App.tsx)
|
||||||
let finalHeaders: CustomHeaders = customHeaders || [];
|
let finalHeaders: CustomHeaders = customHeaders || [];
|
||||||
|
|
||||||
// Add OAuth token if available and no custom headers are set
|
const isEmptyAuthHeader = (header: CustomHeaders[number]) =>
|
||||||
if (finalHeaders.length === 0) {
|
header.name.trim().toLowerCase() === "authorization" &&
|
||||||
|
header.value.trim().toLowerCase() === "bearer";
|
||||||
|
|
||||||
|
// Check for empty Authorization headers and show validation error
|
||||||
|
const hasEmptyAuthHeader = finalHeaders.some(
|
||||||
|
(header) => header.enabled && isEmptyAuthHeader(header),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasEmptyAuthHeader) {
|
||||||
|
toast({
|
||||||
|
title: "Invalid Authorization Header",
|
||||||
|
description:
|
||||||
|
"Authorization header is enabled but empty. Please add a token or disable the header.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsOAuthToken = !finalHeaders.some(
|
||||||
|
(header) =>
|
||||||
|
header.enabled &&
|
||||||
|
header.name.trim().toLowerCase() === "authorization",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (needsOAuthToken) {
|
||||||
const oauthToken = (await serverAuthProvider.tokens())?.access_token;
|
const oauthToken = (await serverAuthProvider.tokens())?.access_token;
|
||||||
if (oauthToken) {
|
if (oauthToken) {
|
||||||
|
// Add the OAuth token
|
||||||
finalHeaders = [
|
finalHeaders = [
|
||||||
|
// Remove any existing Authorization headers with empty tokens
|
||||||
|
...finalHeaders.filter((header) => !isEmptyAuthHeader(header)),
|
||||||
{
|
{
|
||||||
name: "Authorization",
|
name: "Authorization",
|
||||||
value: `Bearer ${oauthToken}`,
|
value: `Bearer ${oauthToken}`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue