n8n-io/n8n · error · BadRequestError

997

997

Error message

MFA code expired. Close the modal and enable MFA again

What it means

The TOTP code supplied to POST /rest/mfa/enable failed verification (`totp.verifySecret` returned false with a window of 10). The error carries hint code 997 so the frontend knows to tear down the enrollment modal and restart it. The wide window (10 steps) means this usually indicates a wrong secret rather than mere clock skew.

Source

Thrown at packages/cli/src/controllers/mfa.controller.ts:139

		const { id, mfaEnabled } = req.user;

		await this.externalHooks.run('mfa.beforeSetup', [req.user]);

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

		if (!mfaCode) throw new BadRequestError('Token is required to enable MFA feature');

		if (mfaEnabled) throw new BadRequestError('MFA already enabled');

		if (!secret || !recoveryCodes.length) {
			throw new BadRequestError('Cannot enable MFA without generating secret and recovery codes');
		}

		const verified = this.mfaService.totp.verifySecret({ secret, mfaCode, window: 10 });

		if (!verified)
			throw new BadRequestError('MFA code expired. Close the modal and enable MFA again', 997);

		const updatedUser = await this.mfaService.enableMfa(id);

		this.eventService.emit('user-mfa-enabled', {
			user: {
				id: req.user.id,
				email: req.user.email,
				firstName: req.user.firstName,
				lastName: req.user.lastName,
				role: req.user.role,
			},
		});

		this.authService.issueCookie(res, updatedUser, verified, req.browserId);
	}

	@Post('/disable', {
		ipRateLimit: true,

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Re-scan the QR: GET /rest/mfa/qr to get a fresh secret, then re-add to the authenticator.
  2. Re-enter the secret carefully if typed manually (watch base32 ambiguities).
  3. Sync the device clock and generate a fresh code before submitting.
Defensive patterns

Strategy: try-catch

Validate before calling

// Only shape-check; TOTP validity cannot be confirmed client-side.
if (!/^\d{6}$/.test(mfaCode)) throw new Error('Enter a 6-digit code.');

Try / catch

try {
  await restApi.post('/mfa/enable', { mfaCode });
} catch (e) {
  if (e.response?.data?.hint?.code === 997 || /expired/i.test(e.response?.data?.message)) {
    // restart enrollment: new QR, re-add to authenticator
    await restartMfaEnrollment();
  } else throw e;
}

Prevention

When it happens

Trigger: POST /rest/mfa/enable where totp.verifySecret({ secret, mfaCode, window: 10 }) returns false.

Common situations: User scanned the wrong QR / typed the secret manually with a typo; authenticator seeded from a previous, rotated secret; code re-used after expiry.

Related errors


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