Changelog
What's new in each release of @ackplus/nest-auth.
All packages release together at the same version: the five npm packages
(@ackplus/nest-auth, -client, -react, -react-native, -contracts) plus
nest_auth_flutter on pub.dev. Current stable: 2.18.0.
- Upgrading from v1? Follow the v1 → v2 migration guide.
- Upgrading within v2 (e.g.
2.0.x→2.2.x)? It's additive/non-breaking — see Upgrading within v2. Bump every package to the same version.
2.18.0 — zero database queries per request; a lost refresh reply no longer signs the user out
- Added (backend):
session.refreshTokenReuse.graceSeconds(default0). When a refresh reply is lost (dropped mobile signal, client timeout, proxy cut), the client retries with the refresh token that was just replaced, and reuse detection used to revoke the session. With a grace set, that token is accepted once within the window: the server rotates again and answers with a fresh pair. A second use of the old token, the pair whose reply was lost, or any retry after the window is still reuse. See Refresh-token rotation and reuse. Closes #14. - Added (backend):
NestAuthEvents.REFRESH_TOKEN_GRACE_USED, payload{ sessionId, userId }, emitted when the grace accepts a retry.
Upgrade note: no action needed; the default keeps today's behaviour. The previous token is stored in the existing
refreshTokencolumn, so there's no migration and custom session stores need no change. Downgrading below 2.18.0 after sessions have rotated with the grace on signs those sessions out once.
- Added (backend, performance):
session.userStateCache. The guard's per-request user read (isActive,mustChangePassword,emailVerifiedAt) is now cached, so with the Redis session store a warm authenticated request costs no database query at all — measured, down from one. Defaults to on for Redis and off for the database/memory stores, because only a shared cache can carry an invalidation to every instance. See user-state cache. - Added (backend):
NestAuthCacheService, exported from the package root.invalidateUser(userId)/invalidateUsers(ids)drop an account's cached state — required after any write the ORM cannot see (repository.update()and other bulkUPDATEs, raw SQL, migrations, another app on the same database). Writes through TypeORM, including a bareuser.save(), are invalidated automatically by aNestAuthUserentity subscriber. - Added (backend):
NestAuthEvents.USER_STATE_INVALIDATED, payload{ userId }, emitted on every invalidation. - Fixed (backend): a Redis connection the library opened is now closed when the Nest application shuts down. It used to be left open, leaking a socket per app instance that ioredis kept reconnecting — visible as
ECONNRESETnoise on teardown and as connections piling up in processes that boot more than one app. A client you supply viasession.redis.clientis still left alone.
Upgrade note: no action for most apps. If you change
isActive,mustChangePasswordoremailVerifiedAtwith raw SQL, a bulkUPDATEor a migration while using the Redis store, add aNestAuthCacheService.invalidateUser(userId)call — otherwise that account keeps its old state for up tottlSeconds(default 300).session.userStateCache: { enabled: false }restores the previous read-every-request behaviour.
2.16.0 — Redis outages fail closed; share your own Redis connection
- Fixed (backend, security): a session-store outage no longer opens optional-auth routes. The guard treated any error during authentication as a failed sign-in, so on a route with optional auth a Redis outage was read as "no session": the request was reset to anonymous and served. A route carrying
@NestAuthRolesor@NestAuthPermissionsalongside optional auth was therefore open for the duration of the outage. Two independentcatchblocks did this and both are fixed. - Changed (backend): a store outage is now refused on every route —
503SESSION_STORE_UNAVAILABLEfor required auth, and for optional auth when a token is presented. A request with no token at all is unaffected.503rather than401is deliberate: a401makes clients sign the user out, so a brief blip would log out every user. The outage is logged once when it starts and once when it clears, not once per request. - Added (backend):
session.redis.clientreuses your existing ioredis connection instead of opening a second one, so TLS, password, sentinel and cluster are configured once. Other connection fields are ignored whenclientis set;keyPrefixandttlSecondsstill apply. - Added (backend):
SessionStoreUnavailableError, exported from the package root, and theSESSION_STORE_UNAVAILABLEerror code in both the server and@ackplus/nest-auth-contracts. - Fixed (backend):
session.redis.clientsurvives config merging.getOptionsdeep-merges caller options, which rebuilds a class instance as a plain object and strips its prototype; an injected client used to arrive with every method gone (this.redis.smembers is not a function).session.storeandcustomAuthProviderswere already protected.
2.15.0 — account-switch fix, console platform-user lock, custom rate-limit buckets
- Fixed (React, security):
AuthProviderno longer shows the previous account after itsclientchanges, which is how the multi-account recipe switches accounts. It now clearssessionDataandsessionand returns tostatus: 'loading'before verifying the new client, and ignores late answers from the old one. If the new account can't be verified (the API is down) it reports'unknown'with no permissions, instead of keeping the old account's. - Added (client types):
IAccountSwitcherdeclares optionalcreatePendingClient?()anddiscardPendingClient?(client), so the add-account flow type-checks throughuseAccountSwitcher().managerwithout a cast.CookieAccountManagerimplements neither. - Added (admin console, security):
adminConsole.allowPlatformUserManagement(defaulttrue). Set it tofalseto keep platform (super-admin) users out of the console: creating one, granting or revoking platform access and setting platform roles answer403 PLATFORM_USER_MANAGEMENT_DISABLED, and existing platform users are read-only there. See Admin console. - Added (backend): app-defined rate-limit buckets. Any key in
security.rateLimit.bucketswithwindowMsandmaxis a bucket of its own, usable as@RateLimit('<name>'). Built-in overrides may be partial ({ max: 10 }keeps the default window). - Changed (backend): a
@RateLimitnaming a bucket that is neither built-in nor configured now fails at boot, even with rate limiting disabled. Such a name used to leave the route unthrottled without an error.
Upgrade note: if an app passed an unknown bucket name to
@RateLimitthrough a cast, declare it insecurity.rateLimit.buckets(or use a built-in name), or the app won't start.
2.14.1 — axios 1.20 typing
- Fixed (client):
attachToAxiosaccepts axios 1.20+ instances again.AxiosLikeInstance.requestwas generic and conflicted with axios 1.20'srequesttyping; it is nowrequest(config): Promise<unknown>. Types only.
2.14.0 — per-account session caps; evictions fire onRevoked
- Added (backend):
session.maxSessionsPerUseralso accepts a resolver,({ userId, data }) => number, asked once per sign-in, so the cap can depend on role, tenant or plan. It may be async. A plain number behaves as before, and an invalid result falls back to10. - Changed (backend): evicting a session to make room now goes through the normal revoke path and fires
session.onRevoked(session, 'evicted').'evicted'is a new member ofSessionRevokeReason.
Upgrade note: if your
onRevokedswitches exhaustively onreason, add a case for'evicted'.
2.13.1 — AI coding skill
- New package:
@ackplus/nest-auth-skill— agent skills for Claude Code, Codex, Cursor and other AI coding agents, installed with one command:npx @ackplus/nest-auth-skill. It detects the project's stack and installs the matching skill:nest-auth(NestJS backend),nest-auth-react(React, Next.js, React Native) and/ornest-auth-flutter(Flutter). It writes only agent instruction files; it does not install or configure authentication. See AI coding skill. - No changes to the library packages.
2.13.0 — API keys: an off switch, and a session for key requests
- Added (backend):
accessKeys: { enabled: false }switches API-key sign-in off — the guard rejects everyApiKeycredential with401 API_KEY_AUTH_DISABLEDandcreateAccessKeyrefuses to mint keys. Defaulttrue. - Changed (backend): a request authenticated by key now carries a session for the key's user, built per request —
RequestContext.currentTenantId()/currentUserId()/currentUserAccess()andrequest.session.datawork as for a signed-in user, so key requests can be tenant-scoped.session.data.authTypeis'api-key'. It is never stored and has noid;logout,switch-tenantand MFA endpoints treat a key request as having no session.GET /auth/menow works with a key (it used to 500). - Added:
createAccessKey(userId, name, { tenantId })pins a key to one of the user's tenants. Unpinned keys act in the user's only active membership (always the case in ISOLATED mode). - Fixed: a deactivated user's key no longer authenticates (
401 ACCOUNT_INACTIVE), and role checks for a key no longer countinactivememberships.
Migration: one nullable column,
nest_auth_access_keys.tenantId(varchar). Apps onsynchronize: falsemust add it; existing keys keep working unpinned. See API Keys.
2.12.0 — live session roles & permissions; filter and rate-limit fixes
- Changed (backend, default on): a session's roles/permissions are no longer frozen at sign-in. The guard re-resolves
session.data.roles/.permissionsfrom the same rows sign-in reads (tenant access row, or the platform marker), on a 30s per-instance cache — so a permission added to a role, a role granted/revoked, or a membership goinginactivenow reaches already-signed-in sessions within the window, and the guard finally agrees withGET /auth/me. A missing/inactiveaccess row resolves to no grants (plain@Auth()routes still authenticate — this revokes grants, not the session). Tune or disable viaauthorization.liveSessionRoles({ ttl }/false). Onlyroles/permissionsare refreshed;customizeSessionDatafields are untouched,guards.afterAuthstill runs after the refresh (existing workaround hooks keep working), and a failed read keeps the sign-in copy.rotateSessionalso re-resolves instead of copying the stale snapshot forward. - Fixed (backend):
AuthExceptionFilterno longer strips structured exception fields —errors,problems, and any other keys a handler attaches now pass through (per-row import errors were the reported case).messagestays a single string and untouched responses are byte-identical. - Fixed (backend): registering
RateLimitGuardtwice can no longer halve a limit.@RateLimit(bucket)already carriesUseGuards(RateLimitGuard); listing the guard again made every request count twice (signupmax 5 allowed 2). The guard is now idempotent per request. See Rate limiting.
Upgrade note: the live refresh adds one lightweight query per session per 30s per instance. Set
authorization.liveSessionRoles: falseto keep the old frozen-at-sign-in behavior.
2.11.1 — grant, revoke & create platform users from the console
- Added (admin API):
PATCH /api/users/:idacceptsisPlatformUser: true | falseto grant or revoke theNestAuthPlatformAccessmarker (grant is idempotent and applied beforeplatformRoleIds, so one request can grant + set roles).POST /api/usersacceptsisPlatformUser: trueto provision a tenant-less platform user directly. Both requireplatformAccess.enabled(400 PLATFORM_ACCESS_DISABLEDotherwise). - Added (admin console UI): "Grant access" / "Revoke" on the user detail page and a "Platform user (super-admin)" option in Create User — shown only when
platformAccess.enabled. - Note: this is a deliberate privilege-escalation surface — with
platformAccess.enabled, any console operator can mint a super-admin. Restrict console access accordingly.
2.11.0 — platform users in the admin console
- Added (admin API):
GET /api/users?scope=all|platform|tenantfilters by access scope; list/detail responses hydrate both scopes and includeplatformAccess+isPlatformUser.PATCH /api/users/:idacceptsplatformRoleIds(roles-only; refused with400 NOT_PLATFORM_USERfor non-platform users). - Added (admin console UI): access-scope filter and "Platform" chip on the users list; a "Platform Access" section with role management on the user detail page.
- Contracts: new
INestAuthPlatformAccess;INestAuthUsergains optionalplatformAccess.
2.10.4 — forRootAsync reads options lazily
- Fixed (backend): phone login was dead under
forRootAsync—POST /auth/loginwithproviderName: 'phone'returnedINVALID_PROVIDER/PROVIDER_NOT_FOUNDdespitephoneAuth: { enabled: true }.AuthProviderRegistryServicelives inCoreModule(an import ofNestAuthModule) and doesn't depend on the async options provider, so Nest constructed it before the async factory ransetOptions(). It read the package defaults (phoneAuth.enabled: false) in its constructor and never registered the provider. Same class as the 2.8.0JwtServicebug. - Fixed: the registry now reads options through a lazy getter and registers built-in providers in
onModuleInit(after every provider, including the async factory, is instantiated).BaseAuthProvider.enabledis a live getter; assigningenabledstill wins, so custom providers are unaffected. Thegoogle/facebook/github/apple/jwtproviders also captured their config objects at construction — silently disabling them underforRootAsync— and now read live.AuthService.authConfigis a live getter too, which is what blocked phone signup. - Fixed:
forRootAsynccould fail to boot entirely.forRootsetsglobalfrom the merged options (defaultisGlobal: true), butforRootAsyncreadisGlobaloff the async wrapper (useClass/useFactory/inject), normallyundefined— so the module came up non-global andTenantServicecouldn't resolveDebugLoggerService. Apps that passedisGlobal: true(as the docs example does) were unaffected. See Module reference.
No API changes, and forRoot behaviour is unchanged.
2.10.3 — user-access membership moves to status
- Changed (backend):
NestAuthUserAccessmembership is gated bystatus(active|inactive, via the newNestAuthUserAccessStatusEnum) instead ofisActive. Thestatuscolumn stays a string; theisActivecolumn andINestAuthUserAccess.isActiveare removed. Login, session resolution,RequestContext.currentUserAccess(), tenant listing, and admin tenant-sync all filter and setstatus. - Changed (admin API): the admin user-detail response no longer carries
isActiveon each access entry — readstatus. - Fixed:
UserService.getTenantsByUserIdentityawaited the query builder and then rangetMany()twice, discarding the first result set.ensureUserAccessnow reactivates an existinginactivemembership instead of returning it unchanged.
⚠️ Backfill before you drop
isActive, or deactivated members silently regain access. No previous version ever wrotestatus, so every existing row carries the column default'active'— including memberships an admin deactivated (which only setisActive = false). Moving the gate tostatuswould re-activate all of them.
Running
synchronize: true? Take the backfill before booting the upgraded app — synchronize dropsisActiveon start, and with it the only record of who was deactivated.
2.10.2 — mustChangePassword on the multi-account sign-in
- Fixed (client): a forced-password-change prompt was dead in any app with
allowMultipleAccounts: true. The backend returnsmustChangePassword: trueon the login response and the single-accountlogin()exposed it, butAccountManager.addAccount()resolved to anAccountSnapshotwith no such field and discarded the login response — so the sign-in call could never tell you the member was on an admin-issued temporary password. - Added:
AccountSnapshot.mustChangePassword, set on the snapshot returned byaddAccount()/commitAccount()(and the ReactaddAccount/completeMfa), in bothAccountManagerandCookieAccountManager. Header mode reads it from the/auth/melookupcommitAccountalready performs, so there's no extra round-trip, and the MFA-commit path is covered too. - By design, one-shot: it rides the returned snapshot only — never
listAccounts(), never persisted. A cachedtruewould outlive the password change and trap the user on the change-password screen. Re-check later withgetSessionUserData()(GET /auth/me);undefinedmeans "not observed here", not "false", and themustChangePassword.enforceguard is still the real enforcement. See Force password change and multi-account switching.
2.10.1 — fix: fresh visitor stuck on load
- Fixed:
POST /auth/refresh-tokenwith no token now returns 401 (was 400). Since the SDK treats only 401/403 as a definitive logout (2.9.0), the old 400 made a fresh visitor / cleared-storage boot look indeterminate, so the app hung on load instead of showing login. The client SDK also short-circuitsrefresh()in header mode when there is no stored token (no doomed request, robust against older backends). Authenticated flows unchanged.
2.10.0 — recovery codes as a backup authenticator
- New
POST /auth/mfa/verify-recovery-code— redeem a single-use recovery code to complete the sign-in (likemfa/verify), returning a full session. Unlikereset-totp, MFA stays enabled and your factors are kept — the code acts as a backup authenticator (GitHub/Google model). The recovery-verified session can enrol a fresh authenticator viasetup-totpinline, so recovering a lost device is one flow, not two sign-ins. EmitsMFA_RECOVERY_CODE_USED. The client SDKs exposeverifyRecoveryCode({ code, trustDevice? })(ReactuseNestAuth().verifyRecoveryCode, FlutterNestAuthClient.verifyRecoveryCode). - Multiple recovery codes.
generate-recovery-codenow issues a set (default 10,mfa.recoveryCodeCount), one hashed single-use row each in a newnest_auth_mfa_recovery_codestable. Response is{ codes: string[], code }(code=codes[0], back-compat). The legacy single-column code is still honoured. - New opt-in
mfa.requireVerifiedContactForEnrollment(defaultfalse): only allow enrolling a new authenticator when the user has a verified email/phone. - Additive — no existing endpoint or response shape changed. See MFA → Recovery codes.
Migration: adds one table,
nest_auth_mfa_recovery_codes. Apps onsynchronize: falsemust add it in a migration (iduuid pk,userIduuid,codeHashvarchar,usedAttimestamp null,createdAttimestamp). Nothing else changed.
2.9.2 — MFA config fix (backend)
- Fixed:
mfa.methodsnow replaces the default[EMAIL, TOTP]instead of being concatenated with it. The config was deep-merged (arrays concatenate), somfa.methods: ['totp']still got EMAIL merged back in and a TOTP-only / email-only setup was impossible. A provided list now wins; the default applies only whenmethodsis omitted. Backend-only, no API/SDK changes.
2.9.1 — MFA hardening (backend)
Backend-only patch (the JS/TS and Flutter SDKs are unchanged — no API shapes changed).
- Security (MFA): closed a password-only MFA bypass. The challenge-stage token login issues before the second factor could reach the routes that change MFA config (every MFA route was
@SkipMfa()), so a password-only attacker could enrol their own authenticator and satisfy the challenge with it. Nowsetup-totp,verify-totp-setup,generate-recovery-code,toggle, and device deletion require a fully MFA-verified session (challenge token →401); the routes a locked-out user needs (status,challenge,verify,reset-totp) still work. First-time enrolment is unaffected. - Fixed (MFA):
reset-totpcould permanently lock out a TOTP-only user. It deleted the TOTP secrets and spent the recovery code but leftisMfaEnabled: true→ the next login returnedisRequiresMfawith an empty method list. It now turns MFA off when no verified method remains (a surviving email/SMS method keeps it on). - Fixed (MFA):
defaultMfaMethodin the login response now falls back to a method the user actually has, instead of returning the app-wide default even when the user isn't enrolled in it.
Migration: only affects a custom UI that called
setup-totp/generate-recovery-codewith the pending challenge token mid-login — completemfa/verifyfirst. Standard flows are unaffected. See MFA → the challenge flow.
2.9.0 — SDKs stop logging users out on network/server blips
The bug. The client SDKs destroyed the session on any failed refresh/verify, not just a real rejection. refresh() cleared tokens and emitted a logout on every non-2xx — including a network failure (the synthesised status 0), a timeout, 429, and all 5xx. verifySession() returned { valid: false } for those same failures, so "we couldn't reach the server" looked identical to "the session is invalid", and the React AuthProvider then fired onUnauthenticated() (redirect to login) during outages. A brief connectivity blip logged people out.
The fix — one rule, everywhere. A session may only be ended by a definitive rejection: the server answered refresh/verify with 401 (or 403). Everything else is indeterminate — tokens are preserved, no logout fires, and a retryable error is thrown.
-
Fixed (client):
refresh()clears state only on 401/403 (viaclearAuthState(), skipping the pointless/doomed/auth/logoutround trip); on anything else it throws and touches nothing. -
Fixed (client):
verifySession()throws on an indeterminate failure and returns{ valid: false }only on 401/403. An expired access token with a live refresh token still verifies. -
Added (client): every auth error carries
error.kind: 'rejected' | 'indeterminate'(+error.statusCode), plus exportedclassifyAuthFailure/AuthFailureKind, so you classify without re-deriving from status codes. Default messages for network/timeout/5xx are now user-friendly. -
Fixed (react):
AuthProviderfiresonUnauthenticated()only on a definitive rejection; an indeterminate failure keeps the user where they are and surfaceserror. NewAuthStatusvalue'unknown'for "couldn't determine". The exported guards (AuthGuard,GuestGuard,RequireRole,RequirePermission) also honour it — during an outage they render the loading fallback instead of redirecting to login or denying access. -
Fixed (next): the SSR helpers apply the rule too —
getServerAuthdistinguishes an indeterminate verify failure (5xx/timeout/network) from a definitive 401/403, andwithAuthreturns a retryable503instead of a401during a backend outage. -
React Native inherits the fix; Flutter already preserved tokens on failure — now pinned with a regression test.
-
Breaking-ish (why a minor bump):
verifySession()now throws on indeterminate failures instead of resolving to{ valid: false }. Catch it and branch onerror.kind. New'unknown'AuthStatusvalue.
Consumers that snapshot-and-restore tokens around refresh (e.g. a
preserveTokensAcrossRefresh()wrapper) can delete that workaround — the SDK now preserves tokens itself.
2.8.5 — social login uuid crash (Postgres)
- Fixed (backend): every social login — Google, Apple, Facebook, GitHub — 500'd on Postgres with
invalid input syntax for type uuid. These providers (and the opt-injwtlogin provider, and custom SSO providers) return the provider's external subject (the OAuthsub/ account id) fromvalidate(), but the post-validate()"known user?" lookup fed that into theuuidauth_identity.userIdcolumn. It now resolves by the external subject (providerId) instead — the same keyhandleSocialLoginalready uses. On SQLite/sqljsthe column type isn't enforced, so the in-memory tests never caught it (the path returned a spuriousINVALID_CREDENTIALSthere instead of crashing). - Added (backend): an exported
SocialAuthProviderbase. Google/Apple/Facebook/GitHub extend it, and it resolves the linked identity byproviderId. If you write a custom social / SSO provider, extendSocialAuthProvider(notBaseAuthProvider) so you inherit the correct lookup and don't hit this crash. See Custom OAuth / SSO provider. - Internal: the login flow now calls a new provider seam
findLinkedIdentity(validated)(default resolves by ouruserId; social/external providers override to resolve byproviderId), sofindIdentityByUserIdkeeps meaning "by our user id". No API surface changed for consumers of the built-in providers — social login just works on Postgres now.
No config or migration changes. If you pin the version, bump every package to
2.8.5together.
2.8.4 — admin console tenant / role-guard visibility
- Fixed (admin UI): the Tenants module, tenant columns/filters, and the role-guard filter options now appear consistently. They're all driven by
GET /auth/client-config; a signal mismatch — the layout keyed offtenants.enabled === truewhile the Users page keyed off the resolved tenant mode — could hide the Tenants nav while other tenant UI still showed.tenantEnabledis now derived from the resolved tenant mode, so every surface uses one signal. - Fixed (admin UI): a failed
/client-configload now logs a clear console warning (with the URL + status) instead of silently rendering an empty admin — the usual cause of "Tenants / guards are missing" is a wrong admin base path or an unconfigured global prefix. - If the Tenants module still doesn't show: make sure your server config has
tenant: { enabled: true, mode: 'isolated' }(setenabled: trueexplicitly), and thatGET /auth/client-configreturns yourtenants+roleGuards(a customclientConfig.factorymust preserve them).
2.8.3 — MFA fixes
- Fixed (MFA): the TOTP QR now shows your configured app name + the user's email instead of
"SecretKey"—mfa.totp.issueris finally applied.setupTotpaccepts an optional label (viaPOST /auth/mfa/setup-totpbody orclient.setupTotp({ label })) to disambiguate multi-tenant / multi-account users, and returnsotpAuthUrl/issuer/account. See MFA → TOTP enrollment. - Fixed (MFA):
GET /auth/mfa/statusnow reportsallowUserToggle/canTogglefrom policy (config), not from whether the user already has MFA on — so a member with MFA off can actually turn it on.
2.8.2was a version-only re-publish (identical code to2.8.1).
2.8.1 — consumer-feedback fixes
A patch release addressing feedback on 2.8.0.
- Fixed (backend):
forRootAsynccan mint tokens again —JwtServiceand friends read module options lazily instead of capturing them at construction (fixesMissing session.jwt.secret, and removes a latent pre-2.8.0 hazard whereforRootAsynccould sign with the insecure default secret). - Fixed (backend): custom auth providers (
customAuthProviders) work with a plainnew MyProvider(opts)andforRoot— the registry injects the repositories, and the config merge preserves the instance's methods. - Fixed (backend):
NestAuthBlockedEmailDomainis exported fromNestAuthEntitiesand the barrel, so migrations can create the blocked-email-domains table. - Added (contracts):
NestAuthErrorCode— a browser-safe enum of every server errorcode(with a drift-guard test), so a frontend matches typed codes instead of bare strings. - Changed (MFA):
verifyMfasurfaces the specific OTP reason (expired / invalid / "request a new code") instead of a generic failure. A wrong MFA code now returns the specificVERIFICATION_CODE_*code rather thanMFA_CODE_INVALID. - Added (barrel): the rate-limit / captcha / lockout decorators and guards are now exported for reuse on your own routes.
- Fixed (client): a cross-tab refresh lock (Web Locks) so two tabs of the same account no longer log each other out on refresh; graceful fallback on React Native / SSR / older browsers.
2.8.0 — security hardening
The P0 security-hardening release: ~24 fixes across the auth core and the embedded admin console. A handful change how existing apps boot or authenticate — read the Breaking items and the 2.7.x → 2.8.0 upgrade steps before you bump. Everything under Added (opt-in) is off by default, so nothing else changes until you enable it.
-
Breaking:
session.jwt.secretis now required. The shipped default ('secret') is gone — boot throws when the signing key is missing or a known-insecure value. A short (<32-char) secret warns; setsession.jwt.validateSecretStrength: trueto make that a hard error too. -
Breaking: the
jwtlogin provider is now opt-in. It used to register automatically wheneversession.jwtexisted, and it trusts any token signed with your secret and mints a session for itssub— a privileged bypass that must be enabled deliberately withsession.jwt.enableLoginProvider: true. As defense-in-depth the auth guard now only acceptstype: 'access'tokens, so a refresh token presented as a Bearer is rejected. -
Breaking (admin console): a
secretKey/sessionSecretunder 32 chars (or a known-weak value) now throws at boot instead of warning. Disable the console if you can't supply a strong key. -
Breaking (admin console): when no dedicated
sessionSecretis set, the admin session-signing key is now derived fromsecretKeyrather than being the rawsecretKey. Admin sessions minted by older versions are invalidated — admins re-login once after upgrade. -
Breaking (admin console): admin login is throttled by default (
429after ~5/min). Opt out withadminConsole.bruteForce.enabled: false. -
Breaking (admin console): the admin session cookie is now
SecureunlessNODE_ENVis explicitlydevelopmentortest(was:Secureonly whenNODE_ENV === 'production', so staging / unset / misconfigured prod shipped it in cleartext). Force it off withadminConsole.cookie.secure: false. -
Breaking (admin console): admin DTO validation and an 8-char admin-password floor are now enforced regardless of whether your app registers a global
ValidationPipe(those DTO rules were previously inert without one). -
Breaking (social): account-linking now requires a verified provider email. A social identity whose email matches an existing local account no longer silently attaches unless the provider verified that email — the user must sign in with their existing method and link deliberately. Restore the old behavior per-provider with
social.requireVerifiedEmailForLinking: false. -
Added (opt-in): built-in CSRF for cookie-authenticated, state-changing requests via
security.csrf.enabled— a double-submit token (non-httpOnly cookie the SPA echoes back in a header) plus an optionalallowedOriginscheck. No-op for bearer/header auth; required if you setsameSite: 'none'. -
Added (opt-in): rate limiting for sensitive endpoints (login, signup, forgot-password, passwordless/OTP send + verify, MFA verify) via
security.rateLimit.enabled. Supply a sharedstorefor multi-instance deployments. -
Added (opt-in): password strength policy + HIBP breach check (
password.policy), enforced uniformly at every password-set path (signup, change, reset, admin-set) and on admin-console passwords. Turn on the breach check withpassword.policy.checkBreached: true(Have I Been Pwned k-anonymity, fail-open). -
Added (opt-in): email-verification gating —
registration.requireVerifiedEmail: truehard-blocks a signed-in-but-unverified user from protected routes. -
Added (opt-in): soft account lockout (
security.lockout.enabled) keyed by identifier + IP so an attacker can't lock a victim's logins from another IP, plus a provider-agnostic CAPTCHA hook (security.captcha.verify) for abuse-prone routes. -
Added (opt-in): disposable / throwaway email-domain screening at sign-up (
emailAuth.disposable) — a DB-backed blocklist seedable from a built-in ~8k default list and managed from a new Blocked Emails console page.blockmode rejects with403 EMAIL_DOMAIN_NOT_ALLOWED;flagmode allows the sign-up but emits an event. -
Hardened (admin console): signup is now bootstrap-only — once the first admin exists, the secret-key
POST <admin>/signupis refused and further admins must be created by a signed-in admin from the dashboard, so a leakedsecretKeycan't mint unlimited super-admins. Restore the legacy shared-key path withadminConsole.allowPublicSignupAfterFirstAdmin: true. -
Hardened (admin console): revocable admin sessions — a dashboard password change now bumps
tokenVersionand revokes that admin's outstanding session cookies (previously only the secret-key reset flow did). -
Hardened (admin console): anti-framing + transport headers on every admin route —
X-Frame-Options: DENY, a realContent-Security-Policywithframe-ancestors 'none'(a<meta>CSP can't set that, so a logged-in admin was frameable),X-Content-Type-Options: nosniff, andReferrer-Policy: no-referrer— and the injectedwindow.__NEST_AUTH_CONFIG__JSON is escaped so no config value can break out of the inline<script>. -
Hardened (admin console): the secret-key-gated signup / reset-password endpoints are throttled and de-oracled — the bootstrap-closed check now runs before the key comparison, so after bootstrap a wrong vs. correct key are byte-identical (the key-grinding oracle is gone). Admin login also runs a dummy Argon2 verify on the not-found path, killing the email-enumeration timing side-channel.
-
Hardened (admin console): a last-admin delete guard (
409 ADMIN_LAST_REMAINING) so you can't lock everyone out,ArrayMaxSize(1000)+ per-item caps on bulk blocked-domain add, and the SPA now echoes the CSRF double-submit token so the dashboard keeps working withsecurity.csrfenabled. -
Fixed: refresh-token reuse now revokes the session. A replayed / rotated-out refresh token invalidates the whole session instead of rejecting just the one request — containing token theft rather than letting the attacker retry.
-
Fixed: TOTP device deletion is scoped to its owner — you can no longer delete another user's TOTP device. OTP verification attempts are capped and recovery/OTP codes now use a CSPRNG.
-
Fixed (social): social login persists
firstName/lastName/avatarUrlfrom the provider (fixes Apple's display name arriving only on the first authorization and never being saved).
2.7.6 — email/phone-first tenant picker helpers
- Added (backend):
UserService.getTenantsByEmail(email)/getTenantsByPhone(phone)— cross-tenant helpers for app-owned email/phone-first login pickers (especially ISOLATED). No public nest-auth HTTP route; call from your own controller. See Logging in under a tenant (ISOLATED).
2.7.5 — richer /auth/client-config for login/signup UIs
- Added (backend):
GET /auth/client-confignow returns passwordless flags, OAuth public client/app ids (google/facebook/apple/github),customProviders,platformAccess.enabled, andaccessTokenType— so login/signup screens can render without hardcoding. Secrets (clientSecret,appSecret, private keys, JWT secrets) are never included; public OAuth client/app ids are safe to expose. - Added (contracts):
IClientConfig(+ related public config types) is the shared response shape;@ackplus/nest-auth-clientre-exports it. - Docs: client + hooks-reference pages updated for the expanded config.
2.7.4 — no silent anonymous requests when no account is active
- Fixed (client): with no active account,
AccountManager.getAuthHeaders()/getAuthHeadersSync()/shouldSendCookies()/refresh()resolved to nothing silently, so every request through an attached axios/fetch went out anonymous and 401'd — while anAuthProviderfed a separate bootstrap client still reported the user signed in. Two token sources disagreeing with no signal. - Added:
fallbackClientonAccountManagerConfig— the client to use when no account is active (typically your bootstrap client) — plus a publicresolveActiveClient()(also onIAccountSwitcher) so your auth provider and your attached HTTP client resolve the same client and can't diverge. - Added:
onNoActiveAccount({ method })— fires when auth resolves to nothing, so you can log or redirect instead of silently 401ing. Defaults are unchanged (still non-fatal empty headers) when neither option is configured. See HTTP adapters and multi-account switching. - Behavior: resolution is boot-safe — the async resolvers await the account index before answering and the sync ones return the neutral default until it has loaded, so a persisted active account is never briefly impersonated by the
fallbackClient.
2.7.3 — duplication-safe React contexts
- Fixed (React): when
@ackplus/nest-auth-reactends up installed twice (common in pnpm/monorepos when a peer-React version split double-installs it), each copy calledcreateContext()and got its own context object — so<AuthProvider>from one copy populated one context while hooks imported from the other read a different, still-default one.isLoadingstayedtrueforever andAuthGuard,RequirePermission/RequireRole, and thewithRequirePermission/withRequireRoleHOCs silently rendered a blank page for authenticated users. - Fixed:
AuthContextandAccountSwitcherContextare now cross-realm singletons pinned onglobalThisviaSymbol.for(...), so every duplicate copy shares one context object. (Safe because React itself stays a single instance viapeerDependency— only the context identity broke.) - Added: guards no longer fail completely silently while loading — when a guard renders nothing purely because auth is still loading and no loading UI was supplied, a one-time dev-only
console.warnpoints at a duplicate install as the likely cause. Production builds stay silent. - No API changes — a drop-in fix. The real remedy is still to dedupe
@ackplus/nest-auth-reactto a single copy; this makes the app work even when you can't.
2.7.2 — multi-account storage GC + reset()
- Fixed (client):
AccountManager(header mode) no longer leaks orphaned per-account token namespaces in storage (<prefix>a_<uuid>_access_token/_refresh_token/_session). An interrupted add-account, an abandoned MFA/OTP pending client, or an index/storage desync previously stranded namespaces forever, becauseremoveAccountcan only target namespaces still in the index. Onready()the manager now reaps any namespace the index no longer references (opt out withreapOrphanStorageOnReady: false; a corrupt index never triggers reaping). - Added:
AccountManager.reset()(also onCookieAccountManager, theIAccountSwitcherinterface, and the ReactuseAccountSwitcher()) — remove every account, revoke each session, and wipe all per-account storage. Use it for a "plain sign-in starts a fresh single-account session" rule. - Added:
AccountManager.discardPendingClient(client)— clear an abandonedcreatePendingClient()/AccountMfaRequiredErrorpending client so its tokens don't linger. - Added: optional
StorageAdapter.keys()(implemented by the local/session/memory adapters) that powers the GC. See multi-account switching.
2.7.1 — shared-axios refresh-deadlock fix
- Fixed (client): sharing one axios for both
AuthClient(createAxiosAdapter) andattachToAxiosno longer deadlocks on an expired session. The bootverifySession401 started a refresh whose refresh-token request re-entered the same interceptor and parked forever — the app hung on the splash screen and never cleared tokens or redirected to login.createAxiosAdapternow tagsAuthClient's own requests andattachToAxiosskips them; the newNEST_AUTH_ADAPTER_REQUESTexport lets custom adapters opt out too. - Behavior:
attachToAxios/attachToFetchnow default-skip the auth endpoints (/auth/refresh-token,/auth/login,/auth/logout,/auth/logout-all) — they are never bearer-injected or refresh-retried. Do login/logout via theAuthClient/AccountManagermethods; if you renamed those endpoints, pass the custom paths inskipPaths. See HTTP adapters.
2.7.0 — platform-user listing + passwordless login completion
- Added (backend):
UserService.getPlatformUsers(options?),getPlatformUsersAndCount(options?), andgetPlatformUsersByRole(roleName, guard?)— list super-admins by thePlatformAccessmarker without scanning tenant users (the list analog ofgetPlatformUserByEmail). Callerwhere/relations/ pagination are honored. - Added (client + React):
AuthClient.passwordlessLogin(dto)anduseNestAuth().passwordlessLogin(dto)complete a passwordless sign-in — exchange the emailed/texted code for a session (the completion step forpasswordlessSend), returning a normal auth response. NewIPasswordlessLoginRequest({ identifier, code, channel?, tenantId?, rememberMe? });channeldefaults to trying both email and SMS. WrapsPOST /auth/login— no backend change.
2.6.0 — multi-account switcher DX
- Added:
attachToAxios/attachToFetchaccept anAccountManager/AuthHeaderProvider, and the managers expose instanceattachToAxios()/attachToFetch()— a shared axios/fetch follows the active account with no re-attach on switch. - Added: account naming —
AccountSnapshot.tenantName,AccountMeta,addAccount(dto, { meta }), andsetAccountMeta(accountId, meta)— so a same-email owner's accounts are distinguishable in the switcher. - Added (React):
useAccountSwitcher().completeMfa(error, verifyDto)to finish an MFA-gatedaddAccount;commitAccountis now onIAccountSwitcher, and cookie mode surfacesAccountMfaRequiredErrortoo. - Added (React):
GuestGuard.allowWhenAddingAccount+ a new<AddAccountGuard>for a Gmail-style "add another account" flow (render the login form while already signed in). See multi-account switching.
2.5.2 — tenant-less platform-user provisioning
- Fixed (ISOLATED): provisioning a platform (super-admin) user no longer throws
TENANT_ID_REQUIREDunderTENANT_MODE=isolated.UserService.createUser/getUserByEmailrequire atenantIdthere, which broke admin/platform-admin bootstrap on boot — platform admins are tenant-less. - Added: first-class
UserService.createPlatformUser(data)andgetPlatformUserByEmail(email)— tenant-less provisioning + lookup that work in every tenant mode. A platform user is identified by thePlatformAccessmarker (the same row the login path enforces), so the lookup never returns a regular tenant account, andcreatePlatformUserestablishes that marker atomically. See Platform admin portal.
Note: entries for 2.3.0 – 2.5.1 are not yet backfilled on this page; see each package's
CHANGELOG.md/ the GitHub releases for the interim versions.
2.2.0 — ISOLATED fixes, tenant lookup, cleanup
- Fixed (ISOLATED tenant scoping):
forgot-password,verify-forgot-password-otp, and phone login now scope the account lookup by the resolvedtenantId. Previously, when the same email existed in multiple tenants, a reset/login could resolve the wrong account. Reset/verify tokens round-trip the tenant, so links land in the correct isolated tenant. - Added:
GET /auth/tenants/lookup?slug=(public) — resolve a tenant slug → id so an ISOLATED login form can supply the righttenantId. Exact-slug only (no enumeration). See Logging in under a tenant (ISOLATED). - Fixed: the request-context middleware wildcard now uses the named form (
{*splat}) on Express 5 / path-to-regexp v8, silencing theLegacyRouteConverterwarning; Express 4 is unaffected. - Removed: dangling
InitializeAdminrequest/response DTOs and theIInitializeAdminRequest/IInitializeAdminResponsecontracts (no route consumed them). - Docs: corrected the multi-tenancy page — ISOLATED is logical identity isolation in one database (same email = a separate account per tenant;
switchTenantdisabled; login needs atenantId). The library does not switch data sources per tenant. New ISOLATED login recipe.
2.1.1 — client-config hooks
- Added:
AuthClient.getClientConfig()(+ theIClientConfigtype) — fetch the backend's public config with no auth. - Added (React):
useClientConfig()anduseMultiAccountEnabled()— gate UI (e.g. the account switcher) on what the backend actually enables. - Docs: multi-account integrated into the config / client / React reference pages.
2.1.0 — Multi-account login & switching
Log into several accounts on one client and switch the active one (Gmail/Slack-style). Especially natural in ISOLATED mode, where the same email is a distinct account per tenant.
- Backend: opt-in
session.allowMultipleAccounts(defaultfalse), surfaced onGET /auth/client-config. Cookie mode gains per-account cookies + a non-httpOnly active-account selector and aGET /auth/accountslisting endpoint. The backend was already multi-session; switching is client-side. - Client SDK:
AccountManager(header mode — one client per account, namespaced storage) andCookieAccountManager(cookie mode), behind a sharedIAccountSwitcherinterface. - React SDK:
AccountSwitcherProvider(separate fromAuthProvider) +useAccountSwitcher/useAccounts/useActiveAccount. - Recipe: Multi-account login & switching.
2.0.4 — @Public() works under a global guard
- Fixed:
NestAuthAuthGuardnow honours@Public()(IS_PUBLIC_KEY) — previously a silent no-op. The documented globalAPP_GUARD+@Public()pattern works; the library's own public routes (/auth/login,/auth/signup, refresh, password reset, SSO callback,client-config) and the admin console are pre-marked, so a global guard no longer 401s login. See Guards.
2.0.3 — Postgres portability & optional peers
- Fixed:
nest_auth_trusted_devices.revokedAtuseddatetime, which Postgres rejects (the app couldn't boot). It now uses an inferred, portable type (boots on Postgres, MySQL, SQLite). - Fixed: the optional
apple-authpeer is now lazy-loaded — apps that don't install it (or use native Apple sign-in) boot fine. (google-auth-library/fbwere already lazy.)
2.0.2 — public-barrel exports
- Fixed:
Public/IS_PUBLIC_KEYandAuthExceptionFilterare now exported from the package barrel (they were defined but unreachable), plus a new@CurrentUser()decorator and a re-exportedCurrentAdmin. - Added: a package
exportsmap; corrected npm description/keywords.
2.0.1 — first stable v2
The first stable release of v2 (the 2.0.0-beta.* line preceded it). See the overview below and the v1 → v2 migration guide.
2.0.0 — What's new (v2 overview)
v2 is a major release: the same NestAuthModule.forRoot() wiring and flat config, but a substantially hardened core, several new capabilities, and complete docs. See the migration guide for breaking changes and how to upgrade.
New capabilities
- Passwordless login — email/SMS OTP via
passwordless: { enabled, allowSignUp }, with client/ReactpasswordlessSendhelpers and aPOST /auth/passwordless/sendendpoint. - Phone verification —
POST /auth/send-phone-verificationandPOST /auth/verify-phone, backed by a shared OTP flow service. - Platform admin — a first-class, cross-tenant super-admin (
platformAccess: { enabled, validate }). See Platform admin portal. - Embedded admin dashboard — a full management UI served at
/auth/admin(enable withadminConsole), backed by a documented REST API. No separate install. GET /auth/me— a guarded current-user endpoint.
Reliability
- Atomic user mutations — signup, admin/programmatic create, update, and delete each run in a single transaction. A failing hook, listener, or multi-step write rolls back completely: no partially-created or half-updated users. Lifecycle events fire only after commit.
- Full lifecycle hooks — added
user.beforeUpdate/afterUpdate/beforeDelete/afterDelete, and the transactionalEntityManageris now passed to create/update/delete andonSignup/onLoginhooks so your sync code commits atomically with the user. See the hooks reference. - RBAC events —
RoleServiceandPermissionServicenow emitROLE_*/PERMISSION_*created/updated/deleted events so role and permission changes are syncable. See the events reference.
Security hardening
- Secrets hashed at rest — API-key secrets, MFA recovery codes, OTP codes, and trusted-device tokens are now stored hashed (and verified with constant-time comparisons). Trusted devices also support explicit revocation. Existing API keys must be regenerated — see the migration guide.
- Refresh-token rotation + reuse detection — each refresh issues a new token and rejects a replayed/old one.
- Configurable password hashing — bring-your-own
password.hash/password.verify, or tune the built-in Argon2 (password.argon2). - OAuth hardening — Google
requireVerifiedEmail/ multi-audience native id-tokens, Apple native identityToken verification, and GitHub Enterprise endpoint overrides.
Developer experience
- Complete API reference — a generated OpenAPI 3.0 spec rendered in the admin console and the docs site.
- Real-database tests — the suite runs against a real database with no mocks.
- Lighter & modern — dropped the
momentdependency; requires Node ≥ 20 and pnpm ≥ 10.
Breaking changes
A focused set: token-TTL config renamed, a few hook/SDK method renames, and the API-key rehash. All of them — with before/after code — are in the v1 → v2 migration guide.