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
- Restart the OAuth login to obtain a new challenge — expiry is not extendable
- Treat error-challenge-expired and error-challenge-not-found identically in UI code: both mean 'start over'
- Show the remaining time on the 2FA screen so users act before expiry
- 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
- Handle error-challenge-expired and error-challenge-not-found identically: restart
- Show a countdown so users finish before expiry
- Do not cache challenge state across page reloads
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
- error-parameter-required
- error-challenge-not-found
- error-invalid-challenge-method
- totp-max-attempts
- error-user-not-found
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/a16f5aa1d33b9780.
Report an issue: GitHub.