RocketChat/Rocket.Chat · error · Meteor.Error

error-challenge-expired

error-challenge-expired

Error message

challenge expired

What it means

Thrown by POST twoFactorChallenges.sendEmailCode when the pending challenge exists but its expireAt timestamp is in the past. Email 2FA challenges during OAuth login are time-boxed; after expiry the challenge record remains but is no longer usable, so requesting a new code for it fails.

Source

Thrown at apps/meteor/server/api/v1/twoFactorChallenges.ts:30

API.v1.addRoute(
	'twoFactorChallenges.sendEmailCode',
	{ validateParams: isTwoFactorChallengesSendEmailCodeParamsPOST, rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 5 } },
	{
		async post() {
			const { challengeId } = this.bodyParams;

			if (!challengeId) {
				throw new Meteor.Error('error-parameter-required', 'challengeId is required');
			}

			const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId);

			if (!challenge) {
				throw new Meteor.Error('error-challenge-not-found', 'challenge not found');
			}

			if (challenge.expireAt && challenge.expireAt < new Date()) {
				throw new Meteor.Error('error-challenge-expired', 'challenge expired');
			}

			if (challenge.method !== 'email') {
				throw new Meteor.Error('error-invalid-challenge-method', 'invalid challenge method');
			}

			const { userId } = challenge;

			const user = await getUserForCheck(userId);

			if (!user) {
				throw new Meteor.Error('error-user-not-found', 'user not found');
			}

			await emailCheckForOAuth.sendEmailCode(user);

			return API.v1.success();
		},

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Restart the OAuth login to obtain a new challenge — expiry is not extendable
  2. Treat error-challenge-expired and error-challenge-not-found identically in UI code: both mean 'start over'
  3. Show the remaining time on the 2FA screen so users act before expiry
  4. Trigger the resend proactively if the user is still on the page near the expiry mark

Example fix

// before
if (e.error === 'error-challenge-not-found') restart(); // expired challenges slip through
// after
if (e.error === 'error-challenge-not-found' || e.error === 'error-challenge-expired') restart();
Defensive patterns

Strategy: retry

Validate before calling

if (Date.now() > challengeExpiresAt - safetyMarginMs) restartLoginFlow(); // proactive restart before expiry

Try / catch

catch (e) { if (e?.error === 'error-challenge-expired') { const fresh = await startOAuthLogin(); return resendCode(fresh.challengeId); } throw e; }

Prevention

When it happens

Trigger: User starts an OAuth login, gets the 2FA challenge, waits past the expiry window (typically minutes), then clicks 'resend code' or resumes the page; clock-skewed clients delaying the resend call; background tabs that resume an old flow.

Common situations: Long-lived SPA tabs that keep stale challenge state; users stepping away mid-login; email delivery delays pushing the user past the window before they think to resend.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/a16f5aa1d33b9780. Report an issue: GitHub.