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.10.4.
- 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.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.