From de98620601f780bcff8cde1b1e647289d0531831 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Sat, 21 Feb 2026 08:36:21 -0800 Subject: [PATCH 1/6] fix: add rate limiting to auth service endpoints The auth service had no rate limiting, leaving login, password reset, and anonymous sign-in vulnerable to brute-force attacks. Co-authored-by: Cursor --- apps/auth/app.ts | 13 ++++++++++--- apps/auth/package.json | 5 +++-- package-lock.json | 3 ++- tests/unit/auth/rate-limiting.test.ts | 25 +++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 tests/unit/auth/rate-limiting.test.ts diff --git a/apps/auth/app.ts b/apps/auth/app.ts index fc1a8a6..fd4562f 100644 --- a/apps/auth/app.ts +++ b/apps/auth/app.ts @@ -6,6 +6,7 @@ import { auth } from '@wxyc/authentication'; import { toNodeHandler } from 'better-auth/node'; import cors from 'cors'; import express from 'express'; +import rateLimit from 'express-rate-limit'; const port = process.env.AUTH_PORT || '8080'; @@ -107,9 +108,15 @@ if (process.env.NODE_ENV !== 'production') { ); } -// Mount the Better Auth handler for all auth routes -// app.use() will handle all methods and paths under /auth -app.use('/auth', toNodeHandler(auth)); +const authRateLimit = rateLimit({ + windowMs: 15 * 60 * 1000, + limit: 10, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: { error: 'Too many requests, please try again later.' }, +}); + +app.use('/auth', authRateLimit, toNodeHandler(auth)); //endpoint for healthchecks app.get('/healthcheck', async (req, res) => { diff --git a/apps/auth/package.json b/apps/auth/package.json index 4841dd7..2ce02df 100644 --- a/apps/auth/package.json +++ b/apps/auth/package.json @@ -16,12 +16,13 @@ "author": "Jackson Meade", "license": "MIT", "dependencies": { - "@wxyc/database": "^1.0.0", "@wxyc/authentication": "^1.0.0", + "@wxyc/database": "^1.0.0", "better-auth": "^1.3.23", "cors": "^2.8.5", "dotenv": "^17.2.1", - "express": "^5.1.0" + "express": "^5.1.0", + "express-rate-limit": "^8.2.1" }, "peerDependencies": { "drizzle-orm": "^0.41.0" diff --git a/package-lock.json b/package-lock.json index 9ef6de8..24ada26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,8 @@ "better-auth": "^1.3.23", "cors": "^2.8.5", "dotenv": "^17.2.1", - "express": "^5.1.0" + "express": "^5.1.0", + "express-rate-limit": "^8.2.1" }, "devDependencies": { "@types/cors": "^2.8.17", diff --git a/tests/unit/auth/rate-limiting.test.ts b/tests/unit/auth/rate-limiting.test.ts new file mode 100644 index 0000000..0658aca --- /dev/null +++ b/tests/unit/auth/rate-limiting.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +describe('Auth service rate limiting', () => { + const authAppSource = readFileSync( + resolve(__dirname, '../../../apps/auth/app.ts'), + 'utf-8' + ); + + it('imports express-rate-limit', () => { + expect(authAppSource).toMatch(/express-rate-limit/); + }); + + it('configures a rate limiter with rateLimit()', () => { + expect(authAppSource).toMatch(/rateLimit\s*\(/); + }); + + it('applies rate limiting before the auth handler', () => { + const rateLimitIndex = authAppSource.search(/rateLimit\s*\(/); + const authHandlerIndex = authAppSource.search(/toNodeHandler\s*\(\s*auth\s*\)/); + expect(rateLimitIndex).toBeGreaterThan(-1); + expect(authHandlerIndex).toBeGreaterThan(-1); + expect(rateLimitIndex).toBeLessThan(authHandlerIndex); + }); +}); From e15d9fd58db6837c0036af0bb85adb13872cb296 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 09:46:41 -0800 Subject: [PATCH 2/6] style: format files with Prettier --- tests/unit/auth/rate-limiting.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/unit/auth/rate-limiting.test.ts b/tests/unit/auth/rate-limiting.test.ts index 0658aca..c68d5c2 100644 --- a/tests/unit/auth/rate-limiting.test.ts +++ b/tests/unit/auth/rate-limiting.test.ts @@ -2,10 +2,7 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; describe('Auth service rate limiting', () => { - const authAppSource = readFileSync( - resolve(__dirname, '../../../apps/auth/app.ts'), - 'utf-8' - ); + const authAppSource = readFileSync(resolve(__dirname, '../../../apps/auth/app.ts'), 'utf-8'); it('imports express-rate-limit', () => { expect(authAppSource).toMatch(/express-rate-limit/); From 4b9630286caba586c902a2fdcaeb884381f8be08 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 14:32:25 -0800 Subject: [PATCH 3/6] test: disable auth rate limiting in test environments The auth rate limiter (10 req/15 min) causes integration test failures because tests like discogs.spec.js call signInAnonymous() repeatedly. Disable the rate limiter when NODE_ENV=test or USE_MOCK_SERVICES=true, matching the pattern used by the backend's rateLimiting middleware. Also set NODE_ENV=test on the CI auth container in docker-compose so the rate limiter is skipped during integration tests. --- apps/auth/app.ts | 24 ++++++++++++++++-------- dev_env/docker-compose.yml | 1 + tests/unit/auth/rate-limiting.test.ts | 15 +++++++++------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/auth/app.ts b/apps/auth/app.ts index fd4562f..cde9420 100644 --- a/apps/auth/app.ts +++ b/apps/auth/app.ts @@ -108,15 +108,23 @@ if (process.env.NODE_ENV !== 'production') { ); } -const authRateLimit = rateLimit({ - windowMs: 15 * 60 * 1000, - limit: 10, - standardHeaders: 'draft-7', - legacyHeaders: false, - message: { error: 'Too many requests, please try again later.' }, -}); +// Disable rate limiting in test environments to avoid flaky integration tests. +// This matches the pattern used by the backend's rateLimiting middleware. +const isTestEnv = process.env.NODE_ENV === 'test' || process.env.USE_MOCK_SERVICES === 'true'; + +if (isTestEnv) { + app.use('/auth', toNodeHandler(auth)); +} else { + const authRateLimit = rateLimit({ + windowMs: 15 * 60 * 1000, + limit: 10, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: { error: 'Too many requests, please try again later.' }, + }); -app.use('/auth', authRateLimit, toNodeHandler(auth)); + app.use('/auth', authRateLimit, toNodeHandler(auth)); +} //endpoint for healthchecks app.get('/healthcheck', async (req, res) => { diff --git a/dev_env/docker-compose.yml b/dev_env/docker-compose.yml index e8136b1..841420c 100644 --- a/dev_env/docker-compose.yml +++ b/dev_env/docker-compose.yml @@ -90,6 +90,7 @@ services: dockerfile: Dockerfile.auth profiles: [ci] environment: + - NODE_ENV=test - DB_HOST=ci-db - DB_PORT=5432 - DB_USERNAME=${DB_USERNAME} diff --git a/tests/unit/auth/rate-limiting.test.ts b/tests/unit/auth/rate-limiting.test.ts index c68d5c2..3e7173b 100644 --- a/tests/unit/auth/rate-limiting.test.ts +++ b/tests/unit/auth/rate-limiting.test.ts @@ -12,11 +12,14 @@ describe('Auth service rate limiting', () => { expect(authAppSource).toMatch(/rateLimit\s*\(/); }); - it('applies rate limiting before the auth handler', () => { - const rateLimitIndex = authAppSource.search(/rateLimit\s*\(/); - const authHandlerIndex = authAppSource.search(/toNodeHandler\s*\(\s*auth\s*\)/); - expect(rateLimitIndex).toBeGreaterThan(-1); - expect(authHandlerIndex).toBeGreaterThan(-1); - expect(rateLimitIndex).toBeLessThan(authHandlerIndex); + it('applies rate limiting to the auth handler in production', () => { + // The rate limiter is applied conditionally: skipped in test env, active otherwise. + // Verify the production branch wires authRateLimit before toNodeHandler(auth). + expect(authAppSource).toMatch(/authRateLimit,\s*toNodeHandler\s*\(\s*auth\s*\)/); + }); + + it('disables rate limiting in test environments', () => { + expect(authAppSource).toMatch(/isTestEnv/); + expect(authAppSource).toMatch(/NODE_ENV.*test|USE_MOCK_SERVICES/); }); }); From b4311de7f08e8a0dfc07a6c108b41d2727ca3a6e Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 16:29:39 -0800 Subject: [PATCH 4/6] fix: include AUTH_BYPASS in rate-limiting test env check AUTH_BYPASS is guaranteed to be set in CI, unlike NODE_ENV which may not propagate reliably through Docker. This ensures the auth service disables rate limiting during integration tests, preventing 429 errors. --- apps/auth/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/auth/app.ts b/apps/auth/app.ts index cde9420..1677f48 100644 --- a/apps/auth/app.ts +++ b/apps/auth/app.ts @@ -110,7 +110,7 @@ if (process.env.NODE_ENV !== 'production') { // Disable rate limiting in test environments to avoid flaky integration tests. // This matches the pattern used by the backend's rateLimiting middleware. -const isTestEnv = process.env.NODE_ENV === 'test' || process.env.USE_MOCK_SERVICES === 'true'; +const isTestEnv = process.env.NODE_ENV === 'test' || process.env.USE_MOCK_SERVICES === 'true' || process.env.AUTH_BYPASS === 'true'; if (isTestEnv) { app.use('/auth', toNodeHandler(auth)); From 1c260c5cb4d4ad7dc1a6270d457b37f7fa7b8295 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 16:41:02 -0800 Subject: [PATCH 5/6] fix: stop inlining NODE_ENV at build time in auth service The tsup env config was replacing process.env.NODE_ENV with its build-time value ("development") in the compiled output. This meant the runtime NODE_ENV=test set by docker-compose had no effect, so the rate limiter was always active in CI integration tests, causing 429 Too Many Requests failures. Removing the env block lets process.env.NODE_ENV be read at runtime, which is what the isTestEnv guard in app.ts expects. --- apps/auth/tsup.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/auth/tsup.config.ts b/apps/auth/tsup.config.ts index b58553e..ea7c6d9 100644 --- a/apps/auth/tsup.config.ts +++ b/apps/auth/tsup.config.ts @@ -9,8 +9,5 @@ export default defineConfig((options) => ({ clean: true, sourcemap: true, external: ['@wxyc/database', 'better-auth', 'drizzle-orm', 'express', 'cors', 'postgres'], - env: { - NODE_ENV: process.env.NODE_ENV || 'development', - }, onSuccess: options.watch ? 'node ./dist/app.js' : undefined, })); From 0de2a256377f5d702bf7dd35478ef353429046fb Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 16:46:57 -0800 Subject: [PATCH 6/6] style: format auth app.ts with Prettier --- apps/auth/app.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/auth/app.ts b/apps/auth/app.ts index 1677f48..61addcd 100644 --- a/apps/auth/app.ts +++ b/apps/auth/app.ts @@ -110,7 +110,8 @@ if (process.env.NODE_ENV !== 'production') { // Disable rate limiting in test environments to avoid flaky integration tests. // This matches the pattern used by the backend's rateLimiting middleware. -const isTestEnv = process.env.NODE_ENV === 'test' || process.env.USE_MOCK_SERVICES === 'true' || process.env.AUTH_BYPASS === 'true'; +const isTestEnv = + process.env.NODE_ENV === 'test' || process.env.USE_MOCK_SERVICES === 'true' || process.env.AUTH_BYPASS === 'true'; if (isTestEnv) { app.use('/auth', toNodeHandler(auth));