277 lines · 10.0 KB · TypeScript
RawDownload
import * as Cloudflare from "alchemy/Cloudflare/Workers/Runtime";import { HttpApiBuilder, HttpApi } from "effect/unstable/httpapi";import { HttpRouter, HttpServer } from "effect/unstable/http";import { describe, expect, it } from "@effect/vitest";import { Data, Effect, Layer } from "effect";import type { Effect as EffectType } from "effect/Effect"; import { CloudAuthPublicApi } from "./api";import { CloudAuthPublicHandlers } from "./handlers";import { UserStoreService } from "./context";import { WorkOSError } from "./errors";import { WorkOSAuth } from "./workos"; const TestAuthPublicApi = HttpApi.make("cloudWeb").add(CloudAuthPublicApi);type EffectSuccess<T> = T extends EffectType<infer A, infer _E, infer _R> ? A : never;type AuthenticateWithCodeResult = EffectSuccess<  ReturnType<WorkOSAuth["Service"]["authenticateWithCode"]>>;const fakeUser: AuthenticateWithCodeResult["user"] = {  object: "user",  id: "user_1",  email: "test@example.com",  emailVerified: true,  profilePictureUrl: null,  firstName: null,  lastName: null,  lastSignInAt: null,  locale: null,  createdAt: "2026-01-01T00:00:00.000Z",  updatedAt: "2026-01-01T00:00:00.000Z",  externalId: null,  metadata: {},}; class UnstubbedWorkOSMethod extends Data.TaggedError("UnstubbedWorkOSMethod")<{  method: string;}> {} const makeAuthFetch = (workos: Partial<WorkOSAuth["Service"]>) => {  const WorkOSTest = Layer.succeed(    WorkOSAuth,    new Proxy(workos as WorkOSAuth["Service"], {      get: (target, prop) => {        if (prop in target) return target[prop as keyof typeof target];        return () =>          Effect.fail(            new UnstubbedWorkOSMethod({              method: typeof prop === "string" ? prop : (prop.description ?? "symbol"),            }),          );      },    }),  );  const UserStoreTest = Layer.succeed(UserStoreService)({    use: <A>() => Effect.sync(() => undefined as A),  });  const WorkerEnvironmentTest = Layer.succeed(Cloudflare.WorkerEnvironment, {    VITE_PUBLIC_SITE_URL: "http://test.local",  });  const apiLayer = HttpApiBuilder.layer(TestAuthPublicApi).pipe(    Layer.provide(CloudAuthPublicHandlers),    Layer.provideMerge(WorkOSTest),    Layer.provideMerge(UserStoreTest),    Layer.provideMerge(WorkerEnvironmentTest),    Layer.provideMerge(HttpServer.layerServices),    Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),  );  const web = HttpRouter.toWebHandler(apiLayer, { disableLogger: true });  return web.handler as (request: Request) => Promise<Response>;}; describe("Auth callback handlers", () => {  it.effect("routes login", () =>    Effect.gen(function* () {      let observedState: string | undefined;      const fetch = makeAuthFetch({        getAuthorizationUrl: (_redirectUri: string, state?: string) => {          observedState = state;          return `https://auth.example.test?state=${state}`;        },      });      const response = yield* Effect.promise(() =>        fetch(new Request("http://test.local/auth/login")),      );      expect(response.status).toBe(302);      expect(observedState).toMatch(/^[0-9a-f]{64}$/);      expect(response.headers.get("location")).toBe(        `https://auth.example.test?state=${observedState}`,      );      const setCookie = response.headers.get("set-cookie") ?? "";      expect(setCookie).toContain(`wos-login-state=${observedState}`);      expect(setCookie).toContain("Max-Age=600");    }),  );   it.effect("rejects callback state without the matching login state cookie", () =>    Effect.gen(function* () {      let authenticateCalls = 0;      const fetch = makeAuthFetch({        authenticateWithCode: () =>          Effect.sync(() => {            authenticateCalls++;            const result: AuthenticateWithCodeResult = {              user: fakeUser,              accessToken: "access_token",              refreshToken: "refresh_token",              sealedSession: "sealed_session",            };            return result;          }),      });       const response = yield* Effect.promise(() =>        fetch(          new Request("http://test.local/auth/callback?code=attacker-code&state=attacker-state"),        ),      );       expect(response.status).toBe(400);      expect(authenticateCalls).toBe(0);    }),  );   // Some WorkOS-initiated redirects don't include a state parameter.  // The schema treats state as optional; the handler skips the CSRF  // check when state is absent and proceeds with the code exchange.  it.effect("accepts a callback without state", () =>    Effect.gen(function* () {      const fetch = makeAuthFetch({        authenticateWithCode: () =>          Effect.succeed({            user: fakeUser,            accessToken: "access_token",            refreshToken: "refresh_token",            organizationId: "org_1",            sealedSession: "sealed_session_no_state",          } satisfies AuthenticateWithCodeResult),      });       const response = yield* Effect.promise(() =>        fetch(new Request("http://test.local/auth/callback?code=stateless-code")),      );       expect(response.status).toBe(302);      expect(response.headers.get("location")).toBe("/");      const setCookie = response.headers.get("set-cookie") ?? "";      expect(setCookie).toContain("wos-session=sealed_session_no_state");    }),  );   it.effect("sets the session cookie and clears login state on matching callback state", () =>    Effect.gen(function* () {      const fetch = makeAuthFetch({        authenticateWithCode: () =>          Effect.succeed({            user: fakeUser,            accessToken: "access_token",            refreshToken: "refresh_token",            organizationId: "org_1",            sealedSession: "sealed_session",          } satisfies AuthenticateWithCodeResult),      });       const response = yield* Effect.promise(() =>        fetch(          new Request("http://test.local/auth/callback?code=code&state=state_1", {            headers: { cookie: "wos-login-state=state_1" },          }),        ),      );       expect(response.status).toBe(302);      expect(response.headers.get("location")).toBe("/");      const setCookie = response.headers.get("set-cookie") ?? "";      expect(setCookie).toContain("wos-session=sealed_session");      expect(setCookie).toContain("Max-Age=604800");      expect(setCookie).toContain("wos-login-state=");      expect(setCookie).toContain("Max-Age=0");    }),  );   // Regression: an invited user signing in for the first time has only a  // *pending* membership (the WorkOS-side representation of an unaccepted  // invitation). The callback used to pick `data[0]` regardless of status  // and call refreshSession into that org, which 400s and surfaces as  // WorkOSError → 500. We now skip pending memberships entirely.  it.effect("does not refresh into a pending membership; leaves session org-less", () =>    Effect.gen(function* () {      let refreshCalls = 0;      const fetch = makeAuthFetch({        authenticateWithCode: () =>          Effect.succeed({            user: fakeUser,            accessToken: "access_token",            refreshToken: "refresh_token",            sealedSession: "sealed_session_no_org",          } satisfies AuthenticateWithCodeResult),        // The handler only reads `data[*].organizationId` and        // `data[*].status`, so we stub a minimal shape matching that        // contract instead of hand-rolling the full WorkOS SDK types.        // oxlint-disable-next-line executor/no-double-cast        listUserMemberships: (() =>          Effect.succeed({            data: [              {                organizationId: "org_pending",                status: "pending",              },            ],          })) as unknown as WorkOSAuth["Service"]["listUserMemberships"],        refreshSession: () =>          Effect.sync(() => {            refreshCalls++;            return null;          }),      });       const response = yield* Effect.promise(() =>        fetch(          new Request("http://test.local/auth/callback?code=code&state=state_2", {            headers: { cookie: "wos-login-state=state_2" },          }),        ),      );       expect(response.status).toBe(302);      expect(response.headers.get("location")).toBe("/");      expect(refreshCalls).toBe(0);      const setCookie = response.headers.get("set-cookie") ?? "";      expect(setCookie).toContain("wos-session=sealed_session_no_org");    }),  );   // Regression: even with an active membership, if WorkOS rejects the  // refresh (e.g. membership just revoked, brief race), the callback  // should still succeed with an org-less session rather than 500.  it.effect("falls through to org-less session when refresh fails", () =>    Effect.gen(function* () {      const fetch = makeAuthFetch({        authenticateWithCode: () =>          Effect.succeed({            user: fakeUser,            accessToken: "access_token",            refreshToken: "refresh_token",            sealedSession: "sealed_session_fallback",          } satisfies AuthenticateWithCodeResult),        // Same minimal-shape stub as above — see comment in the        // pending-membership test.        // oxlint-disable-next-line executor/no-double-cast        listUserMemberships: (() =>          Effect.succeed({            data: [              {                organizationId: "org_active",                status: "active",              },            ],          })) as unknown as WorkOSAuth["Service"]["listUserMemberships"],        refreshSession: (() =>          Effect.fail(new WorkOSError())) as WorkOSAuth["Service"]["refreshSession"],      });       const response = yield* Effect.promise(() =>        fetch(          new Request("http://test.local/auth/callback?code=code&state=state_3", {            headers: { cookie: "wos-login-state=state_3" },          }),        ),      );       expect(response.status).toBe(302);      expect(response.headers.get("location")).toBe("/");      const setCookie = response.headers.get("set-cookie") ?? "";      expect(setCookie).toContain("wos-session=sealed_session_fallback");    }),  );});