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
- Re-scan the QR: GET /rest/mfa/qr to get a fresh secret, then re-add to the authenticator.
- Re-enter the secret carefully if typed manually (watch base32 ambiguities).
- 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
- Detect hint code 997 in the response and tear down the modal as the server expects.
- Encourage QR scan over manual entry to avoid base32 typos.
- If the user types the secret, normalize uppercase and strip spaces before comparing.
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.