diff --git a/.changeset/quiet-access-caches.md b/.changeset/quiet-access-caches.md new file mode 100644 index 00000000000..799af465faa --- /dev/null +++ b/.changeset/quiet-access-caches.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/workers-auth": patch +--- + +Use the current Cloudflare Access service-token credentials after environment variables change. Interactive Access cookie caching is unchanged. diff --git a/packages/workers-auth/src/access.ts b/packages/workers-auth/src/access.ts index 5320b9cfc33..4302f4cc0cf 100644 --- a/packages/workers-auth/src/access.ts +++ b/packages/workers-auth/src/access.ts @@ -110,12 +110,10 @@ export async function getAccessHeaders( if (clientId && clientSecret) { logger.debug("Using Access Service Token headers for domain:", domain); - const headers = { + return { "CF-Access-Client-Id": clientId, "CF-Access-Client-Secret": clientSecret, }; - headersCache[domain] = headers; - return headers; } // Warn if only one of the two env vars is set diff --git a/packages/workers-auth/tests/access.test.ts b/packages/workers-auth/tests/access.test.ts index ee985f426c4..7eafb30e844 100644 --- a/packages/workers-auth/tests/access.test.ts +++ b/packages/workers-auth/tests/access.test.ts @@ -67,6 +67,7 @@ const msw = setupServer(); beforeAll(() => msw.listen({ onUnhandledRequest: "error" })); afterEach(() => { + vi.unstubAllEnvs(); msw.restoreHandlers(); msw.resetHandlers(); }); @@ -84,6 +85,7 @@ const isNonInteractiveOrCI = () => true; describe("access", () => { beforeEach(() => { + vi.unstubAllEnvs(); clearAccessCaches(); silentLogger.warn = vi.fn(); msw.use(...mswAccessHandlers); @@ -170,6 +172,77 @@ describe("access", () => { expect(silentLogger.warn).not.toHaveBeenCalled(); }); + it("should not reuse service token headers after the env vars are unset", async ({ + expect, + }) => { + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_ID", "first-client-id.access"); + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_SECRET", "first-client-secret"); + await getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI, + }); + + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_ID", undefined); + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_SECRET", undefined); + + await expect( + getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI, + }) + ).rejects.toThrow("no Access Service Token credentials were found"); + }); + + it("should not reuse service token headers when only CLOUDFLARE_ACCESS_CLIENT_ID remains", async ({ + expect, + }) => { + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_ID", "first-client-id.access"); + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_SECRET", "first-client-secret"); + await getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI, + }); + + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_ID", "second-client-id.access"); + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_SECRET", undefined); + + await expect( + getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI, + }) + ).rejects.toThrow("no Access Service Token credentials were found"); + expect(silentLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("Only CLOUDFLARE_ACCESS_CLIENT_ID was found") + ); + }); + + it("should not reuse service token headers when only CLOUDFLARE_ACCESS_CLIENT_SECRET remains", async ({ + expect, + }) => { + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_ID", "first-client-id.access"); + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_SECRET", "first-client-secret"); + await getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI, + }); + + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_ID", undefined); + vi.stubEnv("CLOUDFLARE_ACCESS_CLIENT_SECRET", "second-client-secret"); + + await expect( + getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI, + }) + ).rejects.toThrow("no Access Service Token credentials were found"); + expect(silentLogger.warn).toHaveBeenCalledWith( + expect.stringContaining( + "Only CLOUDFLARE_ACCESS_CLIENT_SECRET was found" + ) + ); + }); + it("should warn when only CLOUDFLARE_ACCESS_CLIENT_ID is set", async ({ expect, }) => { @@ -297,6 +370,35 @@ See https://developers.cloudflare.com/cloudflare-one/access-controls/service-cre ); }); + it("should reuse the cached CF_Authorization cookie header", async ({ + expect, + }) => { + const fake = createFakeProcess(); + vi.mocked(spawn).mockReturnValueOnce(fake.child); + + const firstHeaders = getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI: () => false, + }); + await vi.waitFor(() => { + expect(spawn).toHaveBeenCalledOnce(); + }); + fake.complete("fetched your token:\n\ntest-access-token\n"); + + await expect(firstHeaders).resolves.toEqual({ + Cookie: "CF_Authorization=test-access-token", + }); + await expect( + getAccessHeaders("access-protected.com", { + logger: silentLogger, + isNonInteractiveOrCI: () => false, + }) + ).resolves.toEqual({ + Cookie: "CF_Authorization=test-access-token", + }); + expect(spawn).toHaveBeenCalledOnce(); + }); + it("should kill a still-pending cloudflared when the process exits", async ({ expect, }) => {