{"record":{"id":"07107a465bdafa22","repo":"NginxProxyManager/nginx-proxy-manager","slug":"no-2fa-challenge-pending","errorCode":null,"errorMessage":"No 2FA challenge pending","messagePattern":"No 2FA challenge pending","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"frontend/src/context/AuthContext.tsx","lineNumber":61,"sourceCode":"\n\tconst handleTokenUpdate = (response: TokenResponse) => {\n\t\tAuthStore.set(response);\n\t\tsetAuthenticated(true);\n\t\tsetTwoFactorChallenge(null);\n\t};\n\n\tconst login = async (identity: string, secret: string) => {\n\t\tconst response = await getToken(identity, secret);\n\t\tif (isTwoFactorChallenge(response)) {\n\t\t\tsetTwoFactorChallenge({ challengeToken: response.challengeToken });\n\t\t\treturn;\n\t\t}\n\t\thandleTokenUpdate(response);\n\t};\n\n\tconst verifyTwoFactor = async (code: string) => {\n\t\tif (!twoFactorChallenge) {\n\t\t\tthrow new Error(\"No 2FA challenge pending\");\n\t\t}\n\t\tconst response = await verify2FA(twoFactorChallenge.challengeToken, code);\n\t\thandleTokenUpdate(response);\n\t};\n\n\tconst cancelTwoFactor = () => {\n\t\tsetTwoFactorChallenge(null);\n\t};\n\n\tconst loginAs = async (id: number) => {\n\t\tconst response = await loginAsUser(id);\n\t\tAuthStore.add(response);\n\t\tqueryClient.clear();\n\t\twindow.location.reload();\n\t};\n\n\tconst logout = () => {\n\t\tif (AuthStore.count() >= 2) {","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/NginxProxyManager/nginx-proxy-manager/blob/934a3fafe5ae82d752f0a18c0f1d0eb050296730/frontend/src/context/AuthContext.tsx#L43-L79","documentation":"The AuthContext in the NPM frontend manages a two-step login flow: after credentials are accepted with a 2FA-enabled account, the server returns a challenge token which is stored in state as twoFactorChallenge. verifyTwoFactor is the second step and refuses to run when no challenge is currently held in state, because it has no challengeToken to send to the verify2FA API.","triggerScenarios":"Calling verifyTwoFactor(code) before a successful first-stage login that returned a challenge, calling it twice (the challenge is cleared after the first attempt or after cancelTwoFactor), or after a page reload/state reset where the in-memory twoFactorChallenge variable is null again.","commonSituations":"UI state bugs where the 2FA code input is shown on the wrong screen, stale rendered forms after the challenge expired or was consumed, direct programmatic calls to the verify function out of order, or a first-factor login failure that the UI ignored before showing the OTP input.","solutions":["Ensure login() (first stage) succeeded and set twoFactorChallenge before rendering the OTP input or calling verifyTwoFactor","Guard the UI: only show/submit the code form when twoFactorChallenge is non-null; otherwise route the user back to the credentials screen","If the challenge was consumed or lost (e.g. after navigation/reload), restart the flow from the credentials step to obtain a new challengeToken","Check that handleTokenUpdate or an earlier verify attempt didn't clear the challenge before a retry is attempted"],"exampleFix":"// before\nconst verifyTwoFactor = async (code: string) => {\n  if (!twoFactorChallenge) {\n    throw new Error(\"No 2FA challenge pending\");\n  }\n  ...\n};\n\n// after: caller checks state first\nif (!twoFactorChallenge) {\n  setShowOtpInput(false);\n  await login(credentials); // restart flow to get a new challenge\n} else {\n  await verifyTwoFactor(code);\n}","handlingStrategy":"type-guard","validationCode":"const canVerify = () => twoFactorChallenge !== null && !!twoFactorChallenge.challengeToken;\n\nif (!canVerify()) {\n  // restart the login flow instead of verifying\n  setShowOtp(false);\n  return;\n}\nawait verifyTwoFactor(code);","typeGuard":"interface TwoFactorChallenge { challengeToken: string; /* ... */ }\n\nconst isChallengePending = (\n  c: TwoFactorChallenge | null,\n): c is TwoFactorChallenge => c !== null && typeof c.challengeToken === 'string';","tryCatchPattern":"try {\n  await verifyTwoFactor(code);\n} catch (e) {\n  if (e instanceof Error && e.message === 'No 2FA challenge pending') {\n    resetToCredentialsStep(); // re-authenticate to get a new challenge\n    return;\n  }\n  throw e;\n}","preventionTips":["Model login as an explicit state machine: 'credentials' -> 'otp' -> 'done'; only render OTP UI in the otp state","Clear and re-acquire the challenge on any navigation, reload, or cancel action","Disable the submit button until the OTP form is visible with a live challenge"],"tags":["nginx-proxy-manager","frontend","two-factor-auth","react","state-machine"],"backgroundTag":"auth-challenge-state-missing","analyzedSha":"934a3fafe5ae82d752f0a18c0f1d0eb050296730","analyzedAt":"2026-08-27T14:34:22.258Z","schemaVersion":2},"datasetVersion":"2026-08-27T19:17:21.184Z"}