Files
Ekklesia/core/tests/identity-runtime.test.ts
T
ekklesiaandClaude Sonnet 5 cafd513019 fix: secure authentication request identity
Replace Better Auth's default trust of a caller-controlled X-Forwarded-For
with a platform-owned client-address resolution boundary: direct mode (the
safe default) trusts only the real socket peer, and an explicit trusted-proxy
mode requires a header name, a concrete trusted-proxy list, and only honors a
forwarded chain from an actual trusted peer. This closes a rate-limit bypass
where rotating a spoofed header let a caller choose a fresh bucket on every
request. Public sign-up is also disabled by default and requires an explicit
typed opt-in, using Better Auth's own disableSignUp gate so no email-existence
leak or second signup path is introduced.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-26 12:20:51 -05:00

414 lines
16 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const betterAuthMock = vi.hoisted(() => vi.fn());
const drizzleAdapterMock = vi.hoisted(() =>
vi.fn((db: unknown, options: unknown) => ({ __adapterOptions: options, db })),
);
vi.mock("better-auth", () => ({ betterAuth: betterAuthMock }));
vi.mock("better-auth/adapters/drizzle", () => ({ drizzleAdapter: drizzleAdapterMock }));
import type { IdentityConfig } from "../src/identity/config.js";
import {
createIdentityRuntime,
IdentityRuntimeError,
TRUSTED_CLIENT_ADDRESS_HEADER,
} from "../src/identity/runtime.js";
import { identityAdapterSchema } from "../src/identity/schema.js";
function validConfig(overrides: Partial<IdentityConfig> = {}): IdentityConfig {
return Object.freeze({
baseUrl: "https://identity.example.test",
publicSignupEnabled: false,
secret: "correct-horse-battery-staple-0123456789",
trustedOrigins: Object.freeze(["https://identity.example.test"]),
...overrides,
});
}
function fakeDb(overrides: Partial<{ execute: ReturnType<typeof vi.fn> }> = {}) {
return { __fakeDb: true, execute: vi.fn().mockResolvedValue(undefined), ...overrides } as never;
}
describe("Core identity runtime construction boundary", () => {
beforeEach(() => {
betterAuthMock.mockReset();
drizzleAdapterMock.mockClear();
});
it("does not construct Better Auth or the Drizzle adapter merely because the module is imported", () => {
expect(betterAuthMock).not.toHaveBeenCalled();
expect(drizzleAdapterMock).not.toHaveBeenCalled();
});
it("constructs explicitly and returns only the two narrow transport capabilities", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() });
expect(Object.keys(runtime).sort()).toEqual(["handleAuthRequest", "resolvePrincipal"]);
expect(typeof runtime.handleAuthRequest).toBe("function");
expect(typeof runtime.resolvePrincipal).toBe("function");
expect(Object.isFrozen(runtime)).toBe(true);
});
it("does not expose the raw database, adapter, or Better Auth instance on the returned value", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
const db = fakeDb();
const runtime = createIdentityRuntime({ config: validConfig(), db });
expect(JSON.stringify(runtime)).toBe("{}");
});
it("passes the explicit ekklesia_core_* schema mapping to the Drizzle adapter", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
const db = fakeDb();
createIdentityRuntime({ config: validConfig(), db });
expect(drizzleAdapterMock).toHaveBeenCalledTimes(1);
const [passedDb, options] = drizzleAdapterMock.mock.calls[0] as [unknown, unknown];
expect(passedDb).toBe(db);
expect(options).toEqual({ provider: "pg", schema: identityAdapterSchema });
});
it("enables exactly email/password (autoSignIn disabled) and rate limiting, nothing else", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({ config: validConfig(), db: fakeDb() });
expect(betterAuthMock).toHaveBeenCalledTimes(1);
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
expect(Object.keys(options).sort()).toEqual([
"advanced",
"baseURL",
"database",
"emailAndPassword",
"logger",
"onAPIError",
"rateLimit",
"secret",
"trustedOrigins",
]);
expect(options.emailAndPassword).toEqual({
autoSignIn: false,
disableSignUp: true,
enabled: true,
maxPasswordLength: 128,
minPasswordLength: 8,
});
expect(options.rateLimit).toEqual({ enabled: true, max: 100, window: 10 });
expect(options.baseURL).toBe(validConfig().baseUrl);
expect(options.secret).toBe(validConfig().secret);
expect(options.trustedOrigins).toEqual([...validConfig().trustedOrigins]);
});
it("enables sign-up (disableSignUp: false) only when publicSignupEnabled is explicitly true", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({ config: validConfig({ publicSignupEnabled: true }), db: fakeDb() });
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
expect(options.emailAndPassword).toMatchObject({ disableSignUp: false });
});
it("explicitly disables Better Auth's default test-environment origin/CSRF skip", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
expect(options.advanced).toEqual({
disableCSRFCheck: false,
disableOriginCheck: false,
ipAddress: { ipAddressHeaders: [TRUSTED_CLIENT_ADDRESS_HEADER] },
});
});
it("trusts exactly the one platform-owned client-address header, never Better Auth's own default", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
const advanced = options.advanced as { ipAddress?: { ipAddressHeaders?: string[] } };
expect(advanced.ipAddress?.ipAddressHeaders).toEqual([TRUSTED_CLIENT_ADDRESS_HEADER]);
expect(advanced.ipAddress?.ipAddressHeaders).not.toContain("x-forwarded-for");
});
it("configures onAPIError.throw so an unexpected failure escapes Better Auth's own unsanitized fallback logger instead of being swallowed internally", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
expect(options.onAPIError).toEqual({ throw: true });
});
it("configures an explicit, error-only Better Auth logger that never forwards Better Auth's own message or arguments", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
const loggerOption = options.logger as {
disabled?: boolean;
level?: string;
log?: (...args: unknown[]) => void;
};
expect(loggerOption.disabled).toBe(false);
expect(loggerOption.level).toBe("error");
expect(typeof loggerOption.log).toBe("function");
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
try {
const hostileException = new Error(
"password=hunter2 host=db.internal.example port=5432 statement_timeout at /var/app/src/secret.ts:10:5",
);
loggerOption.log?.(
"error",
"Invalid origin: https://evil.example.test",
hostileException,
"select * from ekklesia_core_session",
);
expect(errorSpy).toHaveBeenCalledTimes(1);
const emitted = errorSpy.mock.calls
.flat()
.map((value) =>
value instanceof Error ? `${value.message}\n${value.stack ?? ""}` : String(value),
)
.join("\n");
expect(emitted).not.toContain("hunter2");
expect(emitted).not.toContain("db.internal.example");
expect(emitted).not.toContain("5432");
expect(emitted).not.toContain("/var/app");
expect(emitted).not.toContain("evil.example.test");
expect(emitted.toLowerCase()).not.toContain("select");
expect(emitted.toLowerCase()).not.toContain("stack");
} finally {
errorSpy.mockRestore();
}
});
it("never publishes a Better Auth log at or below warn level", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
const loggerOption = options.logger as { log?: (...args: unknown[]) => void };
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
try {
loggerOption.log?.("warn", "a warning", "detail");
loggerOption.log?.("info", "some info", "detail");
loggerOption.log?.("debug", "some debug", "detail");
expect(errorSpy).not.toHaveBeenCalled();
expect(warnSpy).not.toHaveBeenCalled();
expect(logSpy).not.toHaveBeenCalled();
} finally {
errorSpy.mockRestore();
warnSpy.mockRestore();
logSpy.mockRestore();
}
});
it("only widens the sign-in/sign-up rate-limit window when a narrow override is explicitly injected", () => {
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() });
createIdentityRuntime({
config: validConfig(),
db: fakeDb(),
rateLimitOverride: { max: 1000, window: 1 },
});
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
expect(options.rateLimit).toEqual({
customRules: {
"/sign-in/email": { max: 1000, window: 1 },
"/sign-up/email": { max: 1000, window: 1 },
},
enabled: true,
max: 100,
window: 10,
});
});
it("sanitizes a construction failure without leaking the underlying error, secret, or URL", () => {
const hostileSecret = validConfig().secret;
betterAuthMock.mockImplementation(() => {
throw new Error(
`Better Auth failed for secret ${hostileSecret} at https://internal.example.test`,
);
});
let captured: unknown;
try {
createIdentityRuntime({ config: validConfig(), db: fakeDb() });
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(IdentityRuntimeError);
expect(captured).toMatchObject({ code: "IDENTITY_CONSTRUCTION_FAILED" });
const publicText = `${String(captured)}\n${
captured instanceof Error ? (captured.stack ?? "") : ""
}`;
expect(publicText).not.toContain(hostileSecret);
expect(publicText).not.toContain("internal.example.test");
});
});
describe("Core identity runtime — handleAuthRequest", () => {
beforeEach(() => {
betterAuthMock.mockReset();
drizzleAdapterMock.mockClear();
});
it("delegates directly to Better Auth's Fetch handler and passes the response through unmodified", async () => {
const expectedResponse = new Response("ok", { status: 200 });
const handler = vi.fn().mockResolvedValue(expectedResponse);
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const request = new Request("https://identity.example.test/api/auth/sign-in/email");
const response = await runtime.handleAuthRequest(request);
expect(handler).toHaveBeenCalledWith(request);
expect(response).toBe(expectedResponse);
});
it("passes through Better Auth's own error responses (e.g. 401, 403, 422) unmodified", async () => {
const errorResponse = new Response(JSON.stringify({ code: "INVALID_EMAIL_OR_PASSWORD" }), {
status: 401,
});
const handler = vi.fn().mockResolvedValue(errorResponse);
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const response = await runtime.handleAuthRequest(
new Request("https://identity.example.test/x"),
);
expect(response.status).toBe(401);
});
it("sanitizes a genuinely unexpected handler failure", async () => {
const handler = vi.fn().mockRejectedValue(new Error("postgresql://admin:[email protected]/x"));
betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() });
let captured: unknown;
try {
await runtime.handleAuthRequest(new Request("https://identity.example.test/x"));
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(IdentityRuntimeError);
expect(captured).toMatchObject({ code: "AUTH_REQUEST_FAILED" });
const publicText = `${String(captured)}\n${
captured instanceof Error ? (captured.stack ?? "") : ""
}`;
expect(publicText).not.toContain("db.internal");
expect(publicText).not.toContain("admin");
});
});
describe("Core identity runtime — resolvePrincipal", () => {
beforeEach(() => {
betterAuthMock.mockReset();
drizzleAdapterMock.mockClear();
});
it("maps a valid session to an authenticated principal containing only the opaque user id", async () => {
const getSession = vi.fn().mockResolvedValue({
session: { id: "sess_1", token: "tok" },
user: { email: "[email protected]", id: "user_123", name: "Person" },
});
betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() });
const principal = await runtime.resolvePrincipal(
new Headers({ cookie: "better-auth.session_token=tok" }),
);
expect(principal).toEqual({ kind: "authenticated", userId: "user_123" });
expect(Object.keys(principal)).toEqual(["kind", "userId"]);
expect(Object.isFrozen(principal)).toBe(true);
});
it("maps a missing session with no cookie header to anonymous without probing the database", async () => {
const getSession = vi.fn().mockResolvedValue(null);
const execute = vi.fn().mockResolvedValue(undefined);
betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb({ execute }) });
const principal = await runtime.resolvePrincipal(new Headers());
expect(principal).toEqual({ kind: "anonymous" });
expect(execute).not.toHaveBeenCalled();
});
it("maps an expired/revoked/malformed session (cookie present, null session) to anonymous when the database is reachable", async () => {
const getSession = vi.fn().mockResolvedValue(null);
const execute = vi.fn().mockResolvedValue(undefined);
betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb({ execute }) });
const principal = await runtime.resolvePrincipal(
new Headers({ cookie: "better-auth.session_token=stale-or-garbage" }),
);
expect(principal).toEqual({ kind: "anonymous" });
expect(execute).toHaveBeenCalledTimes(1);
});
it("never downgrades an infrastructure failure to anonymous: throws when the database is unreachable", async () => {
const getSession = vi.fn().mockResolvedValue(null);
const execute = vi.fn().mockRejectedValue(new Error("connection terminated unexpectedly"));
betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb({ execute }) });
let captured: unknown;
try {
await runtime.resolvePrincipal(new Headers({ cookie: "better-auth.session_token=x" }));
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(IdentityRuntimeError);
expect(captured).toMatchObject({ code: "PRINCIPAL_RESOLUTION_FAILED" });
});
it("treats an unexpected throw from getSession itself as a sanitized infrastructure failure", async () => {
const hostile = "password=hunter2 host=db.internal.example port=5432";
const getSession = vi.fn().mockRejectedValue(new Error(hostile));
betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() });
const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() });
let captured: unknown;
try {
await runtime.resolvePrincipal(new Headers({ cookie: "better-auth.session_token=x" }));
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(IdentityRuntimeError);
expect(captured).toMatchObject({ code: "PRINCIPAL_RESOLUTION_FAILED" });
const publicText = `${String(captured)}\n${
captured instanceof Error ? (captured.stack ?? "") : ""
}`;
expect(publicText).not.toContain("hunter2");
expect(publicText).not.toContain("db.internal.example");
});
});