middleware.ts falls back to a hardcoded JWT secret: forged auth-token cookies verify when AUTH_SECRET is unset
Labels / Complexity: bug · security · High Complexity — High
Problem
The auth middleware signs/verifies JWTs with a publicly-known secret whenever AUTH_SECRET is not set. middleware.ts (line 27):
// Ensure AUTH_SECRET is set in .env
const secretKey = process.env.AUTH_SECRET || 'default-fallback-secret-for-dev-only-do-not-use-in-prod';
const secret = new TextEncoder().encode(secretKey);
// Verify signature and expiry with 15s clock tolerance
await jwtVerify(token, secret, {
clockTolerance: 15,
});
The fallback string is in the repository, so anyone can forge an auth-token cookie signed with it. In any deployment where AUTH_SECRET is missing or empty, the middleware's jwtVerify accepts attacker-crafted tokens and the protected routes (/dashboard, /portfolio, /settings, /invest per PROTECTED_ROUTES at the top of the file) are bypassable. Consequences:
Root cause
middleware.ts line 27: the || 'default-fallback-secret-...' fallback. The comment on the line above ("Ensure AUTH_SECRET is set in .env") shows the intent, but the code does not enforce it.
Why this is architecturally hard
- Middleware has no startup hook. The BackEnd fix works because
bootstrap() runs before serving; Next.js middleware runs per-request in the Edge runtime, so the "fail fast at boot" pattern does not apply directly. The contributor must choose: fail closed per-request when the env var is absent (redirect with an explicit server error), or validate the variable at build/startup via next.config.ts/a config module that middleware can import.
- Failing closed changes user experience. If the fix rejects all protected-route requests when
AUTH_SECRET is unset, every environment (local dev included) must set the variable — scripts/validate-env.js exists and may need the variable added to its checks so the failure surfaces early and clearly.
- The verification call site must keep working. The
jwtVerify/clockTolerance logic and the cookie-clear-on-invalid branch should be preserved; the fix is about the key material source, not the verification flow, so the change must be minimal and covered by whatever middleware tests exist (check __tests__/ for middleware coverage before assuming none).
Downstream impact
None for other repos: middleware.ts is frontend-only. PropChain-BackEnd already validates its own secrets, so there is no shared secret contract to change.
Acceptance criteria
- With
AUTH_SECRET unset or empty, protected routes reject requests (redirect) instead of verifying with the fallback secret; with it set, normal verification behaves as today.
- The literal fallback string
'default-fallback-secret-for-dev-only-do-not-use-in-prod' is gone from middleware.ts.
- A test proves a token signed with the old fallback string is rejected when
AUTH_SECRET is set, and that a missing AUTH_SECRET fails closed.
npm run typecheck, npm test, and npm run lint pass.
Out of scope
Rotating the signing algorithm, adding refresh-token handling, and changing PROTECTED_ROUTES are out of scope.
Getting started
middleware.ts — the fallback at line 27 and the verify block below it
scripts/validate-env.js — where the env var should be required so failures surface at startup
__tests__/ — check for existing middleware tests to extend
Commands: npm run typecheck, npm test, npm run lint, npm run validate:env.
Good first files to read: middleware.ts, scripts/validate-env.js.
middleware.ts falls back to a hardcoded JWT secret: forged auth-token cookies verify when AUTH_SECRET is unset
Labels / Complexity: bug · security · High Complexity — High
Problem
The auth middleware signs/verifies JWTs with a publicly-known secret whenever
AUTH_SECRETis not set.middleware.ts(line 27):The fallback string is in the repository, so anyone can forge an
auth-tokencookie signed with it. In any deployment whereAUTH_SECRETis missing or empty, the middleware'sjwtVerifyaccepts attacker-crafted tokens and the protected routes (/dashboard,/portfolio,/settings,/investperPROTECTED_ROUTESat the top of the file) are bypassable. Consequences:AuthServicehad the same class of bug (closed issues [Test] E2E suite for i18n locale switching + RTL hydration safety #663, useApiMutation has no tests: mutation state and error handling are uncovered #892: hardcodedJWT_SECRET/JWT_REFRESH_SECRETfallbacks) and was fixed by rejecting unset secrets at startup (JWT_SECRET must be set and at least 32 charactersinsrc/auth/auth.service.ts). This frontend instance was never fixed. (Frontend issues bug: hardcoded fallback secret in REVALIDATE_WEBHOOK_SECRET #421/security: hardcoded fallback REVALIDATE_WEBHOOK_SECRET in API route #441 fixed the same pattern for a different secret,REVALIDATE_WEBHOOK_SECRET;AUTH_SECRETinmiddleware.tsremains.)Root cause
middleware.tsline 27: the|| 'default-fallback-secret-...'fallback. The comment on the line above ("Ensure AUTH_SECRET is set in .env") shows the intent, but the code does not enforce it.Why this is architecturally hard
bootstrap()runs before serving; Next.js middleware runs per-request in the Edge runtime, so the "fail fast at boot" pattern does not apply directly. The contributor must choose: fail closed per-request when the env var is absent (redirect with an explicit server error), or validate the variable at build/startup vianext.config.ts/a config module that middleware can import.AUTH_SECRETis unset, every environment (local dev included) must set the variable —scripts/validate-env.jsexists and may need the variable added to its checks so the failure surfaces early and clearly.jwtVerify/clockTolerancelogic and the cookie-clear-on-invalid branch should be preserved; the fix is about the key material source, not the verification flow, so the change must be minimal and covered by whatever middleware tests exist (check__tests__/for middleware coverage before assuming none).Downstream impact
None for other repos:
middleware.tsis frontend-only.PropChain-BackEndalready validates its own secrets, so there is no shared secret contract to change.Acceptance criteria
AUTH_SECRETunset or empty, protected routes reject requests (redirect) instead of verifying with the fallback secret; with it set, normal verification behaves as today.'default-fallback-secret-for-dev-only-do-not-use-in-prod'is gone frommiddleware.ts.AUTH_SECRETis set, and that a missingAUTH_SECRETfails closed.npm run typecheck,npm test, andnpm run lintpass.Out of scope
Rotating the signing algorithm, adding refresh-token handling, and changing
PROTECTED_ROUTESare out of scope.Getting started
middleware.ts— the fallback at line 27 and the verify block below itscripts/validate-env.js— where the env var should be required so failures surface at startup__tests__/— check for existing middleware tests to extendCommands:
npm run typecheck,npm test,npm run lint,npm run validate:env.Good first files to read:
middleware.ts,scripts/validate-env.js.