359 lines · 13.6 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { AuthAdministrativeScopes } from "@t3tools/contracts";import { expect, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer"; import * as ServerConfig from "../config.ts";import * as ServerEnvironment from "../environment/ServerEnvironment.ts";import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";import * as PairingGrantStore from "./PairingGrantStore.ts";import * as EnvironmentAuth from "./EnvironmentAuth.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts";import * as SessionStore from "./SessionStore.ts"; /** Pinned so dev-mode cookie tests can assert the port-scoped name. */const TEST_SERVER_PORT = 13_773; const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>  Layer.effect(    ServerConfig.ServerConfig,    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      return {        ...config,        ...overrides,        // Keep the test server deterministic even when the default test layer        // changes its development port.        port: TEST_SERVER_PORT,      } satisfies ServerConfig.ServerConfig["Service"];    }),  ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" }))); const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>  EnvironmentAuth.layer.pipe(    Layer.provide(SqlitePersistenceMemory),    Layer.provide(ServerSecretStore.layer),    Layer.provide(ServerEnvironment.identityLayer),    Layer.provide(makeServerConfigLayer(overrides)),  ); const makeCookieRequest = (  cookieName: string,  sessionToken: string,): Parameters<EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]>[0] =>  ({    cookies: {      [cookieName]: sessionToken,    },    headers: {},  }) as unknown as Parameters<    EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]  >[0]; const makeBearerRequest = (  token: string,): Parameters<EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]>[0] =>  ({    cookies: {},    headers: {      authorization: `Bearer ${token}`,    },  }) as unknown as Parameters<    EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]  >[0]; const requestMetadata = {  deviceType: "desktop" as const,  os: "macOS",  browser: "Chrome",  ipAddress: "192.168.1.23",}; it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {  it.effect("classifies invalid bootstrap credential failures for the HTTP boundary", () =>    Effect.sync(() => {      const error = EnvironmentAuth.toBootstrapExchangeError(        new PairingGrantStore.UnknownBootstrapCredentialError({}),      );       expect(error._tag).toBe("ServerAuthInvalidCredentialError");    }),  );   it.effect("maps unexpected bootstrap failures to 500", () =>    Effect.sync(() => {      const cause = new PairingGrantStore.BootstrapCredentialConsumeError({        cause: new Error("sqlite is unavailable"),      });      const error = EnvironmentAuth.toBootstrapExchangeError(cause);       expect(error._tag).toBe("ServerAuthBootstrapCredentialValidationError");      expect(error.message).toBe("Failed to validate bootstrap credential.");      if (error._tag === "ServerAuthBootstrapCredentialValidationError") {        expect(error.cause).toBe(cause);      }    }),  );   it.effect("issues standard pairing credentials by default", () =>    Effect.gen(function* () {      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;      const sessions = yield* SessionStore.SessionStore;       const pairingCredential = yield* serverAuth.issuePairingCredential();      const exchanged = yield* serverAuth.createBrowserSession(        pairingCredential.credential,        requestMetadata,      );      const verified = yield* serverAuth.authenticateHttpRequest(        makeCookieRequest(sessions.cookieName, exchanged.sessionToken),      );       expect(verified.sessionId.length).toBeGreaterThan(0);      expect(verified.scopes).toEqual([        "orchestration:read",        "orchestration:operate",        "terminal:operate",        "review:write",        "relay:read",      ]);      expect(verified.subject).toBe("one-time-token");    }).pipe(Effect.provide(makeEnvironmentAuthLayer())),  );   it.effect("prefers a bearer token over a stale legacy cookie", () =>    Effect.gen(function* () {      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;      const sessions = yield* SessionStore.SessionStore;      const bearer = yield* serverAuth.issueSession();      const verified = yield* serverAuth.authenticateHttpRequest({        cookies: { [sessions.legacyCookieName ?? "t3_session"]: "stale" },        headers: { authorization: `Bearer ${bearer.token}` },      } as never);       expect(verified.sessionId).toBe(bearer.sessionId);    }).pipe(Effect.provide(makeEnvironmentAuthLayer({ mode: "web", host: "192.168.1.50" }))),  );   it.effect("does not exchange ordinary pairing grants for administrative access tokens", () =>    Effect.gen(function* () {      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;      const pairingCredential = yield* serverAuth.issuePairingCredential();       const error = yield* serverAuth        .exchangeBootstrapCredentialForAccessToken(          pairingCredential.credential,          ["orchestration:read", "access:write"],          requestMetadata,        )        .pipe(Effect.flip);       expect(error._tag).toBe("ServerAuthScopeNotGrantedError");    }).pipe(Effect.provide(makeEnvironmentAuthLayer())),  );   it.effect("inherits a constrained pairing grant when token exchange omits scope", () =>    Effect.gen(function* () {      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;      const pairingCredential = yield* serverAuth.issuePairingCredential({        scopes: ["orchestration:read"],      });       const token = yield* serverAuth.exchangeBootstrapCredentialForAccessToken(        pairingCredential.credential,        undefined,        requestMetadata,      );       expect(token.scope).toBe("orchestration:read");    }).pipe(Effect.provide(makeEnvironmentAuthLayer())),  );   it.effect("rotates desktop bearer sessions without accumulating authorized clients", () =>    Effect.gen(function* () {      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;      const sessions = yield* SessionStore.SessionStore;      const browser = yield* serverAuth.createBrowserSession(        "desktop-bootstrap-token",        requestMetadata,      );      const browserSession = yield* serverAuth.authenticateHttpRequest(        makeCookieRequest(sessions.cookieName, browser.sessionToken),      );      const staleSessions = yield* Effect.forEach([1, 2, 3], () =>        sessions.issue({ subject: "desktop-bootstrap", method: "bearer-access-token" }),      );      const pairing = yield* serverAuth.issuePairingCredential();      const paired = yield* serverAuth.exchangeBootstrapCredentialForAccessToken(        pairing.credential,        undefined,        { ...requestMetadata, label: "T3 Code Desktop" },      );      const first = yield* serverAuth.exchangeBootstrapCredentialForAccessToken(        "desktop-bootstrap-token",        undefined,        requestMetadata,      );      const firstSession = yield* serverAuth.authenticateHttpRequest(        makeBearerRequest(first.access_token),      );      const second = yield* serverAuth.exchangeBootstrapCredentialForAccessToken(        "desktop-bootstrap-token",        undefined,        requestMetadata,      );       const active = yield* serverAuth.listSessions();      const firstError = yield* serverAuth        .authenticateHttpRequest(makeBearerRequest(first.access_token))        .pipe(Effect.flip);      const secondSession = yield* serverAuth.authenticateHttpRequest(        makeBearerRequest(second.access_token),      );       expect(active).toHaveLength(3);      expect(active.map((entry) => entry.sessionId)).toContain(browserSession.sessionId);      expect(active.map((entry) => entry.sessionId)).toContain(secondSession.sessionId);      expect(active.map((entry) => entry.sessionId)).not.toContain(firstSession.sessionId);      expect(firstError._tag).toBe("ServerAuthInvalidCredentialError");      for (const stale of staleSessions) {        const error = yield* sessions.verify(stale.token).pipe(Effect.flip);        expect(error._tag).toBe("SessionTokenRevokedError");      }      const pairedSession = yield* serverAuth.authenticateHttpRequest(        makeBearerRequest(paired.access_token),      );      expect(pairedSession.subject).toBe("one-time-token");      expect(active.map((entry) => entry.sessionId)).toContain(pairedSession.sessionId);    }).pipe(      Effect.provide(        makeEnvironmentAuthLayer({          desktopBootstrapToken: "desktop-bootstrap-token",        }),      ),    ),  );   it.effect("keeps user-issued administrative pairing links manageable", () =>    Effect.gen(function* () {      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;      const pairingCredential = yield* serverAuth.issuePairingCredential({        scopes: AuthAdministrativeScopes,      });      const listedPairingLinks = yield* serverAuth.listPairingLinks();       expect(        listedPairingLinks.find((pairingLink) => pairingLink.id === pairingCredential.id)?.subject,      ).toBe("one-time-token");    }).pipe(Effect.provide(makeEnvironmentAuthLayer())),  );   it.effect("issues startup pairing URLs that bootstrap administrative sessions", () =>    Effect.gen(function* () {      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;      const sessions = yield* SessionStore.SessionStore;       const pairingUrl = yield* serverAuth.issueStartupPairingUrl("http://127.0.0.1:3773");      const token = new URLSearchParams(new URL(pairingUrl).hash.slice(1)).get("token");      const listedPairingLinks = yield* serverAuth.listPairingLinks();      expect(token).toBeTruthy();      expect(        listedPairingLinks.some(          (pairingLink) => pairingLink.subject === "administrative-bootstrap",        ),      ).toBe(false);       const exchanged = yield* serverAuth.createBrowserSession(token ?? "", requestMetadata);      const verified = yield* serverAuth.authenticateHttpRequest(        makeCookieRequest(sessions.cookieName, exchanged.sessionToken),      );       expect(verified.scopes).toEqual([        "orchestration:read",        "orchestration:operate",        "terminal:operate",        "review:write",        "relay:read",        "access:read",        "access:write",        "relay:write",      ]);      expect(verified.subject).toBe("administrative-bootstrap");    }).pipe(Effect.provide(makeEnvironmentAuthLayer())),  );   it.effect(    "lists pairing links and revokes other sessions while keeping the administrative session",    () =>      Effect.gen(function* () {        const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;        const sessions = yield* SessionStore.SessionStore;         const administrativeExchange = yield* serverAuth.createBrowserSession(          "desktop-bootstrap-token",          requestMetadata,        );        const administrativeSession = yield* serverAuth.authenticateHttpRequest(          makeCookieRequest(sessions.cookieName, administrativeExchange.sessionToken),        );        const pairingCredential = yield* serverAuth.issuePairingCredential({          label: "Julius iPhone",        });        const listedPairingLinks = yield* serverAuth.listPairingLinks();        const clientExchange = yield* serverAuth.createBrowserSession(          pairingCredential.credential,          {            ...requestMetadata,            deviceType: "mobile",            os: "iOS",            browser: "Safari",            ipAddress: "192.168.1.88",          },        );        const clientSession = yield* serverAuth.authenticateHttpRequest(          makeCookieRequest(sessions.cookieName, clientExchange.sessionToken),        );        const clientsBeforeRevoke = yield* serverAuth.listClientSessions(          administrativeSession.sessionId,        );        const revokedCount = yield* serverAuth.revokeOtherClientSessions(          administrativeSession.sessionId,        );        const clientsAfterRevoke = yield* serverAuth.listClientSessions(          administrativeSession.sessionId,        );         expect(listedPairingLinks.map((entry) => entry.id)).toContain(pairingCredential.id);        expect(listedPairingLinks.find((entry) => entry.id === pairingCredential.id)?.label).toBe(          "Julius iPhone",        );        expect(clientsBeforeRevoke).toHaveLength(2);        expect(          clientsBeforeRevoke.find((entry) => entry.sessionId === administrativeSession.sessionId)            ?.current,        ).toBe(true);        expect(          clientsBeforeRevoke.find((entry) => entry.sessionId === clientSession.sessionId)?.current,        ).toBe(false);        expect(          clientsBeforeRevoke.find((entry) => entry.sessionId === clientSession.sessionId)?.client            .label,        ).toBe("Julius iPhone");        expect(          clientsBeforeRevoke.find((entry) => entry.sessionId === clientSession.sessionId)?.client            .deviceType,        ).toBe("mobile");        expect(revokedCount).toBe(1);        expect(clientsAfterRevoke).toHaveLength(1);        expect(clientsAfterRevoke[0]?.sessionId).toBe(administrativeSession.sessionId);      }).pipe(        Effect.provide(          makeEnvironmentAuthLayer({            desktopBootstrapToken: "desktop-bootstrap-token",          }),        ),      ),  );});