Skip to content

Commit 15c2985

Browse files
committed
fix(cors): normalize self-hosted origins for CORS and auth
Normalize CLIENT_URL and CHECKOUT_BASE_URL to URL origins so path/trailing slash values still match browser Origin headers, reuse the same helper for Better Auth trustedOrigins, and add focused tests for path and invalid URL cases.
1 parent 6ba9b35 commit 15c2985

File tree

3 files changed

+51
-17
lines changed

3 files changed

+51
-17
lines changed

server/src/utils/auth.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import sendOTPEmail from "@/internal/emails/sendOTPEmail.js";
2121
import { afterOrgCreated } from "./authUtils/afterOrgCreated.js";
2222
import { beforeSessionCreated } from "./authUtils/beforeSessionCreated.js";
2323
import { ADMIN_USER_IDs } from "./constants.js";
24+
import { getSelfHostedOrigins } from "./corsOrigins.js";
2425

2526
export const auth = betterAuth({
2627
baseURL: process.env.BETTER_AUTH_URL,
@@ -68,13 +69,7 @@ export const auth = betterAuth({
6869
"https://*.useautumn.com",
6970
];
7071

71-
// Always trust CLIENT_URL and CHECKOUT_BASE_URL (required for self-hosted deployments)
72-
if (process.env.CLIENT_URL) {
73-
origins.push(process.env.CLIENT_URL);
74-
}
75-
if (process.env.CHECKOUT_BASE_URL) {
76-
origins.push(process.env.CHECKOUT_BASE_URL);
77-
}
72+
origins.push(...getSelfHostedOrigins());
7873

7974
// Better Auth validates origins independently from app-level CORS.
8075
// Allow local multi-port setups for any non-production runtime.

server/src/utils/corsOrigins.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,30 @@ export const ALLOWED_ORIGINS = [
1616
"https://localhost:8080",
1717
];
1818

19+
const toOrigin = ({ url }: { url?: string }): string | undefined => {
20+
if (!url) return undefined;
21+
22+
try {
23+
return new URL(url).origin;
24+
} catch {
25+
return undefined;
26+
}
27+
};
28+
29+
export const getSelfHostedOrigins = (): string[] => {
30+
const origins = [
31+
toOrigin({ url: process.env.CLIENT_URL }),
32+
toOrigin({ url: process.env.CHECKOUT_BASE_URL }),
33+
];
34+
35+
return origins.filter((origin): origin is string => Boolean(origin));
36+
};
37+
1938
/** Allow any localhost origin in dev for multi-worktree support */
2039
export const isAllowedOrigin = (origin: string): string | undefined => {
2140
if (ALLOWED_ORIGINS.includes(origin)) return origin;
22-
23-
// Allow CLIENT_URL and CHECKOUT_BASE_URL for self-hosted deployments
24-
if (process.env.CLIENT_URL && origin === process.env.CLIENT_URL) {
25-
return origin;
26-
}
27-
if (
28-
process.env.CHECKOUT_BASE_URL &&
29-
origin === process.env.CHECKOUT_BASE_URL
30-
) {
31-
return origin;
41+
for (const selfHostedOrigin of getSelfHostedOrigins()) {
42+
if (origin === selfHostedOrigin) return origin;
3243
}
3344

3445
if (

server/tests/unit/corsOrigins.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ describe("isAllowedOrigin", () => {
7878
).toBe("https://autumn-dashboard-production.up.railway.app");
7979
});
8080

81+
test("allows CLIENT_URL origin when URL has path and trailing slash", () => {
82+
process.env.NODE_ENV = "production";
83+
process.env.CLIENT_URL =
84+
"https://autumn-dashboard-production.up.railway.app/app/";
85+
expect(
86+
isAllowedOrigin("https://autumn-dashboard-production.up.railway.app"),
87+
).toBe("https://autumn-dashboard-production.up.railway.app");
88+
});
89+
8190
test("allows CHECKOUT_BASE_URL in production", () => {
8291
process.env.NODE_ENV = "production";
8392
process.env.CHECKOUT_BASE_URL =
@@ -87,6 +96,15 @@ describe("isAllowedOrigin", () => {
8796
).toBe("https://autumn-checkout-production.up.railway.app");
8897
});
8998

99+
test("allows CHECKOUT_BASE_URL origin when URL has path", () => {
100+
process.env.NODE_ENV = "production";
101+
process.env.CHECKOUT_BASE_URL =
102+
"https://autumn-checkout-production.up.railway.app/c";
103+
expect(
104+
isAllowedOrigin("https://autumn-checkout-production.up.railway.app"),
105+
).toBe("https://autumn-checkout-production.up.railway.app");
106+
});
107+
90108
test("allows both CLIENT_URL and CHECKOUT_BASE_URL simultaneously", () => {
91109
process.env.NODE_ENV = "production";
92110
process.env.CLIENT_URL = "https://dashboard.mycompany.com";
@@ -109,6 +127,16 @@ describe("isAllowedOrigin", () => {
109127
).toBeUndefined();
110128
});
111129

130+
test("ignores invalid self-hosted URL env values", () => {
131+
process.env.NODE_ENV = "production";
132+
process.env.CLIENT_URL = "autumn-dashboard-production.up.railway.app";
133+
process.env.CHECKOUT_BASE_URL = "not-a-url";
134+
135+
expect(
136+
isAllowedOrigin("https://autumn-dashboard-production.up.railway.app"),
137+
).toBeUndefined();
138+
});
139+
112140
test("rejects custom domains when env URLs are unset", () => {
113141
process.env.NODE_ENV = "production";
114142
delete process.env.CLIENT_URL;

0 commit comments

Comments
 (0)