feat(deploy) : serve sudloh.com alongside guide.sudloh.com
This commit is contained in:
+15
-1
@@ -11,7 +11,7 @@ vi.mock("better-auth/plugins", () => ({ admin: (options: unknown) => options,
|
||||
captcha: (options: unknown) => ({ id: "captcha", options }),
|
||||
emailOTP: (options: unknown) => ({ id: "email-otp", options }) }));
|
||||
|
||||
const envNames = ["NODE_ENV", "DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_SECRET", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "TURNSTILE_SECRET_KEY", "BUZZ_DEMO_MODE", "RESEND_API_KEY", "TRUSTED_CLIENT_IP_HEADER"] as const;
|
||||
const envNames = ["NODE_ENV", "DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_TRUSTED_ORIGINS", "BETTER_AUTH_SECRET", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "TURNSTILE_SECRET_KEY", "BUZZ_DEMO_MODE", "RESEND_API_KEY", "TRUSTED_CLIENT_IP_HEADER"] as const;
|
||||
const testEnv = process.env as Record<string, string | undefined>;
|
||||
const originalEnv = envNames.map((name) => testEnv[name]);
|
||||
|
||||
@@ -27,6 +27,7 @@ beforeEach(() => {
|
||||
mocks.auth.mockReturnValue({ api: { getSession: mocks.session } });
|
||||
mocks.session.mockResolvedValue(null);
|
||||
delete testEnv.TRUSTED_CLIENT_IP_HEADER;
|
||||
delete testEnv.BETTER_AUTH_TRUSTED_ORIGINS;
|
||||
});
|
||||
afterEach(() => {
|
||||
envNames.forEach((name, index) => {
|
||||
@@ -37,6 +38,19 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("actual administrator session boundary", () => {
|
||||
it("resolves auth callbacks on either configured production host", async () => {
|
||||
testEnv.BETTER_AUTH_URL = "https://guide.sudloh.com";
|
||||
testEnv.BETTER_AUTH_TRUSTED_ORIGINS = "https://guide.sudloh.com, https://sudloh.com";
|
||||
(await import("./server")).getAuth();
|
||||
const options = mocks.auth.mock.calls.at(-1)![0];
|
||||
expect(options.baseURL).toEqual({
|
||||
allowedHosts: ["guide.sudloh.com", "guide.sudloh.com", "sudloh.com"],
|
||||
fallback: "https://guide.sudloh.com",
|
||||
protocol: "https",
|
||||
});
|
||||
expect(options.trustedOrigins).toEqual(["https://guide.sudloh.com", "https://sudloh.com"]);
|
||||
});
|
||||
|
||||
it("does not grant access when the old demo flag is enabled", async () => {
|
||||
testEnv.BUZZ_DEMO_MODE = "true";
|
||||
const { getAdminSession, requireAdmin } = await import("./server");
|
||||
|
||||
+9
-5
@@ -47,6 +47,9 @@ function hasAuthConfiguration(): boolean {
|
||||
|
||||
function createAuth() {
|
||||
const google = googleCredentials();
|
||||
const baseURL = required("BETTER_AUTH_URL");
|
||||
const trustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS
|
||||
?.split(",").map((origin) => origin.trim()).filter(Boolean);
|
||||
return betterAuth({
|
||||
appName: "Buzz Guide",
|
||||
database: drizzleAdapter(getDb(), {
|
||||
@@ -59,11 +62,12 @@ function createAuth() {
|
||||
},
|
||||
transaction: true,
|
||||
}),
|
||||
baseURL: required("BETTER_AUTH_URL"),
|
||||
trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS
|
||||
?.split(",")
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean),
|
||||
baseURL: trustedOrigins?.length ? {
|
||||
allowedHosts: [baseURL, ...trustedOrigins].map((origin) => new URL(origin).host),
|
||||
fallback: baseURL,
|
||||
protocol: new URL(baseURL).protocol === "https:" ? "https" : "http",
|
||||
} : baseURL,
|
||||
trustedOrigins,
|
||||
secret: required("BETTER_AUTH_SECRET"),
|
||||
socialProviders: google ? { google } : undefined,
|
||||
account: {
|
||||
|
||||
@@ -3,10 +3,13 @@ import { boundedBody, errorResponse, HttpError, readJson, requireSameOrigin, wit
|
||||
import { trustedClientAddress } from "./rate-limit";
|
||||
|
||||
const originalAuthUrl = process.env.BETTER_AUTH_URL;
|
||||
const originalTrustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS;
|
||||
const originalClientIpHeader = process.env.TRUSTED_CLIENT_IP_HEADER;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
if (originalTrustedOrigins === undefined) delete process.env.BETTER_AUTH_TRUSTED_ORIGINS;
|
||||
else process.env.BETTER_AUTH_TRUSTED_ORIGINS = originalTrustedOrigins;
|
||||
if (originalAuthUrl === undefined) delete process.env.BETTER_AUTH_URL;
|
||||
else process.env.BETTER_AUTH_URL = originalAuthUrl;
|
||||
if (originalClientIpHeader === undefined) delete process.env.TRUSTED_CLIENT_IP_HEADER;
|
||||
@@ -14,6 +17,20 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("request boundaries", () => {
|
||||
it("allows both configured domains while rejecting unlisted origins", () => {
|
||||
process.env.BETTER_AUTH_URL = "https://guide.sudloh.com";
|
||||
process.env.BETTER_AUTH_TRUSTED_ORIGINS = "https://guide.sudloh.com, https://sudloh.com";
|
||||
for (const origin of ["https://guide.sudloh.com", "https://sudloh.com"]) {
|
||||
expect(() => requireSameOrigin(new Request("http://internal", { headers: { origin } }))).not.toThrow();
|
||||
}
|
||||
for (const origin of ["http://sudloh.com", "https://evil.sudloh.com", "https://sudloh.com.evil.test"]) {
|
||||
expect(() => requireSameOrigin(new Request("http://internal", { headers: { origin } }))).toThrow("cross-origin");
|
||||
}
|
||||
expect(() => requireSameOrigin(new Request("http://internal", {
|
||||
headers: { origin: "https://sudloh.com", "sec-fetch-site": "cross-site" },
|
||||
}))).toThrow("cross-origin");
|
||||
});
|
||||
|
||||
it("compares origin to configuration, not attacker-controlled Host", () => {
|
||||
process.env.BETTER_AUTH_URL = "https://guide.example.test";
|
||||
expect(() => requireSameOrigin(new Request("https://evil.test", { headers: { origin: "https://evil.test", host: "evil.test" } }))).toThrow("cross-origin");
|
||||
|
||||
@@ -25,8 +25,11 @@ export function errorResponse(cause: unknown): Response {
|
||||
export function requireSameOrigin(request: Request) {
|
||||
const configured = process.env.BETTER_AUTH_URL;
|
||||
if (!configured) throw new HttpError(503, "origin-not-configured");
|
||||
const allowedOrigins = [new URL(configured).origin, ...(
|
||||
process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",").map((value) => value.trim()).filter(Boolean) ?? []
|
||||
)];
|
||||
const origin = request.headers.get("origin");
|
||||
if (origin !== new URL(configured).origin || request.headers.get("sec-fetch-site") === "cross-site") {
|
||||
if (!origin || !allowedOrigins.includes(origin) || request.headers.get("sec-fetch-site") === "cross-site") {
|
||||
throw new HttpError(403, "cross-origin-request");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user