2026-05-18-authjs-phase-a-foundation

Mon May 18 2026 07:00:00 GMT+0700 (Western Indonesia Time)type/changelogtopic/authtopic/migration

2026-05-18 — Auth.js v5 Phase A foundation shipped

Commits. f21f71d Day-1 audit fixes (C4 + H4) · d5b94ce Phase A migration.

What landed

The bespoke rotating-refresh-family JWT stack is gone. Auth.js v5 with the Credentials provider now sits on a JWT-strategy session (sliding 7-day expiry, 24-hour update window). Headline numbers from d5b94ce: 20 files changed, 539 insertions, 1184 deletions — a net reduction of ~640 lines.

New:

  • src/auth.config.ts — edge-safe NextAuthConfig. Session/JWT module augmentation pins session.user to { userId, email, name, role, newUser, currency }.
  • src/auth.ts — Node entrypoint. Credentials authorize() ports the bcrypt + lockout + per-email rate limit logic from the old actions/user.ts:login. Custom CredentialsSignin subclasses surface error codes (credentials, account_locked, rate_limited).
  • src/app/api/auth/[...nextauth]/route.ts — mounts handlers so next-auth/react works.
  • src/middleware.tsNextAuth(authConfig).auth wrapper. Public/protected redirects with callbackUrl, plus the four security headers (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy).

Replaced (kept public shape):

  • src/lib/auth.ts — was ~470 lines of crypto/refresh-family/blacklist; now a 60-line shim. getUserData() calls auth(). UserData type and mappedUserData() preserved.
  • src/components/auth/AuthContext.tsx<SessionProvider> plus a useAuth() compat hook over useSession()/signIn()/signOut(). The ~10 consumer components compile unchanged.
  • src/lib/actions/auth.tssignup() now createUsersignIn("credentials", ...). loginAction/logoutAction stubbed with deprecation.
  • src/lib/actions/user.tsupdateNewUserStatus no longer re-issues a token; client refreshes via update({ newUser }).

Deleted:

  • src/lib/auth-cookies.ts, src/lib/auth-edge.ts
  • src/lib/utils/tokenBlacklist.ts, src/lib/utils/refreshTokenSession.ts
  • src/lib/models/tokenBlacklistModel.ts, src/lib/models/refreshTokenSessionModel.ts
  • src/lib/utils/api-client.ts::fetchCurrentUser
  • JWT_SECRET_KEY / JWT_REFRESH_SECRET_KEY env vars (replaced by AUTH_SECRET)

Audit findings remap

| Item | Status after 2026-05-18 | | ------------------------- | -------------------------------------------- | | C1 Password reset | Superseded → Phase B | | C2 Email verification | Superseded → Phase B | | C3 Signup enumeration | Partial mask in d5b94ce; full fix Phase B | | C4 API error leakage | ✅ Done — f21f71d | | H1 CSP | Phase C | | H2 Login timing | Phase C | | H3 Blacklist race | ✅ Moot — file deleted in d5b94ce | | H4 PWA cache leak | ✅ Done — f21f71d | | M1 Legacy cookie reads | Superseded by Auth.js | | M2 Role field | Phase C decision | | M3 Dual JWT secrets | Superseded by Auth.js (single AUTH_SECRET) | | M4 CSRF posture | Improved by Auth.js defaults | | M5 Logger stringifier | Phase C | | L1 Integration API tokens | Elevated to Phase C | | L2–L7 | Backlog unchanged |

Surprises + delta vs plan

  • Adapter decision sidestepped. The plan called for either dropping the custom userId field or writing a thin mongoose adapter. Phase A is Credentials-only and Credentials doesn't need an adapter — that decision is deferred to Phase B when Google OAuth lands (the OAuth flow needs to persist accounts/verificationTokens collections).
  • emailVerified + image schema additions deferred. Same reason: only needed once OAuth + verification land.
  • Faster than estimated. Plan said ~5d for Phase A; landed same day. Risk: less hardening time than budgeted — Phase B/C still need their full estimates.
  • tests/e2e/auth.spec.ts rewrite not needed. The spec is fully black-box (form fill + URL assertions); it works as-is against Auth.js. global.setup.ts likewise — only requires AUTH_SECRET in the env.
  • Sign-out smoke-test gap. curl POST to /api/auth/signout didn't fully round-trip; the in-browser flow via next-auth/react::signOut() is what the compat layer uses and was verified manually by the user.

Verification

npx tsc --noEmit clean for all touched files (four pre-existing errors in integrations.routes.test.ts:42 and transaction.ts:1612-1614 unchanged). End-to-end curl smoke covering: unauth redirect with callbackUrl, public /login, bad-credentials with code=credentials, good-credentials with authjs.session-token cookie, augmented session JSON, /api/user/current parity through the shim, authed protected access, authed /login back-redirect.

Next

  • Phase B — start. Google OAuth (AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET + Google Cloud Console setup), custom mongoose adapter (decision 2026-05-18 (b)), Resend transport (RESEND_API_KEY + MAIL_FROM), password-reset token model + flow with sessionsValidAfter-based revocation (decision 2026-05-18 (b)), email-verification token model + flow, auto-link on verified-email match in signIn callback (decision 2026-05-18, revised from earlier "block"), full enumeration-safe signup.
  • Cleanup leftovers (low priority). src/app/api/user/current/route.ts is preserved for the tests/e2e/api-unauthorized.spec.ts 401 contract — could be removed if we update that test.

As-built reference

  • [[Projects/personal-finance-notion/context/auth-as-built-2026-05-19]] — cross-file picture of the entire migrated stack after Phase A + B + C, including the Phase A pieces that landed in this commit.