Invalid 2FA Codes (Authenticator App and Email)

I’m facing an issue where both the authenticator app and email verification codes are marked as “invalid” even though they are correct.

Before adding the email authentication, the 2FA via the authenticator app was working fine, but after adding the email version, they are both broken. Both the authenticator and email codes are being flagged as “invalid,” even though the correct codes are being entered.

What I’ve tried:
Checked the code generation for both email and authenticator app (tokens are generated and sent correctly).
Double-checked that the input codes have spaces and dashes removed before verification.

Any debugging tips or areas to investigate?
Thanks in advance for any help!

The relevant parts of my code are:

AccountController.cs:

   [HttpPost]
   [AllowAnonymous]
   [ValidateAntiForgeryToken]
   public async Task<IActionResult> Login(LoginViewModel model, string? returnUrl = null)
   {
       ViewData["ReturnUrl"] = returnUrl;

       model.Username = model.Username?[..Math.Min(model.Username.Length, 256)] ?? string.Empty;

       var user = await _userManager.Users.FirstOrDefaultAsync(t => t.UserName == model.Username && t.AccessEnabled);

       // Password attempt: lockout if failure threshold reached
       var result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true);

       if (result.Succeeded)
       {
           await LogSuccessfulLogin(user);
           // Check if user needs to enable 2FA
           if (user.Is2FArequired)
           {
               return RedirectToAction(nameof(TwoStepVerification), new { userId = user.Id, rememberMe = model.RememberMe, returnUrl });
           }
           
           return RedirectToLocal(returnUrl);
       }
       if (result.RequiresTwoFactor)
       {
           return RedirectToAction(nameof(TwoStepVerification), new { userId = user.Id, rememberMe = model.RememberMe, returnUrl });
       }
       if (result.IsLockedOut)
       {
           await LogAccountLocked(user);
           return RedirectToAction(nameof(Lockout));
       }

       // Log failed attempt with generic message
       await LogFailedLoginAttempt(user.UserName);
       ModelState.AddModelError("CustomError", "Login failed. Please try again.");
       return View(model);
   }

   // GET: Two-step verification method selection
   [HttpGet]
   [AllowAnonymous]
   public async Task<IActionResult> TwoStepVerification(string userId, bool rememberMe, string? returnUrl = null)
   {
       var user = await _userManager.Users.FirstOrDefaultAsync(t => t.Id.ToLower() == userId.ToLower() && t.AccessEnabled);
       if (user == null)
       {
           return RedirectToAction("Login");
       }

       var model = new TwoStepVerificationViewModel
       {
           Email = user.Email, // Assume user has an email
           TwoFactorMethod = string.Empty,
           UserId = user.Id,
           RememberMe = rememberMe,
       };

       ViewData["ReturnUrl"] = returnUrl;
       return View(model); // This view will allow the user to choose the 2FA method (email or authenticator)
   }

   [HttpPost]
   [AllowAnonymous]
   [ValidateAntiForgeryToken]
   public async Task<IActionResult> TwoStepVerification(TwoStepVerificationViewModel model, string? returnUrl = null)
   {
       var user = await _userManager.Users.FirstOrDefaultAsync(t => t.Id.ToLower() == model.UserId.ToLower() && t.AccessEnabled);

       // Check the selected method
       if (model.TwoFactorMethod == "email")
       {
           return RedirectToAction(nameof(LoginWith2faEmail), new { userId = model.UserId, rememberMe = model.RememberMe, returnUrl });
       }
       else if (model.TwoFactorMethod == "authenticator")
       {          
           return RedirectToAction(nameof(LoginWith2fa), new { userId = model.UserId, returnUrl });
       }
       return View(model);
   }

   [HttpGet]
   [AllowAnonymous]
   public async Task<IActionResult> LoginWith2faEmail(string userId, bool rememberMe, string? returnUrl = null)
   {
       ViewData["ReturnUrl"] = returnUrl;
       
       var user = await _userManager.Users.FirstOrDefaultAsync(t => t.Id.ToLower() == userId.ToLower() && t.AccessEnabled);
      
       LoginWith2faViewModel model = new()
       {
           RememberMe = rememberMe,
           UserId = user.Id,
       };

       // Generate the token for email
       var token = await _userManager.GenerateTwoFactorTokenAsync(user, "Email");
       // Prepare the email body
       
       return View(model);
   }

   [HttpPost]
   [AllowAnonymous]
   public async Task<IActionResult> LoginWith2faEmail(LoginWith2faViewModel model, string? returnUrl = null)
   {
       // Remove spaces and dashes from the code input
       var verificationCode = model.TwoFactorCode?.Replace(" ", string.Empty).Replace("-", string.Empty);

       var result = await _signInManager.TwoFactorSignInAsync("Email", verificationCode, model.RememberMe, model.RememberMachine);

       if (result.Succeeded)
       {
           _logger.LogInformation("User {UserId} successfully logged in with 2FA.", model.UserId);
            var user = await _userManager.Users.FirstOrDefaultAsync(t => t.Id.ToLower() == model.UserId.ToLower() && t.AccessEnabled);
           return RedirectToLocal(returnUrl);
       }
       if (result.IsLockedOut)
       {
           var user = await _userManager.GetUserAsync(User);
           _logger.LogWarning("User with ID {UserId} account locked after 2FA attempt.", model.UserId);
           return RedirectToAction(nameof(Lockout));
       }

       _logger.LogWarning("Invalid authenticator code entered");
       return View(model);
   }

   [HttpGet]
   [AllowAnonymous]
   public async Task<IActionResult> LoginWith2fa(string userId, bool rememberMe = false, string? returnUrl = null)
   {
       // Ensure the user ID is valid and access is enabled
       var user = await _userManager.Users.FirstOrDefaultAsync(t => t.Id.ToLower() == userId.ToLower() && t.AccessEnabled);

       var model = new LoginWith2faViewModel
       {
           RememberMe = rememberMe,
           UserId = user.Id
       };

       ViewData["ReturnUrl"] = returnUrl;
       return View(model);
   }

   [HttpPost]
   [AllowAnonymous]
   public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, string? returnUrl = null)
   {
       try
       {
           // Load the user by ID and ensure access is enabled
           var user = await _userManager.Users.FirstOrDefaultAsync(t => t.Id.ToLower() == model.UserId.ToLower() && t.AccessEnabled);

           // Remove spaces and dashes from the code input
           var verificationCode = model.TwoFactorCode?.Replace(" ", string.Empty).Replace("-", string.Empty);

           var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(verificationCode, model.RememberMe, model.RememberMachine);

           if (result.Succeeded)
           {
               _logger.LogInformation("User {UserId} successfully logged in with 2FA.", model.UserId);
               return RedirectToLocal(returnUrl);
           }
           else if (result.IsLockedOut)
           {
               _logger.LogWarning("User with ID {UserId} account locked after 2FA attempt.", user.Id);
               return RedirectToAction(nameof(Lockout));
           }
           else
           {
               _logger.LogWarning("Invalid authenticator code entered for user '{UserId}'.", user.Id);
               ModelState.AddModelError(string.Empty, "Invalid code. Please try again.");
               return View(model);
           }
       }
       catch (Exception ex)
       {
           _logger.LogError(ex, "Exception occurred during 2FA login attempt for user {UserId}.", model.UserId);
           ModelState.AddModelError(string.Empty, "An error occurred during the login process. Please try again.");
       }
       ModelState.AddModelError(string.Empty, "There was a problem logging in. Please try again.");
       return View(model);
   }

