diff --git a/packages/core/src/lib/utils/env.ts b/packages/core/src/lib/utils/env.ts index 38e825a750..159c1a0d71 100644 --- a/packages/core/src/lib/utils/env.ts +++ b/packages/core/src/lib/utils/env.ts @@ -2,11 +2,27 @@ import type { AuthAction } from "../../types.js" import type { AuthConfig } from "../../index.js" import { setLogger } from "./logger.js" -/** Set default env variables on the config object */ -export function setEnvDefaults(envObject: any, config: AuthConfig) { +/** + * Set default env variables on the config object + * @param suppressWarnings intended for framework authors. + */ +export function setEnvDefaults( + envObject: any, + config: AuthConfig, + suppressBasePathWarning = false +) { try { const url = envObject.AUTH_URL - if (url && !config.basePath) config.basePath = new URL(url).pathname + if (url) { + if (config.basePath) { + if (!suppressBasePathWarning) { + const logger = setLogger(config) + logger.warn("env-url-basepath-redundant") + } + } else { + config.basePath = new URL(url).pathname + } + } } catch { } finally { config.basePath ??= `/auth` @@ -53,7 +69,6 @@ export function createActionURL( envObject: any, config: Pick ): URL { - const logger = setLogger(config) const basePath = config?.basePath let envUrl = envObject.AUTH_URL ?? envObject.NEXTAUTH_URL @@ -61,11 +76,10 @@ export function createActionURL( if (envUrl) { url = new URL(envUrl) if (basePath && basePath !== "/" && url.pathname !== "/") { - logger.warn( - url.pathname === basePath - ? "env-url-basepath-redundant" - : "env-url-basepath-mismatch" - ) + if (url.pathname !== basePath) { + const logger = setLogger(config) + logger.warn("env-url-basepath-mismatch") + } url.pathname = "/" } } else { diff --git a/packages/core/test/env.test.ts b/packages/core/test/env.test.ts index 214e12210a..4e3413e310 100644 --- a/packages/core/test/env.test.ts +++ b/packages/core/test/env.test.ts @@ -1,29 +1,25 @@ -import { - afterAll, - afterEach, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest" +import { beforeEach, describe, expect, it, vi } from "vitest" import { AuthConfig } from "../src/index.js" import { setEnvDefaults, createActionURL } from "../src/lib/utils/env.js" import Auth0 from "../src/providers/auth0.js" import Resend from "../src/providers/resend.js" -const testConfig: AuthConfig = { - providers: [Auth0, Resend({})], -} +const logger = { warn: vi.fn() } -let authConfig: AuthConfig +describe("config is inferred from environment variables", () => { + const testConfig: AuthConfig = { + providers: [Auth0, Resend({})], + logger, + } -beforeEach(() => { - authConfig = { ...testConfig } // clone -}) + let authConfig: AuthConfig + + beforeEach(() => { + authConfig = { ...testConfig } // clone + vi.resetAllMocks() + }) -describe("config is inferred from environment variables", () => { it("providers (client id, client secret, issuer, api key)", () => { const env = { AUTH_AUTH0_ID: "asdf", @@ -41,18 +37,21 @@ describe("config is inferred from environment variables", () => { expect(p1.issuer).toBe(env.AUTH_AUTH0_ISSUER) // @ts-expect-error expect(p2.apiKey).toBe(env.AUTH_RESEND_KEY) + expect(logger.warn).not.toHaveBeenCalled() }) it("AUTH_SECRET", () => { const env = { AUTH_SECRET: "secret" } setEnvDefaults(env, authConfig) expect(authConfig.secret?.[0]).toBe(env.AUTH_SECRET) + expect(logger.warn).not.toHaveBeenCalled() }) it("AUTH_SECRET, prefer config", () => { const env = { AUTH_SECRET: "0", AUTH_SECRET_1: "1" } setEnvDefaults(env, authConfig) expect(authConfig.secret).toEqual(["1", "0"]) + expect(logger.warn).not.toHaveBeenCalled() }) it("AUTH_SECRET, prefer config", () => { @@ -60,18 +59,21 @@ describe("config is inferred from environment variables", () => { authConfig.secret = ["old"] setEnvDefaults(env, authConfig) expect(authConfig.secret).toEqual(["old"]) + expect(logger.warn).not.toHaveBeenCalled() }) it("AUTH_REDIRECT_PROXY_URL", () => { const env = { AUTH_REDIRECT_PROXY_URL: "http://example.com" } setEnvDefaults(env, authConfig) expect(authConfig.redirectProxyUrl).toBe(env.AUTH_REDIRECT_PROXY_URL) + expect(logger.warn).not.toHaveBeenCalled() }) it("AUTH_URL", () => { const env = { AUTH_URL: "http://n/api/auth" } setEnvDefaults(env, authConfig) expect(authConfig.basePath).toBe("/api/auth") + expect(logger.warn).not.toHaveBeenCalled() }) it("AUTH_URL + prefer config", () => { @@ -80,12 +82,23 @@ describe("config is inferred from environment variables", () => { authConfig.basePath = fromConfig setEnvDefaults(env, authConfig) expect(authConfig.basePath).toBe(fromConfig) + expect(logger.warn).toHaveBeenCalledWith("env-url-basepath-redundant") + }) + + it("AUTH_URL + prefer config but suppress base path waring", () => { + const env = { AUTH_URL: "http://n/api/auth" } + const fromConfig = "/basepath-from-config" + authConfig.basePath = fromConfig + setEnvDefaults(env, authConfig, true) + expect(authConfig.basePath).toBe(fromConfig) + expect(logger.warn).not.toHaveBeenCalled() }) it("AUTH_URL, but invalid value", () => { const env = { AUTH_URL: "secret" } setEnvDefaults(env, authConfig) expect(authConfig.basePath).toBe("/auth") + expect(logger.warn).not.toHaveBeenCalled() }) it.each([ @@ -97,14 +110,13 @@ describe("config is inferred from environment variables", () => { ])(`%j`, (env, expected) => { setEnvDefaults(env, authConfig) expect(authConfig).toMatchObject(expected) + expect(logger.warn).not.toHaveBeenCalled() }) }) describe("createActionURL", () => { - const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) - - afterEach(() => { - consoleWarnSpy.mockClear() + beforeEach(() => { + vi.resetAllMocks() }) it.each([ @@ -234,10 +246,23 @@ describe("createActionURL", () => { }, expected: "https://sub.domain.env.com/api/auth/signout", }, + { + args: { + action: "signout", + protocol: undefined, + headers: new Headers({}), + env: { AUTH_URL: "http://localhost:3000/my-app/api/auth" }, + config: { basePath: "/my-app/api/auth" }, + }, + expected: "http://localhost:3000/my-app/api/auth/signout", + }, ])("%j", ({ args, expected }) => { + const argsWithLogger = { ...args, config: { ...args.config, logger } } // @ts-expect-error - expect(createActionURL(...Object.values(args)).toString()).toBe(expected) - expect(consoleWarnSpy).not.toHaveBeenCalled() + expect(createActionURL(...Object.values(argsWithLogger)).toString()).toBe( + expected + ) + expect(logger.warn).not.toHaveBeenCalled() }) it.each([ @@ -249,7 +274,10 @@ describe("createActionURL", () => { env: { AUTH_URL: "http://localhost:3000/my-app/api/auth/" }, config: { basePath: "/my-app/api/auth" }, }, - expected: "http://localhost:3000/my-app/api/auth/signout", + expected: { + url: "http://localhost:3000/my-app/api/auth/signout", + warningMessage: "env-url-basepath-mismatch", + }, }, { args: { @@ -259,15 +287,17 @@ describe("createActionURL", () => { env: { AUTH_URL: "https://sub.domain.env.com/my-app" }, config: { basePath: "/api/auth" }, }, - expected: "https://sub.domain.env.com/api/auth/signout", + expected: { + url: "https://sub.domain.env.com/api/auth/signout", + warningMessage: "env-url-basepath-mismatch", + }, }, ])("Duplicate path configurations: %j", ({ args, expected }) => { + const argsWithLogger = { ...args, config: { ...args.config, logger } } // @ts-expect-error - expect(createActionURL(...Object.values(args)).toString()).toBe(expected) - expect(consoleWarnSpy).toHaveBeenCalled() - }) - - afterAll(() => { - consoleWarnSpy.mockRestore() + expect(createActionURL(...Object.values(argsWithLogger)).toString()).toBe( + expected.url + ) + expect(logger.warn).toHaveBeenCalledWith(expected.warningMessage) }) }) diff --git a/packages/next-auth/src/lib/env.ts b/packages/next-auth/src/lib/env.ts index 697ad31099..f24e688360 100644 --- a/packages/next-auth/src/lib/env.ts +++ b/packages/next-auth/src/lib/env.ts @@ -30,6 +30,6 @@ export function setEnvDefaults(config: NextAuthConfig) { } catch { } finally { config.basePath ||= "/api/auth" - coreSetEnvDefaults(process.env, config) + coreSetEnvDefaults(process.env, config, true) } } diff --git a/packages/next-auth/test/env.test.ts b/packages/next-auth/test/env.test.ts new file mode 100644 index 0000000000..d5bd609bc2 --- /dev/null +++ b/packages/next-auth/test/env.test.ts @@ -0,0 +1,167 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { NextRequest } from "next/server.js" + +import { reqWithEnvURL, setEnvDefaults } from "../lib/env" +import { setEnvDefaults as coreSetEnvDefaults } from "@auth/core" +import type { NextAuthConfig } from "../lib/index.js" + +vi.mock("next/server.js", () => ({ + NextRequest: vi.fn(), +})) + +vi.mock("@auth/core", () => ({ + setEnvDefaults: vi.fn(), +})) + +describe("env", () => { + beforeEach(() => { + vi.resetModules() + vi.resetAllMocks() + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + describe("reqWithEnvURL", () => { + it("should return the original request if AUTH_URL and NEXTAUTH_URL are not set", () => { + const mockReq = { + nextUrl: { href: "http://example.com", origin: "http://example.com" }, + } + const result = reqWithEnvURL(mockReq as NextRequest) + + expect(result).toBe(mockReq) + }) + + it("should return a new request with modified URL if AUTH_URL is set", () => { + vi.stubEnv("AUTH_URL", "http://auth.example.com") + + const mockReq = { + nextUrl: { + href: "http://example.com/path", + origin: "http://example.com", + }, + } + const mockNewReq = {} + vi.mocked(NextRequest).mockReturnValue(mockNewReq as NextRequest) + + const result = reqWithEnvURL(mockReq as NextRequest) + + expect(NextRequest).toHaveBeenCalledWith( + "http://auth.example.com/path", + mockReq + ) + expect(result).toBe(mockNewReq) + }) + }) + + describe("setEnvDefaults", () => { + it("should set secret from AUTH_SECRET", () => { + vi.stubEnv("AUTH_SECRET", "test-secret") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.secret).toBe("test-secret") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it("should set secret from NEXTAUTH_SECRET if AUTH_SECRET is not set", () => { + vi.stubEnv("NEXTAUTH_SECRET", "next-auth-secret") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.secret).toBe("next-auth-secret") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it("should not override existing secret in config", () => { + vi.stubEnv("AUTH_SECRET", "test-secret") + + const config = { secret: "existing-secret" } as NextAuthConfig + setEnvDefaults(config) + + expect(config.secret).toBe("existing-secret") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it("should prioritize AUTH_SECRET over NEXTAUTH_SECRET", () => { + vi.stubEnv("AUTH_SECRET", "auth-secret") + vi.stubEnv("NEXTAUTH_SECRET", "nextauth-secret") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.secret).toBe("auth-secret") + }) + + it("should set basePath from AUTH_URL", () => { + vi.stubEnv("AUTH_URL", "http://example.com/custom-auth") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.basePath).toBe("/custom-auth") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it("should set basePath from NEXTAUTH_URL if AUTH_URL is not set", () => { + vi.stubEnv("NEXTAUTH_URL", "http://example.com/next-auth") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.basePath).toBe("/next-auth") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it('should not set basePath if URL pathname is "/"', () => { + vi.stubEnv("AUTH_URL", "http://example.com/") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.basePath).toBe("/api/auth") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it("should not override existing basePath in config", () => { + vi.stubEnv("AUTH_URL", "http://example.com/custom-auth") + + const config = { basePath: "/existing-path" } as NextAuthConfig + setEnvDefaults(config) + + expect(config.basePath).toBe("/existing-path") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it("should prioritize AUTH_URL over NEXTAUTH_URL", () => { + vi.stubEnv("AUTH_URL", "http://example.com/auth-url") + vi.stubEnv("NEXTAUTH_URL", "http://example.com/nextauth-url") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.basePath).toBe("/auth-url") + }) + + it("should default basePath to /api/auth if no URL is set", () => { + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.basePath).toBe("/api/auth") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + + it("should handle invalid URL gracefully", () => { + vi.stubEnv("AUTH_URL", "invalid-url") + + const config = {} as NextAuthConfig + setEnvDefaults(config) + + expect(config.basePath).toBe("/api/auth") + expect(coreSetEnvDefaults).toHaveBeenCalledWith(process.env, config, true) + }) + }) +})