Skip to content

Commit ff98047

Browse files
BekacruCopilot
andauthored
feat: add support for trusted proxy headers in base URL inference (#6285)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3ed454b commit ff98047

File tree

4 files changed

+88
-4
lines changed

4 files changed

+88
-4
lines changed

packages/better-auth/src/auth/auth.test.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { createAuthEndpoint } from "@better-auth/core/api";
1+
import {
2+
createAuthEndpoint,
3+
createAuthMiddleware,
4+
} from "@better-auth/core/api";
25
import type { router } from "better-auth/api";
3-
import { describe, expectTypeOf, test } from "vitest";
6+
import { describe, expect, expectTypeOf, test } from "vitest";
7+
import { createAuthClient } from "../client";
8+
import { getTestInstance } from "../test-utils";
49
import type { Auth } from "../types";
510
import { betterAuth } from "./auth";
611

@@ -62,3 +67,60 @@ describe("auth type", () => {
6267
expectTypeOf<R>().toEqualTypeOf<{ data: { message: string } }>();
6368
});
6469
});
70+
71+
describe("auth with trusted proxy headers", () => {
72+
test("shouldn't infer base url from proxy headers if trusted", async () => {
73+
let baseURL: string | undefined;
74+
const { auth, customFetchImpl } = await getTestInstance({
75+
baseURL: undefined,
76+
advanced: {
77+
trustedProxyHeaders: true,
78+
},
79+
hooks: {
80+
before: createAuthMiddleware(async (ctx) => {
81+
baseURL = ctx.context.baseURL;
82+
}),
83+
},
84+
});
85+
const client = createAuthClient({
86+
fetchOptions: {
87+
customFetchImpl,
88+
},
89+
baseURL: "http://localhost:3000",
90+
});
91+
const res = await client.$fetch("/ok", {
92+
headers: {
93+
"x-forwarded-host": "localhost:3001",
94+
"x-forwarded-proto": "http",
95+
},
96+
});
97+
expect(baseURL).toBe("http://localhost:3001/api/auth");
98+
});
99+
test("shouldn't infer base url from proxy headers if not trusted", async () => {
100+
let baseURL: string | undefined;
101+
const { customFetchImpl } = await getTestInstance({
102+
baseURL: undefined,
103+
advanced: {
104+
trustedProxyHeaders: false,
105+
},
106+
hooks: {
107+
before: createAuthMiddleware(async (ctx) => {
108+
baseURL = ctx.context.baseURL;
109+
}),
110+
},
111+
});
112+
const client = createAuthClient({
113+
fetchOptions: {
114+
customFetchImpl,
115+
},
116+
baseURL: "http://localhost:3000",
117+
});
118+
const res = await client.$fetch("/ok", {
119+
headers: {
120+
"x-forwarded-host": "localhost:3001",
121+
"x-forwarded-proto": "http",
122+
},
123+
});
124+
expect(baseURL).toBe("http://localhost:3000/api/auth");
125+
});
126+
});

packages/better-auth/src/auth/base.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ export const createBetterAuth = <Options extends BetterAuthOptions>(
3030
const ctx = await authContext;
3131
const basePath = ctx.options.basePath || "/api/auth";
3232
if (!ctx.options.baseURL) {
33-
const baseURL = getBaseURL(undefined, basePath, request);
33+
const baseURL = getBaseURL(
34+
undefined,
35+
basePath,
36+
request,
37+
undefined,
38+
ctx.options.advanced?.trustedProxyHeaders,
39+
);
3440
if (baseURL) {
3541
ctx.baseURL = baseURL;
3642
ctx.options.baseURL = getOrigin(ctx.baseURL) || undefined;
@@ -40,6 +46,7 @@ export const createBetterAuth = <Options extends BetterAuthOptions>(
4046
);
4147
}
4248
}
49+
4350
ctx.trustedOrigins = [
4451
...(options.trustedOrigins
4552
? Array.isArray(options.trustedOrigins)

packages/better-auth/src/utils/url.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function getBaseURL(
5555
path?: string,
5656
request?: Request,
5757
loadEnv?: boolean,
58+
trustedProxyHeaders?: boolean | undefined,
5859
) {
5960
if (url) {
6061
return withPath(url, path);
@@ -76,7 +77,7 @@ export function getBaseURL(
7677

7778
const fromRequest = request?.headers.get("x-forwarded-host");
7879
const fromRequestProto = request?.headers.get("x-forwarded-proto");
79-
if (fromRequest && fromRequestProto) {
80+
if (fromRequest && fromRequestProto && trustedProxyHeaders) {
8081
return withPath(`${fromRequestProto}://${fromRequest}`, path);
8182
}
8283

packages/core/src/types/init-options.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,20 @@ export type BetterAuthAdvancedOptions = {
257257
generateId?: GenerateIdFn | false | "serial" | "uuid";
258258
}
259259
| undefined;
260+
/**
261+
* Trusted proxy headers
262+
*
263+
264+
* - `x-forwarded-host`
265+
* - `x-forwarded-proto`
266+
*
267+
* If set to `true` and no `baseURL` option is provided, we will use the headers to infer the
268+
* base URL.
269+
*
270+
* ⚠︎ This may expose your application to security vulnerabilities if not
271+
* used correctly. Please use this with caution.
272+
*/
273+
trustedProxyHeaders?: boolean | undefined;
260274
};
261275

262276
export type BetterAuthOptions = {

0 commit comments

Comments
 (0)