Nest Authbeta

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.


2.10.4 — forRootAsync reads options lazily

  • Fixed (backend): phone login was dead under forRootAsyncPOST /auth/login with providerName: 'phone' returned INVALID_PROVIDER / PROVIDER_NOT_FOUND despite phoneAuth: { enabled: true }. AuthProviderRegistryService lives in CoreModule (an import of NestAuthModule) and doesn't depend on the async options provider, so Nest constructed it before the async factory ran setOptions(). It read the package defaults (phoneAuth.enabled: false) in its constructor and never registered the provider. Same class as the 2.8.0 JwtService bug.
  • 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.enabled is a live getter; assigning enabled still wins, so custom providers are unaffected. The google / facebook / github / apple / jwt providers also captured their config objects at construction — silently disabling them under forRootAsync — and now read live. AuthService.authConfig is a live getter too, which is what blocked phone signup.
  • Fixed: forRootAsync could fail to boot entirely. forRoot sets global from the merged options (default isGlobal: true), but forRootAsync read isGlobal off the async wrapper (useClass / useFactory / inject), normally undefined — so the module came up non-global and TenantService couldn't resolve DebugLoggerService. Apps that passed isGlobal: 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): NestAuthUserAccess membership is gated by status (active | inactive, via the new NestAuthUserAccessStatusEnum) instead of isActive. The status column stays a string; the isActive column and INestAuthUserAccess.isActive are removed. Login, session resolution, RequestContext.currentUserAccess(), tenant listing, and admin tenant-sync all filter and set status.
  • Changed (admin API): the admin user-detail response no longer carries isActive on each access entry — read status.
  • Fixed: UserService.getTenantsByUserIdentity awaited the query builder and then ran getMany() twice, discarding the first result set. ensureUserAccess now reactivates an existing inactive membership instead of returning it unchanged.

⚠️ Backfill before you drop isActive, or deactivated members silently regain access. No previous version ever wrote status, so every existing row carries the column default 'active'including memberships an admin deactivated (which only set isActive = false). Moving the gate to status would re-activate all of them.

-- 1. backfill FIRST
UPDATE nest_auth_user_accesses SET status = 'inactive' WHERE "isActive" = false;
-- 2. then drop the old column (or let synchronize do it)
ALTER TABLE nest_auth_user_accesses DROP COLUMN "isActive";

Running synchronize: true? Take the backfill before booting the upgraded app — synchronize drops isActive on 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 returns mustChangePassword: true on the login response and the single-account login() exposed it, but AccountManager.addAccount() resolved to an AccountSnapshot with 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 by addAccount() / commitAccount() (and the React addAccount / completeMfa), in both AccountManager and CookieAccountManager. Header mode reads it from the /auth/me lookup commitAccount already 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 cached true would outlive the password change and trap the user on the change-password screen. Re-check later with getSessionUserData() (GET /auth/me); undefined means "not observed here", not "false", and the mustChangePassword.enforce guard 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-token with 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-circuits refresh() 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 (like mfa/verify), returning a full session. Unlike reset-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 via setup-totp inline, so recovering a lost device is one flow, not two sign-ins. Emits MFA_RECOVERY_CODE_USED. The client SDKs expose verifyRecoveryCode({ code, trustDevice? }) (React useNestAuth().verifyRecoveryCode, Flutter NestAuthClient.verifyRecoveryCode).
  • Multiple recovery codes. generate-recovery-code now issues a set (default 10, mfa.recoveryCodeCount), one hashed single-use row each in a new nest_auth_mfa_recovery_codes table. Response is { codes: string[], code } (code = codes[0], back-compat). The legacy single-column code is still honoured.
  • New opt-in mfa.requireVerifiedContactForEnrollment (default false): 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 on synchronize: false must add it in a migration (id uuid pk, userId uuid, codeHash varchar, usedAt timestamp null, createdAt timestamp). Nothing else changed.


