{"record":{"id":"a16f5aa1d33b9780","repo":"RocketChat/Rocket.Chat","slug":"error-challenge-expired","errorCode":"error-challenge-expired","errorMessage":"challenge expired","messagePattern":"challenge expired","errorType":"exception","errorClass":"Meteor.Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/server/api/v1/twoFactorChallenges.ts","lineNumber":30,"sourceCode":"API.v1.addRoute(\n\t'twoFactorChallenges.sendEmailCode',\n\t{ validateParams: isTwoFactorChallengesSendEmailCodeParamsPOST, rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 5 } },\n\t{\n\t\tasync post() {\n\t\t\tconst { challengeId } = this.bodyParams;\n\n\t\t\tif (!challengeId) {\n\t\t\t\tthrow new Meteor.Error('error-parameter-required', 'challengeId is required');\n\t\t\t}\n\n\t\t\tconst challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId);\n\n\t\t\tif (!challenge) {\n\t\t\t\tthrow new Meteor.Error('error-challenge-not-found', 'challenge not found');\n\t\t\t}\n\n\t\t\tif (challenge.expireAt && challenge.expireAt < new Date()) {\n\t\t\t\tthrow new Meteor.Error('error-challenge-expired', 'challenge expired');\n\t\t\t}\n\n\t\t\tif (challenge.method !== 'email') {\n\t\t\t\tthrow new Meteor.Error('error-invalid-challenge-method', 'invalid challenge method');\n\t\t\t}\n\n\t\t\tconst { userId } = challenge;\n\n\t\t\tconst user = await getUserForCheck(userId);\n\n\t\t\tif (!user) {\n\t\t\t\tthrow new Meteor.Error('error-user-not-found', 'user not found');\n\t\t\t}\n\n\t\t\tawait emailCheckForOAuth.sendEmailCode(user);\n\n\t\t\treturn API.v1.success();\n\t\t},","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/api/v1/twoFactorChallenges.ts#L12-L48","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nif (e.error === 'error-challenge-not-found') restart(); // expired challenges slip through\n// after\nif (e.error === 'error-challenge-not-found' || e.error === 'error-challenge-expired') restart();","handlingStrategy":"retry","validationCode":"if (Date.now() > challengeExpiresAt - safetyMarginMs) restartLoginFlow(); // proactive restart before expiry","typeGuard":null,"tryCatchPattern":"catch (e) { if (e?.error === 'error-challenge-expired') { const fresh = await startOAuthLogin(); return resendCode(fresh.challengeId); } throw e; }","preventionTips":["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"],"tags":["rest-api","two-factor","oauth","expiry","authentication"],"backgroundTag":"challenge-expired","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}