Skip to main content

v0.7.1

Someone can change your phone number and switch off your MFA​

Changing a phone number is more destructive than it looks. nauth-toolkit clears the verified flag, deletes every SMS MFA device bound to the old number, and — if SMS was the last factor — switches MFA off entirely.

Until this release, none of that reached the account owner. It was recorded in the audit table and nowhere else.

Phone-change alerts​

The new phoneChanged notification tells the owner what happened, including how many SMS devices went with the old number and whether MFA is now off.

src/config/auth.config.ts
emailNotifications: {
enabled: true,
suppress: {
phoneChanged: false, // opt in, like every optional notification
},
}

The alert is addressed to the account email, never to either phone number. A phone change is routine — people change carriers and jobs — so the mailbox is the stable channel. Notifying the old number would also fail exactly when it matters, since an attacker who changed the number no longer controls it.

For your own delivery, implement IPhoneChangedHook, or on NestJS use the @PhoneChangedHook() decorator:

src/hooks/phone-changed.hook.ts
@Injectable()
@PhoneChangedHook()
export class PhoneChangedAlert implements IPhoneChangedHook {
async execute(metadata: PhoneChangedMetadata): Promise<void> {
await this.alerts.send(metadata.user.email, {
newPhone: metadata.newPhone,
removedDevices: metadata.deactivatedMFADevices,
mfaNowOff: metadata.mfaDisabled,
});
}
}

See Notifications and IPhoneChangedHook.

MFA device removal now notifies​

Related, and worth enabling even if you skip the above: removing an MFA device as a side effect of a contact change now fires mfaDeviceRemoved. Previously that hook only fired when a user removed a device themselves, so a phone or email change stripped a second factor silently.

src/config/auth.config.ts
suppress: {
mfaDeviceRemoved: false,
}

Password reset on a disabled account​

Requesting a reset for an account an admin had disabled sent a code, and confirming it reported success — while login stayed blocked. Nothing told the user why they still could not get in.

The request is now skipped, without revealing that the account exists, and confirmation answers ACCOUNT_LOCKED. That check runs only after the reset code is verified, so the lock is never disclosed to someone who does not hold the code.

Documentation​

A new Environment Cheat Sheet collects the cookie, CORS, CSRF, passkey and social-callback values that actually work for four deployment shapes — everything on localhost, a dev-server proxy or same-site alias against a remote API, same-origin behind a reverse proxy, and frontend and API on sibling subdomains. It ends with a symptom-to-cause table for when cookies silently fail to stick.

Two corrections worth checking against your own code:

  • disableUser never sets isActive: false. The API reference showed that in the response example, but the call only touches isLocked. isActive is enforced at login yet nothing in the toolkit ever writes it. If you branch on isActive in that response, it has always been true. See Admin Operations.
  • The NestJS hook decorators are exported. Seven decorator pages claimed otherwise and told you to register hooks by hand through HookRegistryService. @EmailChangedHook(), @PasswordChangedHook() and the rest all import from @nauth-toolkit/nestjs and work as their own examples show.

Removed: code that never ran​

The account-lockout notification is gone. The hook behind it was never invoked, so no lockout email was ever sent — removing it changes nothing you could observe.

If your config sets this key, delete the line:

src/config/auth.config.ts
emailNotifications: {
suppress: {
accountLockout: true, // no longer a valid key
},
}

The failed-login lockout is unaffected: it is IP-based and answers RATE_LIMIT_LOGIN.

Removed alongside it, all equally unreachable: IAccountLockedHook, AccountLockedMetadata, registerAccountLocked() / executeAccountLocked(), @AccountLockedHook(), sendLockoutEmail() and sendAccountLockedEmail(), and TemplateType.ACCOUNT_LOCKOUT.

Four audit event types go with them: ACCOUNT_ACTIVATED, ACCOUNT_DEACTIVATED, ACCOUNT_LOCKED and ACCOUNT_UNLOCKED. None was ever written. If you alert on account lock or unlock, use ACCOUNT_DISABLED and ACCOUNT_ENABLED — what the toolkit has always recorded. See Audit Logs.