eyaltoledano/claude-task-master · error · AuthenticationError

INVALID_MFA_CODE

INVALID_MFA_CODE

Error message

Invalid MFA code: ${error.message}

What it means

SupabaseClient.verifyMFA() throws this when `client.auth.mfa.verify({factorId, challengeId, code})` returns an error — Supabase rejected the TOTP code (or the challenge). It is surfaced as AuthenticationError with code INVALID_MFA_CODE, distinguishing a wrong/expired code from infrastructure failures.

Source

Thrown at packages/tm-core/src/modules/integration/clients/supabase-client.ts:567

				await client.auth.mfa.challenge({ factorId });

			if (challengeError || !challengeData) {
				throw new AuthenticationError(
					`Failed to create MFA challenge: ${challengeError?.message || 'Unknown error'}`,
					'MFA_VERIFICATION_FAILED'
				);
			}

			// Verify the TOTP code
			const { data, error } = await client.auth.mfa.verify({
				factorId,
				challengeId: challengeData.id,
				code
			});

			if (error) {
				this.logger.error('MFA verification failed:', error);
				throw new AuthenticationError(
					`Invalid MFA code: ${error.message}`,
					'INVALID_MFA_CODE'
				);
			}

			if (!data) {
				throw new AuthenticationError(
					'No data returned from MFA verification',
					'INVALID_RESPONSE'
				);
			}

			// After successful MFA verification, refresh the session to get the upgraded AAL2 session
			const {
				data: { session },
				error: refreshError
			} = await client.auth.refreshSession();

View on GitHub (pinned to c0c98d367c)

Solutions

  1. Ask the user to re-enter the current 6-digit code from the authenticator app (prompt for a fresh challenge on retry).
  2. Create a new challenge before each verification attempt — challenges expire; never reuse a challengeId.
  3. Check authenticator device clock sync (TOTP depends on accurate time).
  4. Verify the code is submitted promptly after the challenge is created; log error.message for specific Supabase codes.

Example fix

// before
const { data: c } = await supabase.auth.mfa.challenge({ factorId });
await client.verifyMFA(factorId, code); // may reuse stale challenge internally
// after
let ok = false;
for (let attempt = 0; attempt < 3 && !ok; attempt++) {
  const code = await promptForTotp(); // fresh code each attempt
  try { await client.verifyMFA(factorId, code); ok = true; }
  catch (e) { if (e.code !== 'INVALID_MFA_CODE') throw e; }
}
Defensive patterns

Strategy: retry

Validate before calling

// guard before submitting
if (!/^[0-9]{6}$/.test(code)) throw new Error('TOTP code must be 6 digits');
// prompt immediately after challenge creation so the code is still in its validity window

Type guard

function isSixDigitCode(c: unknown): c is string {
  return typeof c === 'string' && /^[0-9]{6}$/.test(c);
}

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    await client.verifyMFA(factorId, await promptTotp());
    break;
  } catch (e) {
    if (e instanceof AuthenticationError && e.code === 'INVALID_MFA_CODE') {
      console.error('Incorrect or expired code — try the current 6-digit code');
      continue; // new prompt (and new challenge)
    }
    throw e;
  }
}

Prevention

When it happens

Trigger: User enters a wrong 6-digit TOTP code; code is reused (same code used in the previous 30s window); code entered too slowly (past the validity window); challenge expired between challenge() and verify(); clock drift on the user's authenticator device.

Common situations: Typo'd or stale authenticator code; copying an old code from an authenticator app list; system clock skew on phone or machine; prompting for MFA, waiting (e.g. CI job paused), then submitting after challenge expiry.

Related errors


AI-assisted analysis of eyaltoledano/claude-task-master@c0c98d367c (2026-08-29). Data as JSON: /api/errors/7b0480b17c053235. Report an issue: GitHub.