{"record":{"id":"67024915151fa8e5","repo":"fullstackhero/dotnet-starter-kit","slug":"two-factor-invalid-the-authenticator-code-is-invalid-or","errorCode":null,"errorMessage":"two_factor_invalid: The authenticator code is invalid or expired.","messagePattern":"two_factor_invalid: The authenticator code is invalid or expired\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/IdentityService.cs","lineNumber":86,"sourceCode":"    private async Task VerifyTwoFactorOrThrowAsync(FshUser user, string? twoFactorCode)\n    {\n        if (string.IsNullOrWhiteSpace(twoFactorCode))\n        {\n            throw new CustomException(\n                \"two_factor_required: An authenticator code is required to complete sign-in.\",\n                errors: null,\n                HttpStatusCode.Unauthorized);\n        }\n\n        var valid = await _userManager.VerifyTwoFactorTokenAsync(\n            user,\n            _userManager.Options.Tokens.AuthenticatorTokenProvider,\n            twoFactorCode);\n\n        if (!valid)\n        {\n            _logger.LogWarning(\"Invalid two-factor code for user {UserId}\", user.Id);\n            throw new UnauthorizedException(\"two_factor_invalid: The authenticator code is invalid or expired.\");\n        }\n    }\n\n    public async Task<(string Subject, IEnumerable<Claim> Claims)?>\n        ValidateRefreshTokenAsync(string refreshToken, CancellationToken ct = default)\n    {\n        var tenant = GetValidatedTenant();\n        var user = await FindUserByRefreshTokenAsync(refreshToken, tenant.Id, ct);\n\n        ValidateRefreshTokenExpiry(user);\n        ValidateUserStatus(user);\n        ValidateTenantStatus(tenant);\n\n        var claims = await BuildUserClaimsAsync(user, tenant.Id, ct);\n        return (user.Id, claims);\n    }\n\n    public async Task StoreRefreshTokenAsync(string subject, string refreshToken, DateTime expiresAtUtc, CancellationToken ct = default)","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/IdentityService.cs#L68-L104","documentation":"VerifyTwoFactorOrThrowAsync validates the supplied code via UserManager.VerifyTwoFactorTokenAsync with the AuthenticatorTokenProvider; when the code does not match (wrong, stale, already used, or clock drift) it logs a warning for the user and throws UnauthorizedException with the two_factor_invalid marker.","triggerScenarios":"Typing an expired TOTP (code window passed), a code generated against a wrong secret (e.g. re-enrolled authenticator), garbage/placeholder values, or a recovery code sent where a TOTP token is expected.","commonSituations":"User's device clock skewed by minutes; user re-scanned the QR so old app entries are invalid; copy-paste including whitespace; latency between generating and submitting the last seconds of a 30s window.","solutions":["Generate and submit a fresh code immediately; if it fails, wait for the next TOTP window and retry once.","Check the device/system clock synchronization (NTP) — skewed clocks are the top cause.","Confirm the authenticator entry matches the account's current secret; re-enroll by re-scanning the QR if the secret was reset.","Trim spaces from the pasted code and ensure you send a TOTP code, not a recovery code, to this path."],"exampleFix":"// before\nawait login({ email, password, twoFactorCode: cachedCode }); // stale code -> 401\n// after\nconst code = await promptTotp(); // fresh from authenticator, trimmed\nawait login({ email, password, twoFactorCode: code.trim() });","handlingStrategy":"retry","validationCode":"const code = getFreshTotpCode().trim();\nif (!/^\\d{6}$/.test(code)) { showError('Enter the 6-digit code'); return; }","typeGuard":"function isValidTotpFormat(code) {\n  return typeof code === 'string' && /^\\d{6}$/.test(code.trim());\n}","tryCatchPattern":"try { await login({ email, password, twoFactorCode: code }); }\ncatch (e) {\n  if (String(e.message).includes('two_factor_invalid')) {\n    await waitForNextTotpWindow();\n    return retryWithFreshCode(); // one retry max\n  }\n  throw e;\n}","preventionTips":["Generate the code right before submitting; don't cache codes.","Keep device clocks NTP-synced.","Trim whitespace from pasted codes.","Re-enroll the authenticator after any QR/secret reset."],"tags":["authentication","two-factor","totp","identity"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}