diff --git a/src/lib/auth/cimd-fetch-policy.ts b/src/lib/auth/cimd-fetch-policy.ts index 64e23d61b..d4efb8255 100644 --- a/src/lib/auth/cimd-fetch-policy.ts +++ b/src/lib/auth/cimd-fetch-policy.ts @@ -52,9 +52,15 @@ export async function allowCimdMetadataFetch(url: string) { * Cloudflare's `global_fetch_strictly_public` compatibility flag enforces the * resolve-once, public-address-only network boundary required by CIMD. Keeping * the transport in application code also prevents the auth package from - * selecting a non-Worker fetch implementation. + * selecting a non-Worker fetch implementation. Workers do not implement + * `redirect: "error"`, so use `manual`; CIMD still rejects every non-200 + * response before reading metadata. */ export const fetchCimdMetadataResource: ClientMetadataResourceFetch = ( url, init, -) => fetch(url, init); +) => + fetch( + url, + init?.redirect === "error" ? { ...init, redirect: "manual" } : init, + ); diff --git a/tests/unit/cimd-fetch-policy.test.ts b/tests/unit/cimd-fetch-policy.test.ts index d2d0390cf..3d009023f 100644 --- a/tests/unit/cimd-fetch-policy.test.ts +++ b/tests/unit/cimd-fetch-policy.test.ts @@ -1,6 +1,9 @@ import { resolve4, resolve6 } from "node:dns/promises"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { allowCimdMetadataFetch } from "@/lib/auth/cimd-fetch-policy"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + allowCimdMetadataFetch, + fetchCimdMetadataResource, +} from "@/lib/auth/cimd-fetch-policy"; vi.mock("node:dns/promises", () => ({ resolve4: vi.fn(), @@ -16,6 +19,34 @@ describe("CIMD metadata fetch policy", () => { resolve6Mock.mockReset(); }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("adapts redirect rejection to the mode supported by Workers", async () => { + const response = new Response(null, { status: 200 }); + const fetchMock = vi.fn().mockResolvedValue(response); + vi.stubGlobal("fetch", fetchMock); + const signal = new AbortController().signal; + + await expect( + fetchCimdMetadataResource("https://client.example/oauth.json", { + headers: { accept: "application/json" }, + redirect: "error", + signal, + }), + ).resolves.toBe(response); + + expect(fetchMock).toHaveBeenCalledWith( + "https://client.example/oauth.json", + { + headers: { accept: "application/json" }, + redirect: "manual", + signal, + }, + ); + }); + it("accepts hostnames only when every resolved address is public", async () => { resolve4Mock.mockResolvedValue(["203.0.113.10"]); resolve6Mock.mockResolvedValue(["2606:4700:4700::1111"]);