2.9.2 — MFA config fix (backend)

  • Fixed: mfa.methods now replaces the default [EMAIL, TOTP] instead of being concatenated with it. The config was deep-merged (arrays concatenate), so mfa.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 when methods is 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. Now setup-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-totp could permanently lock out a TOTP-only user. It deleted the TOTP secrets and spent the recovery code but left isMfaEnabled: true → the next login returned isRequiresMfa with an empty method list. It now turns MFA off when no verified method remains (a surviving email/SMS method keeps it on).
  • Fixed (MFA): defaultMfaMethod in 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-code with the pending challenge token mid-login — complete mfa/verify first. 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 (via clearAuthState(), skipping the pointless/doomed /auth/logout round 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 exported classifyAuthFailure / AuthFailureKind, so you classify without re-deriving from status codes. Default messages for network/timeout/5xx are now user-friendly.

  • Fixed (react): AuthProvider fires onUnauthenticated() only on a definitive rejection; an indeterminate failure keeps the user where they are and surfaces error. New AuthStatus value '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 — getServerAuth distinguishes an indeterminate verify failure (5xx/timeout/network) from a definitive 401/403, and withAuth returns a retryable 503 instead of a 401 during 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 on error.kind. New 'unknown' AuthStatus value.

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-in jwt login provider, and custom SSO providers) return the provider's external subject (the OAuth sub / account id) from validate(), but the post-validate() "known user?" lookup fed that into the uuid auth_identity.userId column. It now resolves by the external subject (providerId) instead — the same key handleSocialLogin already uses. On SQLite/sqljs the column type isn't enforced, so the in-memory tests never caught it (the path returned a spurious INVALID_CREDENTIALS there instead of crashing).
  • Added (backend): an exported SocialAuthProvider base. Google/Apple/Facebook/GitHub extend it, and it resolves the linked identity by providerId. If you write a custom social / SSO provider, extend SocialAuthProvider (not BaseAuthProvider) 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 our userId; social/external providers override to resolve by providerId), so findIdentityByUserId keeps 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.5 together.


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 off tenants.enabled === true while the Users page keyed off the resolved tenant mode — could hide the Tenants nav while other tenant UI still showed. tenantEnabled is now derived from the resolved tenant mode, so every surface uses one signal.
  • Fixed (admin UI): a failed /client-config load 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' } (set enabled: true explicitly), and that GET /auth/client-config returns your tenants + roleGuards (a custom clientConfig.factory must 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.issuer is finally applied. setupTotp accepts an optional label (via POST /auth/mfa/setup-totp body or client.setupTotp({ label })) to disambiguate multi-tenant / multi-account users, and returns otpAuthUrl / issuer / account. See MFA → TOTP enrollment.
  • Fixed (MFA): GET /auth/mfa/status now reports allowUserToggle / canToggle from policy (config), not from whether the user already has MFA on — so a member with MFA off can actually turn it on.

2.8.2 was a version-only re-publish (identical code to 2.8.1).


2.8.1 — consumer-feedback fixes

