From 326ffc4c4ef7c9b236388304c1fa2ced4a3af1bf Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sun, 19 Apr 2026 20:49:51 +0200 Subject: [PATCH] refactor(config): centralize configuration - Add CODEBAR_AUTH_URL for OAuth callback (not BETTER_AUTH_URL) - Add wildcard support in redirect validation - Add unit tests for redirect validation (15 tests, 93% coverage) - Remove unused host config (not used for server binding) --- .fallowrc.json | 2 +- src/app/utils/redirect.js | 18 ++++--- src/auth.js | 2 +- src/config.js | 3 +- src/index.js | 4 +- test/unit/redirect.test.js | 104 +++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 test/unit/redirect.test.js diff --git a/.fallowrc.json b/.fallowrc.json index 53fa406..0568a68 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", - "entry": ["test/features/*.test.js"], + "entry": ["test/features/*.test.js", "test/unit/*.test.js"], "ignoreDependencies": ["pino"], "ignoreExports": [{ "file": "static/auth-client.js", "exports": ["*"] }] } diff --git a/src/app/utils/redirect.js b/src/app/utils/redirect.js index 5ebc09a..42d4544 100644 --- a/src/app/utils/redirect.js +++ b/src/app/utils/redirect.js @@ -1,10 +1,14 @@ import config from "../../config.js"; -/** - * Checks if a redirect URL is valid (in the allowlist) - * @param {string} redirectUrl - The URL to validate - * @returns {boolean} - True if valid, false otherwise - */ +const matchesGlob = (url, pattern) => { + if (!pattern.includes("*")) return false; + const escaped = pattern + .replace(/[.+?^${}()|[\]\\]/g, "\\$&") + .replace(/\*/g, ".*"); + const regex = new RegExp(`^${escaped}$`); + return regex.test(url); +}; + const isValidRedirectUrl = (redirectUrl) => { if (!redirectUrl || typeof redirectUrl !== "string") { return false; @@ -15,7 +19,9 @@ const isValidRedirectUrl = (redirectUrl) => { return false; } - return config.allowed_redirects.includes(trimmed); + return config.allowed_redirects.some( + (pattern) => trimmed === pattern || matchesGlob(trimmed, pattern), + ); }; /** diff --git a/src/auth.js b/src/auth.js index e96eaa8..9eb62c3 100644 --- a/src/auth.js +++ b/src/auth.js @@ -37,7 +37,7 @@ export { db }; export const auth = betterAuth({ database: db, - baseURL: `http://${appConfig.host}:${appConfig.port}`, + baseURL: appConfig.base_url, logger: { disabled: false, level: "debug", diff --git a/src/config.js b/src/config.js index 4e86dc4..c37568f 100644 --- a/src/config.js +++ b/src/config.js @@ -1,8 +1,7 @@ const config = { port: process.env.PORT || 3000, - host: "localhost", + base_url: process.env.CODEBAR_AUTH_URL || "http://localhost:3000", database_url: process.env.DATABASE_URL || "./auth.db", - // TODO(till): a wildcard pattern would be nice here allowed_redirects: ["http://localhost:3000/demo"], social: { github: { diff --git a/src/index.js b/src/index.js index 9bc8b9f..4c6545f 100644 --- a/src/index.js +++ b/src/index.js @@ -10,8 +10,8 @@ const server = serve( fetch: app.fetch, port: appConfig.port, }, - (info) => { - console.log(`Server is running on http://${appConfig.host}:${info.port}`); + () => { + console.log(`Server is running on ${appConfig.base_url}`); }, ); diff --git a/test/unit/redirect.test.js b/test/unit/redirect.test.js new file mode 100644 index 0000000..ec7306e --- /dev/null +++ b/test/unit/redirect.test.js @@ -0,0 +1,104 @@ +import { test } from "tap"; +import { validateRedirectUrl } from "../../src/app/utils/redirect.js"; +import config from "../../src/config.js"; + +const original = config.allowed_redirects; + +test("redirect validation - exact match", async (t) => { + config.allowed_redirects = ["http://localhost:3000/demo"]; + + t.after(() => { + config.allowed_redirects = original; + }); + + t.equal( + validateRedirectUrl("http://localhost:3000/demo"), + "http://localhost:3000/demo", + ); + t.equal(validateRedirectUrl("http://evil.com/steal"), "/profile"); + t.equal(validateRedirectUrl(""), "/profile"); + t.equal(validateRedirectUrl(null), "/profile"); + t.equal(validateRedirectUrl(" "), "/profile"); +}); + +test("allows subdomain wildcard", async (t) => { + config.allowed_redirects = ["https://*.codebar.io"]; + + t.after(() => { + config.allowed_redirects = original; + }); + + t.equal( + validateRedirectUrl("https://auth.codebar.io"), + "https://auth.codebar.io", + ); + t.equal( + validateRedirectUrl("https://staging.codebar.io"), + "https://staging.codebar.io", + ); + t.equal( + validateRedirectUrl("https://app.codebar.io"), + "https://app.codebar.io", + ); +}); + +test("rejects non-matching domain with wildcard", async (t) => { + config.allowed_redirects = ["https://*.codebar.io"]; + + t.after(() => { + config.allowed_redirects = original; + }); + + t.equal(validateRedirectUrl("https://codebar.io"), "/profile"); + t.equal(validateRedirectUrl("https://evil.com"), "/profile"); +}); + +test("allows path wildcard", async (t) => { + config.allowed_redirects = ["https://codebar.io/*"]; + + t.after(() => { + config.allowed_redirects = original; + }); + + t.equal( + validateRedirectUrl("https://codebar.io/profile"), + "https://codebar.io/profile", + ); + t.equal( + validateRedirectUrl("https://codebar.io/anything/here"), + "https://codebar.io/anything/here", + ); +}); + +test("allows multiple wildcards", async (t) => { + config.allowed_redirects = ["https://*.example.com/*"]; + + t.after(() => { + config.allowed_redirects = original; + }); + + t.equal( + validateRedirectUrl("https://auth.example.com/page"), + "https://auth.example.com/page", + ); + t.equal( + validateRedirectUrl("https://api.example.com/v1/users"), + "https://api.example.com/v1/users", + ); +}); + +test("exact match takes precedence over wildcard", async (t) => { + config.allowed_redirects = [ + "http://localhost:3000/demo", + "http://localhost:3000/*", + ]; + + t.after(() => { + config.allowed_redirects = original; + }); + + t.equal( + validateRedirectUrl("http://localhost:3000/demo"), + "http://localhost:3000/demo", + ); +});