n8n-io/n8n · error · BadRequestError

Invalid MFA token.

Error message

Invalid MFA token.

What it means

The submitted MFA code failed TOTP verification against the user's decrypted secret. mfaService.totp.verifySecret({ secret, mfaCode }) returned false, so n8n rejects the password change with 400 'Invalid MFA token.'.

Source

Thrown at packages/cli/src/controllers/password-reset.controller.ts:215

	})
	async changePassword(
		req: AuthlessRequest,
		res: Response,
		@Body payload: ChangePasswordRequestDto,
	) {
		const { token, password, mfaCode } = payload;

		const user = await this.authService.resolvePasswordResetToken(token);
		if (!user) throw new NotFoundError('');

		if (user.mfaEnabled) {
			if (!mfaCode) throw new BadRequestError('If MFA enabled, mfaCode is required.');

			const { decryptedSecret: secret } = await this.mfaService.getSecretAndRecoveryCodes(user.id);

			const validToken = this.mfaService.totp.verifySecret({ secret, mfaCode });

			if (!validToken) throw new BadRequestError('Invalid MFA token.');
		}

		const passwordHash = await this.passwordUtility.hash(password);

		await this.userService.update(user.id, { password: passwordHash });

		this.logger.info('User password updated successfully', { userId: user.id });

		this.authService.issueCookie(res, user, user.mfaEnabled, req.browserId);

		this.eventService.emit('user-updated', { user, fieldsChanged: ['password'] });

		// if this user used to be an LDAP user
		const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
		if (ldapIdentity) {
			this.eventService.emit('user-signed-up', {
				user,
				userType: 'email',

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Enter a freshly generated code immediately and resubmit.
  2. Sync the authenticator device's clock (TOTP is time-based).
  3. If the device is lost, follow the recovery-code / admin-disabled-MFA path instead of guessing.
Defensive patterns

Strategy: retry

Validate before calling

// No deterministic pre-check; just ensure the code is fresh (<25s old) before submit.
function isCodeFresh(generatedAtMs, maxAgeMs = 25000) { return Date.now() - generatedAtMs < maxAgeMs; }

Try / catch

try {
  await api.post('/change-password', { token, password, mfaCode });
} catch (e) {
  if (e.status === 400 && /Invalid MFA token/.test(e.message)) {
    // re-prompt for a fresh code; do not retry the same code
  } else { throw e; }
}

Prevention

When it happens

Trigger: POST /change-password for an mfaEnabled user where mfaCode is provided but does not match a valid TOTP window for the stored secret.

Common situations: The 30-second TOTP window elapsed between generating and submitting the code; the user read the code from the wrong authenticator device; clock skew between the client device and server; the secret was re-enrolled since the code was generated.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/7a0135395b54da80. Report an issue: GitHub.