diff --git a/docs/content/docs/reference/options.mdx b/docs/content/docs/reference/options.mdx
index 75ae6cf32a..edcb06afc1 100644
--- a/docs/content/docs/reference/options.mdx
+++ b/docs/content/docs/reference/options.mdx
@@ -27,7 +27,11 @@ export const auth = betterAuth({
})
```
-If not explicitly set, the system will check for the environment variable `process.env.BETTER_AUTH_URL`
+If not explicitly set, the system will check for the environment variable `BETTER_AUTH_URL`. If that's also not set, it will be inferred from the incoming request.
+
+
+Relying on request inference is not recommended. For security and stability, always set `baseURL` explicitly in your config or via the `BETTER_AUTH_URL` environment variable.
+
## `basePath`
diff --git a/packages/better-auth/src/api/middlewares/origin-check.test.ts b/packages/better-auth/src/api/middlewares/origin-check.test.ts
index d5f92569a1..d0fda6e60f 100644
--- a/packages/better-auth/src/api/middlewares/origin-check.test.ts
+++ b/packages/better-auth/src/api/middlewares/origin-check.test.ts
@@ -247,8 +247,8 @@ describe("origin check middleware", async (it) => {
});
});
-describe("trustedOrigins regression tests", async (it) => {
- it("should respect trustedOrigins from config array through full request flow", async () => {
+describe("trusted origins with baseURL inferred from request", async (it) => {
+ it("should respect trustedOrigins array when baseURL is NOT in config", async () => {
const { customFetchImpl, testUser } = await getTestInstance({
trustedOrigins: ["http://my-frontend.com"],
emailAndPassword: {
@@ -280,7 +280,7 @@ describe("trustedOrigins regression tests", async (it) => {
expect(res.data?.user).toBeDefined();
});
- it("should reject origins not in trustedOrigins config", async () => {
+ it("should reject untrusted origins even when baseURL is inferred", async () => {
const { customFetchImpl, testUser } = await getTestInstance({
trustedOrigins: ["http://my-frontend.com"],
emailAndPassword: {
@@ -311,7 +311,7 @@ describe("trustedOrigins regression tests", async (it) => {
expect(res.error?.status).toBe(403);
});
- it("should respect BETTER_AUTH_TRUSTED_ORIGINS env variable through full request flow", async () => {
+ it("should respect BETTER_AUTH_TRUSTED_ORIGINS env when baseURL is NOT in config", async () => {
vi.stubEnv("BETTER_AUTH_TRUSTED_ORIGINS", "http://env-frontend.com");
try {
@@ -347,4 +347,90 @@ describe("trustedOrigins regression tests", async (it) => {
vi.unstubAllEnvs();
}
});
+
+ it("should allow requests from inferred baseURL origin", async () => {
+ const { customFetchImpl, testUser } = await getTestInstance({
+ emailAndPassword: {
+ enabled: true,
+ },
+ advanced: {
+ disableCSRFCheck: false,
+ disableOriginCheck: false,
+ },
+ });
+
+ const client = createAuthClient({
+ baseURL: "http://localhost:3000",
+ fetchOptions: {
+ customFetchImpl,
+ headers: {
+ origin: "http://localhost:3000",
+ cookie: "session=test",
+ },
+ },
+ });
+
+ const res = await client.signIn.email({
+ email: testUser.email,
+ password: testUser.password,
+ callbackURL: "http://localhost:3000/dashboard",
+ });
+
+ expect(res.data?.user).toBeDefined();
+ });
+
+ it("should support both config array and env var together when baseURL is inferred", async () => {
+ vi.stubEnv("BETTER_AUTH_TRUSTED_ORIGINS", "http://env-origin.com");
+
+ try {
+ const { customFetchImpl, testUser } = await getTestInstance({
+ trustedOrigins: ["http://config-origin.com"],
+ emailAndPassword: {
+ enabled: true,
+ },
+ advanced: {
+ disableCSRFCheck: false,
+ disableOriginCheck: false,
+ },
+ });
+
+ const client = createAuthClient({
+ baseURL: "http://localhost:3000",
+ fetchOptions: {
+ customFetchImpl,
+ headers: {
+ origin: "http://config-origin.com",
+ cookie: "session=test",
+ },
+ },
+ });
+
+ const res1 = await client.signIn.email({
+ email: testUser.email,
+ password: testUser.password,
+ callbackURL: "http://config-origin.com/dashboard",
+ });
+ expect(res1.data?.user).toBeDefined();
+
+ const client2 = createAuthClient({
+ baseURL: "http://localhost:3000",
+ fetchOptions: {
+ customFetchImpl,
+ headers: {
+ origin: "http://env-origin.com",
+ cookie: "session=test",
+ },
+ },
+ });
+
+ const res2 = await client2.signIn.email({
+ email: testUser.email,
+ password: testUser.password,
+ callbackURL: "http://env-origin.com/dashboard",
+ });
+ expect(res2.data?.user).toBeDefined();
+ } finally {
+ vi.unstubAllEnvs();
+ }
+ });
});
diff --git a/packages/better-auth/src/auth/base.ts b/packages/better-auth/src/auth/base.ts
index 419b4bb43d..6300206c42 100644
--- a/packages/better-auth/src/auth/base.ts
+++ b/packages/better-auth/src/auth/base.ts
@@ -2,6 +2,7 @@ import type { AuthContext, BetterAuthOptions } from "@better-auth/core";
import { runWithAdapter } from "@better-auth/core/context";
import { BASE_ERROR_CODES, BetterAuthError } from "@better-auth/core/error";
import { getEndpoints, router } from "../api";
+import { getTrustedOrigins } from "../context/helpers";
import type { Auth } from "../types";
import { getBaseURL, getOrigin } from "../utils/url";
@@ -37,13 +38,13 @@ export const createBetterAuth = (
if (baseURL) {
ctx.baseURL = baseURL;
ctx.options.baseURL = getOrigin(ctx.baseURL) || undefined;
+ ctx.trustedOrigins = getTrustedOrigins(ctx.options);
} else {
throw new BetterAuthError(
"Could not get base URL from request. Please provide a valid base URL.",
);
}
}
-
if (typeof options.trustedOrigins === "function") {
ctx.trustedOrigins = [
...ctx.trustedOrigins,