A patch release addressing feedback on 2.8.0.

  • Fixed (backend): forRootAsync can mint tokens again — JwtService and friends read module options lazily instead of capturing them at construction (fixes Missing session.jwt.secret, and removes a latent pre-2.8.0 hazard where forRootAsync could sign with the insecure default secret).
  • Fixed (backend): custom auth providers (customAuthProviders) work with a plain new MyProvider(opts) and forRoot — the registry injects the repositories, and the config merge preserves the instance's methods.
  • Fixed (backend): NestAuthBlockedEmailDomain is exported from NestAuthEntities and the barrel, so migrations can create the blocked-email-domains table.
  • Added (contracts): NestAuthErrorCode — a browser-safe enum of every server error code (with a drift-guard test), so a frontend matches typed codes instead of bare strings.
  • Changed (MFA): verifyMfa surfaces the specific OTP reason (expired / invalid / "request a new code") instead of a generic failure. A wrong MFA code now returns the specific VERIFICATION_CODE_* code rather than MFA_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.secret is 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; set session.jwt.validateSecretStrength: true to make that a hard error too.

  • Breaking: the jwt login provider is now opt-in. It used to register automatically whenever session.jwt existed, and it trusts any token signed with your secret and mints a session for its sub — a privileged bypass that must be enabled deliberately with session.jwt.enableLoginProvider: true. As defense-in-depth the auth guard now only accepts type: 'access' tokens, so a refresh token presented as a Bearer is rejected.

  • Breaking (admin console): a secretKey / sessionSecret under 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 sessionSecret is set, the admin session-signing key is now derived from secretKey rather than being the raw secretKey. Admin sessions minted by older versions are invalidated — admins re-login once after upgrade.

  • Breaking (admin console): admin login is throttled by default (429 after ~5/min). Opt out with adminConsole.bruteForce.enabled: false.

  • Breaking (admin console): the admin session cookie is now Secure unless NODE_ENV is explicitly development or test (was: Secure only when NODE_ENV === 'production', so staging / unset / misconfigured prod shipped it in cleartext). Force it off with adminConsole.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 optional allowedOrigins check. No-op for bearer/header auth; required if you set sameSite: '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 shared store for 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 with password.policy.checkBreached: true (Have I Been Pwned k-anonymity, fail-open).

  • Added (opt-in): email-verification gatingregistration.requireVerifiedEmail: true hard-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. block mode rejects with 403 EMAIL_DOMAIN_NOT_ALLOWED; flag mode 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>/signup is refused and further admins must be created by a signed-in admin from the dashboard, so a leaked secretKey can't mint unlimited super-admins. Restore the legacy shared-key path with adminConsole.allowPublicSignupAfterFirstAdmin: true.

  • Hardened (admin console): revocable admin sessions — a dashboard password change now bumps tokenVersion and 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 real Content-Security-Policy with frame-ancestors 'none' (a <meta> CSP can't set that, so a logged-in admin was frameable), X-Content-Type-Options: nosniff, and Referrer-Policy: no-referrer — and the injected window.__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 with security.csrf enabled.

  • 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 / avatarUrl from 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-config now returns passwordless flags, OAuth public client/app ids (google / facebook / apple / github), customProviders, platformAccess.enabled, and accessTokenType — 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-client re-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 an AuthProvider fed a separate bootstrap client still reported the user signed in. Two token sources disagreeing with no signal.
  • Added: fallbackClient on AccountManagerConfig — the client to use when no account is active (typically your bootstrap client) — plus a public resolveActiveClient() (also on IAccountSwitcher) 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-react ends up installed twice (common in pnpm/monorepos when a peer-React version split double-installs it), each copy called createContext() 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. isLoading stayed true forever and AuthGuard, RequirePermission / RequireRole, and the withRequirePermission / withRequireRole HOCs silently rendered a blank page for authenticated users.
  • Fixed: AuthContext and AccountSwitcherContext are now cross-realm singletons pinned on globalThis via Symbol.for(...), so every duplicate copy shares one context object. (Safe because React itself stays a single instance via peerDependency — 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.warn points 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-react to 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, because removeAccount can only target namespaces still in the index. On ready() the manager now reaps any namespace the index no longer references (opt out with reapOrphanStorageOnReady: false; a corrupt index never triggers reaping).
  • Added: AccountManager.reset() (also on CookieAccountManager, the IAccountSwitcher interface, and the React useAccountSwitcher()) — 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 abandoned createPendingClient() / AccountMfaRequiredError pending 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) and attachToAxios no longer deadlocks on an expired session. The boot verifySession 401 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. createAxiosAdapter now tags AuthClient's own requests and attachToAxios skips them; the new NEST_AUTH_ADAPTER_REQUEST export lets custom adapters opt out too.
  • Behavior: attachToAxios / attachToFetch now 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 the AuthClient / AccountManager methods; if you renamed those endpoints, pass the custom paths in skipPaths. See HTTP adapters.

2.7.0 — platform-user listing + passwordless login completion

  • Added (backend): UserService.getPlatformUsers(options?), getPlatformUsersAndCount(options?), and getPlatformUsersByRole(roleName, guard?) — list super-admins by the PlatformAccess marker without scanning tenant users (the list analog of getPlatformUserByEmail). Caller where / relations / pagination are honored.
  • Added (client + React): AuthClient.passwordlessLogin(dto) and useNestAuth().passwordlessLogin(dto) complete a passwordless sign-in — exchange the emailed/texted code for a session (the completion step for passwordlessSend), returning a normal auth response. New IPasswordlessLoginRequest ({ identifier, code, channel?, tenantId?, rememberMe? }); channel defaults to trying both email and SMS. Wraps POST /auth/login — no backend change.

2.6.0 — multi-account switcher DX

  • Added: attachToAxios / attachToFetch accept an AccountManager / AuthHeaderProvider, and the managers expose instance attachToAxios() / attachToFetch() — a shared axios/fetch follows the active account with no re-attach on switch.
  • Added: account naming — AccountSnapshot.tenantName, AccountMeta, addAccount(dto, { meta }), and setAccountMeta(accountId, meta) — so a same-email owner's accounts are distinguishable in the switcher.
  • Added (React): useAccountSwitcher().completeMfa(error, verifyDto) to finish an MFA-gated addAccount; commitAccount is now on IAccountSwitcher, and cookie mode surfaces AccountMfaRequiredError too.
  • 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_REQUIRED under TENANT_MODE=isolated. UserService.createUser / getUserByEmail require a tenantId there, which broke admin/platform-admin bootstrap on boot — platform admins are tenant-less.
  • Added: first-class UserService.createPlatformUser(data) and getPlatformUserByEmail(email) — tenant-less provisioning + lookup that work in every tenant mode. A platform user is identified by the PlatformAccess marker (the same row the login path enforces), so the lookup never returns a regular tenant account, and createPlatformUser establishes 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 resolved tenantId. 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 right tenantId. 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 the LegacyRouteConverter warning; Express 4 is unaffected.
  • Removed: dangling InitializeAdmin request/response DTOs and the IInitializeAdminRequest / IInitializeAdminResponse contracts (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; switchTenant disabled; login needs a tenantId). The library does not switch data sources per tenant. New ISOLATED login recipe.

2.1.1 — client-config hooks

  • Added: AuthClient.getClientConfig() (+ the IClientConfig type) — fetch the backend's public config with no auth.
  • Added (React): useClientConfig() and useMultiAccountEnabled() — 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 (default false), surfaced on GET /auth/client-config. Cookie mode gains per-account cookies + a non-httpOnly active-account selector and a GET /auth/accounts listing endpoint. The backend was already multi-session; switching is client-side.
  • Client SDK: AccountManager (header mode — one client per account, namespaced storage) and CookieAccountManager (cookie mode), behind a shared IAccountSwitcher interface.
  • React SDK: AccountSwitcherProvider (separate from AuthProvider) + useAccountSwitcher / useAccounts / useActiveAccount.
  • Recipe: Multi-account login & switching.

2.0.4 — @Public() works under a global guard

  • Fixed: NestAuthAuthGuard now honours @Public() (IS_PUBLIC_KEY) — previously a silent no-op. The documented global APP_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.revokedAt used datetime, which Postgres rejects (the app couldn't boot). It now uses an inferred, portable type (boots on Postgres, MySQL, SQLite).
  • Fixed: the optional apple-auth peer is now lazy-loaded — apps that don't install it (or use native Apple sign-in) boot fine. (google-auth-library / fb were already lazy.)

2.0.2 — public-barrel exports

  • Fixed: Public / IS_PUBLIC_KEY and AuthExceptionFilter are now exported from the package barrel (they were defined but unreachable), plus a new @CurrentUser() decorator and a re-exported CurrentAdmin.
  • Added: a package exports map; 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/React passwordlessSend helpers and a POST /auth/passwordless/send endpoint.
  • Phone verificationPOST /auth/send-phone-verification and POST /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 with adminConsole), 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 transactional EntityManager is now passed to create/update/delete and onSignup / onLogin hooks so your sync code commits atomically with the user. See the hooks reference.
  • RBAC eventsRoleService and PermissionService now emit ROLE_* / 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 moment dependency; 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.

On this page