Program.cs

builder.Services.AddDbContext<IdentityContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
    sqlServerOptionsAction: sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(30),
            errorNumbersToAdd: null);
    })
);
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<IdentityContext>()
    .AddDefaultTokenProviders();

#region Identity Configuration
builder.Services.Configure<IdentityOptions>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = true;
    options.Password.RequireLowercase = true;
    options.Password.RequiredUniqueChars = 6;

    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    options.User.RequireUniqueEmail = true;
    options.Tokens.AuthenticatorTokenProvider = TokenOptions.DefaultAuthenticatorProvider;
});

After examining your code I can’t see anything wrong, however, the issue might be in the inner working’s of the _signInManager.

A couple of potential issues come to mind.

1: Identity cookies are not present

Both methods below require a cookie from the TwoFactorUserIdScheme to be present in the request headers.

  • _signInManager.TwoFactorSignInAsync()
  • _signInManager.TwoFactorAuthenticatorSignInAsync()

You have correctly configured the Identity in the Program.cs so this cookie should be passed in the headers since you called the _signInManager.PasswordSignInAsync() method. Which either sets the ApplicationScheme or TwoFactorUserIdScheme cookie based on the SignInResult.

I’m assuming the _signInManager cannot retrieve the 2FA data from the cookie, not being able to find the user to authenticate and therefore returns a failed SignInResult.

You could test this by avoiding the _signInManager and directly verifying the verificationCode by calling:

await _userManager.VerifyTwoFactorTokenAsync(user, "Email", verificationCode);

2: Using ViewModels instead of cookies

As mentioned in the case above, the _signInManager requires cookies to retrieve 2FA data and authenticate a user.

If you want to use the data passed in the ViewModels to retrieve 2FA data and authenticate a user you’ll have to implement your own Sign-In logic. For example:

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2faEmail(LoginWith2faViewModel model, string? returnUrl = null)
{
    // Remove spaces and dashes from the code input
    var verificationCode = model.TwoFactorCode?.Replace(" ", string.Empty).Replace("-", string.Empty);

    // Find the user by Id.
    var user = await _userManager.FindByIdAsync(model.UserId);
    if (user == null)
    {
        // Do something when user isn't found.
    }

    // Check if user is allowed to sign in.
    if (!await _signInmanager.CanSignInAsync(user) && await _userManager.IsLockedOutAsync(user))
    {
        // Do something when user isn't allowed to sign in.
    }

    if (!await _userManager.VerifyTwoFactorTokenAsync(user, "Email", verificationCode))
    {
        // Do something when the user inputs the wrong code.
    }

    // Finally when the user is allowed to sign in and the code is correct, sign them in.
    await _signInmanager.SignInAsync(user, model.RememberMe, "Your schema name");

    return View(model);
}

I hope this helps resolve your issue!

4

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật