Status: open (M1+M3 superseded post-migration) · Severity: MEDIUM · Source: [[Projects/personal-finance-notion/context/audit-2026-05-17-auth|Auth audit 2026-05-17 §MEDIUM]]
2026-05-18 update. Under [[Projects/personal-finance-notion/decisions/adr-2026-05-18-authjs-migration|Auth.js migration]]:
- M1 (drop legacy cookie fallbacks) — superseded; Auth.js uses its own cookie names, the legacy
access_token/refresh_token/session_tokenreads are gone with the oldsrc/lib/auth.ts.- M3 (require both JWT keys in prod) — superseded; Auth.js uses a single
AUTH_SECRET. The dual-key concern is moot.- M2 (role field), M4 (CSRF doc), M5 (logger stringifier) — M2 + M5 done in [[Projects/personal-finance-notion/backlog/done/p0-authjs-phase-d-pre-launch-hardening|Phase D]]; M4 still applies.
Patch in the first week after launch. Group of related hardening items, none individually large enough for its own note.
src/middleware.ts:17-28, src/lib/auth.ts:294-296, 303-305 read bare access_token / refresh_token / session_token as fallbacks alongside __Host- variants. A non-__Host- cookie can be planted by any subdomain; if you ever serve from *.yourdomain.com a subdomain XSS could override the legit cookie.
Fix: after the production deploy stabilises (and you're sure no live session relies on the legacy name), remove the fallback reads. Keep only __Host- names in prod, bare names in dev.
role fieldResolved (2026-05-25): keep role for upcoming admin features; safe enforcement shipped in [[Projects/personal-finance-notion/backlog/done/p0-authjs-phase-d-pre-launch-hardening|Phase D]] (requireRole, no client-writable role, tests).
src/lib/models/userModel.ts:42-46 defines role: "admin" | "user"; Auth.js session carries it. Was unenforced pre–Phase D.
Fix (Phase D): requireRole("admin") helper + guard at least one handler + test; strip/reject role on signup/profile mutations.
src/lib/auth.ts:36-37 silently falls back JWT_REFRESH_SECRET_KEY → JWT_SECRET_KEY. In production this means one leaked key forges both token types.
Fix: add a startup check:
if (
process.env.NODE_ENV === 'production' &&
!process.env.JWT_REFRESH_SECRET_KEY
) {
throw new Error('JWT_REFRESH_SECRET_KEY must be set in production');
}
Update .env.example to mandate distinct values in prod.
src/lib/utils/csrf.ts exists but isn't enforced — defence currently rests on Next.js's built-in server-action origin check + SameSite=strict. Fine for the current surface, but if a public POST API or webhook is added, this needs revisiting.
Fix: either delete src/lib/utils/csrf.ts if it's dead code (so it doesn't mislead), or add a one-line reminder comment on top of any future /api/* POST/PUT/DELETE route.
Flagged but not verified in the audit. If logger.error JSON-stringifies arbitrary error objects, MongooseError chains can have non-serializable properties and throw inside the logger.
Fix: read src/lib/utils/logger.ts; confirm it uses a safe stringifier (e.g. safe-stable-stringify) or wraps in try/catch. Quick win. Done in Phase D.
csrf.ts is enforced or add a public POST without origin checks — fine today on server actions only, risky if webhooks or open APIs appear. M1/M2/M3/M5 are superseded or shipped.