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,