n8n-io/n8n · error · BadRequestError

If MFA enabled, mfaCode is required.

Error message

If MFA enabled, mfaCode is required.

What it means

The user resolving the reset has MFA enabled, so n8n requires the current TOTP code to authorize the password change. Submitting POST /change-password without an mfaCode for an mfaEnabled user is rejected with 400.

Source

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

	/**
	 * Verify password reset token and update password.
	 */
	@Post('/change-password', {
		skipAuth: true,
		ipRateLimit: true,
	})
	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'] });

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Include a non-empty mfaCode in the request body alongside token and password.
  2. Make the frontend prompt for the authenticator code whenever the user has MFA enabled (e.g. branch on a resolve-token response that indicates mfaEnabled).
  3. Validate the payload shape before sending to avoid a round-trip.

Example fix

// before
// POST /change-password
// { "token": "...", "password": "newP@ss" }
// after
// POST /change-password
{
  "token": "...",
  "password": "newP@ss",
  "mfaCode": "123456"
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate payload shape before POST /change-password when the user has MFA.
function isValidChangePasswordPayload(payload, userMfaEnabled) {
  return Boolean(payload.token && payload.password) &&
    (!userMfaEnabled || (typeof payload.mfaCode === 'string' && payload.mfaCode.trim() !== ''));
}

Type guard

function hasRequiredMfaCode(payload, mfaEnabled) {
  return !mfaEnabled || (typeof payload?.mfaCode === 'string' && payload.mfaCode.length > 0);
}

Try / catch

try {
  await api.post('/change-password', payload);
} catch (e) {
  if (e.status === 400 && /mfaCode is required/.test(e.message)) {
    // prompt for the authenticator code and resend with mfaCode
  } else { throw e; }
}

Prevention

When it happens

Trigger: POST /change-password for a user with user.mfaEnabled === true where the payload's mfaCode field is absent, empty, or undefined.

Common situations: The client form does not render an MFA prompt before submit; a direct API call omitted the field; the frontend assumed MFA was off for the account.

Related errors


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