443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { describe, expect, it } from "vitest";
|
|
import { createAuth } from "../src/auth";
|
|
import {
|
|
booleanEnv,
|
|
csvEnv,
|
|
isTrustedOrigin,
|
|
optionalEnv,
|
|
requiredEnv,
|
|
trustedOrigins,
|
|
type Env,
|
|
} from "../src/env";
|
|
import { buildAuthEmail, sendEmail } from "../src/email";
|
|
import { createGenericOAuthProviders, createSocialProviders } from "../src/oauth";
|
|
import { authPerformanceConfig } from "../src/performance";
|
|
import {
|
|
createApiKeyConfigurations,
|
|
createAuthPlugins,
|
|
getApiKeyFromRequestHeaders,
|
|
} from "../src/plugins";
|
|
import { buildAuthSms, sendSms } from "../src/sms";
|
|
|
|
const env: Env = {
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: " http://localhost:8787, https://app.example.com ",
|
|
};
|
|
|
|
describe("auth env helpers", () => {
|
|
it("parses trusted origins from comma-separated env", () => {
|
|
expect(trustedOrigins(env)).toEqual(["http://localhost:8787", "https://app.example.com"]);
|
|
});
|
|
|
|
it("allows loopback trusted origins to match any port", () => {
|
|
const trusted = ["http://localhost", "http://127.0.0.1", "https://app.example.com"];
|
|
|
|
expect(isTrustedOrigin("http://localhost:5173", trusted)).toBe(true);
|
|
expect(isTrustedOrigin("http://localhost:9999", trusted)).toBe(true);
|
|
expect(isTrustedOrigin("http://127.0.0.1:5173", trusted)).toBe(true);
|
|
expect(isTrustedOrigin("https://app.example.com", trusted)).toBe(true);
|
|
});
|
|
|
|
it("does not allow non-loopback trusted origins to match arbitrary ports", () => {
|
|
const trusted = ["https://app.example.com"];
|
|
|
|
expect(isTrustedOrigin("https://app.example.com:8443", trusted)).toBe(false);
|
|
expect(isTrustedOrigin("https://evil.example.com", trusted)).toBe(false);
|
|
});
|
|
|
|
it("adds a trusted dynamic loopback origin for request-scoped auth config", () => {
|
|
expect(
|
|
trustedOrigins(
|
|
{
|
|
...env,
|
|
TRUSTED_ORIGINS: "http://localhost,https://app.example.com",
|
|
},
|
|
"http://localhost:5174",
|
|
),
|
|
).toContain("http://localhost:5174");
|
|
});
|
|
|
|
it("adds Expo app schemes to trusted origins", () => {
|
|
expect(trustedOrigins({ ...env, EXPO_SCHEME: "cfwauth" })).toEqual([
|
|
"http://localhost:8787",
|
|
"https://app.example.com",
|
|
"cfwauth://",
|
|
]);
|
|
});
|
|
|
|
it("parses optional csv values", () => {
|
|
expect(csvEnv(" google, github ,, ")).toEqual(["google", "github"]);
|
|
});
|
|
|
|
it("parses boolean env values", () => {
|
|
expect(booleanEnv("true")).toBe(true);
|
|
expect(booleanEnv("1")).toBe(true);
|
|
expect(booleanEnv("false")).toBe(false);
|
|
expect(booleanEnv(undefined)).toBe(false);
|
|
});
|
|
|
|
it("throws for missing required env", () => {
|
|
expect(() => requiredEnv({}, "MAIL_FROM")).toThrow("Missing required env: MAIL_FROM");
|
|
});
|
|
|
|
it("returns optional env values", () => {
|
|
expect(optionalEnv({ MAIL_FROM: "noreply@example.com" }, "MAIL_FROM")).toBe(
|
|
"noreply@example.com",
|
|
);
|
|
expect(optionalEnv({}, "MAIL_FROM")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("production auth config", () => {
|
|
it("allows the Web Shell origin in Wrangler trusted origins", () => {
|
|
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8")) as {
|
|
vars: { TRUSTED_ORIGINS: string };
|
|
};
|
|
|
|
expect(csvEnv(config.vars.TRUSTED_ORIGINS)).toContain("https://web.bowong.cc");
|
|
});
|
|
|
|
it("allows gateway and local Web Shell origins in Wrangler trusted origins", () => {
|
|
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8")) as {
|
|
vars: { TRUSTED_ORIGINS: string };
|
|
};
|
|
|
|
expect(csvEnv(config.vars.TRUSTED_ORIGINS)).toEqual(
|
|
expect.arrayContaining([
|
|
"https://cfw-gateway.bowong.cc",
|
|
"http://localhost",
|
|
"http://127.0.0.1",
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("auth performance config", () => {
|
|
it("uses conservative defaults", () => {
|
|
expect(authPerformanceConfig(env)).toEqual({
|
|
sessionCookieCacheMaxAge: 300,
|
|
apiKeyDeferUpdates: true,
|
|
});
|
|
});
|
|
|
|
it("parses session cookie cache max age from env", () => {
|
|
expect(
|
|
authPerformanceConfig({
|
|
...env,
|
|
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "60",
|
|
}).sessionCookieCacheMaxAge,
|
|
).toBe(60);
|
|
});
|
|
|
|
it("falls back for invalid session cookie cache max age", () => {
|
|
expect(
|
|
authPerformanceConfig({
|
|
...env,
|
|
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "0",
|
|
}).sessionCookieCacheMaxAge,
|
|
).toBe(300);
|
|
expect(
|
|
authPerformanceConfig({
|
|
...env,
|
|
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "not-a-number",
|
|
}).sessionCookieCacheMaxAge,
|
|
).toBe(300);
|
|
});
|
|
|
|
it("allows disabling api key deferred updates", () => {
|
|
expect(authPerformanceConfig({ ...env, API_KEY_DEFER_UPDATES: "false" })).toMatchObject({
|
|
apiKeyDeferUpdates: false,
|
|
});
|
|
expect(authPerformanceConfig({ ...env, API_KEY_DEFER_UPDATES: "true" })).toMatchObject({
|
|
apiKeyDeferUpdates: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("auth email adapter", () => {
|
|
it("builds a verification email", () => {
|
|
const message = buildAuthEmail({
|
|
kind: "verify-email",
|
|
to: "user@example.com",
|
|
url: "https://auth.example.com/verify",
|
|
});
|
|
expect(message.subject).toBe("Verify your email");
|
|
expect(message.to).toBe("user@example.com");
|
|
expect(message.text).toContain("https://auth.example.com/verify");
|
|
});
|
|
|
|
it("fails clearly when no mail provider is configured", async () => {
|
|
await expect(
|
|
sendEmail(
|
|
{
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
},
|
|
{
|
|
to: "user@example.com",
|
|
subject: "Test",
|
|
text: "Test",
|
|
},
|
|
),
|
|
).rejects.toThrow("MAIL_PROVIDER is not configured");
|
|
});
|
|
});
|
|
|
|
describe("auth sms adapter", () => {
|
|
it("builds a phone verification sms", () => {
|
|
const message = buildAuthSms({
|
|
kind: "phone-otp",
|
|
to: "+15555550100",
|
|
code: "123456",
|
|
});
|
|
expect(message.to).toBe("+15555550100");
|
|
expect(message.text).toContain("123456");
|
|
});
|
|
|
|
it("fails clearly when no sms provider is configured", async () => {
|
|
await expect(sendSms(env, { to: "+15555550100", text: "123456" })).rejects.toThrow(
|
|
"SMS_PROVIDER is not configured",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("auth plugins", () => {
|
|
it("does not enable captcha without provider config", () => {
|
|
const plugins = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
});
|
|
expect(plugins.map((plugin) => plugin.id)).not.toContain("captcha");
|
|
});
|
|
|
|
it("enables captcha when Cloudflare Turnstile config is present", () => {
|
|
const plugins = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
CAPTCHA_PROVIDER: "cloudflare-turnstile",
|
|
CAPTCHA_SECRET_KEY: "secret",
|
|
});
|
|
expect(plugins.map((plugin) => plugin.id)).toContain("captcha");
|
|
});
|
|
|
|
it("enables email otp plugin by default", () => {
|
|
const plugins = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
});
|
|
expect(plugins.map((plugin) => plugin.id)).toContain("email-otp");
|
|
});
|
|
|
|
it("enables i18n plugin by default", () => {
|
|
const plugins = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
});
|
|
expect(plugins.map((plugin) => plugin.id)).toContain("i18n");
|
|
});
|
|
|
|
it("enables account security and admin plugins", () => {
|
|
const plugins = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
});
|
|
expect(plugins.map((plugin) => plugin.id)).toEqual(
|
|
expect.arrayContaining(["two-factor", "multi-session", "last-login-method", "admin"]),
|
|
);
|
|
});
|
|
|
|
it("enables account center plugins by default", () => {
|
|
const plugins = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
EXPO_SCHEME: "cfwauth",
|
|
});
|
|
expect(plugins.map((plugin) => plugin.id)).toEqual(
|
|
expect.arrayContaining(["username", "phone-number", "organization", "api-key", "expo"]),
|
|
);
|
|
});
|
|
|
|
it("enables jwt and bearer only when requested", () => {
|
|
const disabled = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
});
|
|
expect(disabled.map((plugin) => plugin.id)).not.toContain("jwt");
|
|
expect(disabled.map((plugin) => plugin.id)).not.toContain("bearer");
|
|
|
|
const enabled = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
ENABLE_JWT: "true",
|
|
ENABLE_BEARER: "true",
|
|
});
|
|
expect(enabled.map((plugin) => plugin.id)).toEqual(expect.arrayContaining(["jwt", "bearer"]));
|
|
});
|
|
|
|
it("enables api key deferred updates by default", () => {
|
|
const configurations = createApiKeyConfigurations({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
});
|
|
|
|
expect(configurations).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
configId: "default",
|
|
deferUpdates: true,
|
|
}),
|
|
expect.objectContaining({
|
|
configId: "organization",
|
|
deferUpdates: true,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("enables API keys as get-session credentials only from x-api-key", () => {
|
|
const configurations = createApiKeyConfigurations({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
});
|
|
|
|
expect(configurations).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
configId: "default",
|
|
enableSessionForAPIKeys: true,
|
|
customAPIKeyGetter: expect.any(Function),
|
|
}),
|
|
]),
|
|
);
|
|
expect(getApiKeyFromRequestHeaders(new Headers({ "x-api-key": "cfw_secret" }))).toBe(
|
|
"cfw_secret",
|
|
);
|
|
expect(
|
|
getApiKeyFromRequestHeaders(new Headers({ authorization: "Bearer cfw_secret" })),
|
|
).toBeNull();
|
|
expect(
|
|
getApiKeyFromRequestHeaders(new Headers({ authorization: "Bearer better-auth-session" })),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("allows disabling api key deferred updates", () => {
|
|
const configurations = createApiKeyConfigurations({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
API_KEY_DEFER_UPDATES: "false",
|
|
});
|
|
|
|
expect(configurations).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
configId: "default",
|
|
deferUpdates: false,
|
|
}),
|
|
expect.objectContaining({
|
|
configId: "organization",
|
|
deferUpdates: false,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("auth runtime performance options", () => {
|
|
it("enables session cookie cache with configured max age", () => {
|
|
const auth = createAuth({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "60",
|
|
});
|
|
|
|
expect(auth.options.session?.cookieCache).toEqual({
|
|
enabled: true,
|
|
maxAge: 60,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("oauth config", () => {
|
|
it("creates built-in social providers from env", () => {
|
|
const providers = createSocialProviders({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
GOOGLE_CLIENT_ID: "google-id",
|
|
GOOGLE_CLIENT_SECRET: "google-secret",
|
|
GITHUB_CLIENT_ID: "github-id",
|
|
GITHUB_CLIENT_SECRET: "github-secret",
|
|
});
|
|
expect(Object.keys(providers)).toEqual(["google", "github"]);
|
|
});
|
|
|
|
it("creates generic oauth providers from env", () => {
|
|
const providers = createGenericOAuthProviders({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
GENERIC_OAUTH_PROVIDER_ID: "workos",
|
|
GENERIC_OAUTH_DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
|
GENERIC_OAUTH_CLIENT_ID: "client-id",
|
|
GENERIC_OAUTH_CLIENT_SECRET: "client-secret",
|
|
});
|
|
expect(providers).toEqual([
|
|
{
|
|
providerId: "workos",
|
|
discoveryUrl: "https://example.com/.well-known/openid-configuration",
|
|
clientId: "client-id",
|
|
clientSecret: "client-secret",
|
|
scopes: ["openid", "email", "profile"],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("passkey config", () => {
|
|
it("enables passkey when rp config is present", () => {
|
|
const plugins = createAuthPlugins({
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
PASSKEY_RP_ID: "localhost",
|
|
PASSKEY_RP_NAME: "cfw-auth",
|
|
PASSKEY_ORIGIN: "http://localhost:8788",
|
|
});
|
|
expect(plugins.map((plugin) => plugin.id)).toContain("passkey");
|
|
});
|
|
});
|
|
|
|
describe("Better Auth migration config", () => {
|
|
it("exports a static auth instance for the Better Auth CLI", async () => {
|
|
const module = await import("../src/auth.migration");
|
|
expect(module.auth).toBeDefined();
|
|
expect(module.default).toBe(module.auth);
|
|
expect(typeof module.auth.handler).toBe("function");
|
|
});
|
|
|
|
it("keeps schema-affecting plugins enabled for generated migrations", async () => {
|
|
const { migrationPlugins } = await import("../src/auth.migration");
|
|
const pluginIds = migrationPlugins.map((plugin) => plugin.id);
|
|
|
|
expect(pluginIds).toEqual(
|
|
expect.arrayContaining([
|
|
"open-api",
|
|
"i18n",
|
|
"email-otp",
|
|
"two-factor",
|
|
"multi-session",
|
|
"last-login-method",
|
|
"admin",
|
|
"captcha",
|
|
"generic-oauth",
|
|
"passkey",
|
|
"username",
|
|
"phone-number",
|
|
"organization",
|
|
"api-key",
|
|
"expo",
|
|
"jwt",
|
|
"bearer",
|
|
]),
|
|
);
|
|
});
|
|
});
|