You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

添加邮箱2FA后,验证器与邮箱验证码均提示无效排查求助

2FA验证失效排查求助

输入正确验证码时,Authenticator App和邮箱验证的2FA码均被标记为“无效”。添加邮箱认证前,Authenticator App的2FA功能正常,但添加后两者均失效。已确认:邮箱与验证器的令牌生成及发送正常,验证前已移除输入码中的空格和短横线。请求调试建议与排查方向,相关代码如下:

排查方向与调试建议

  • 验证用户上下文一致性
    检查TwoStepVerification、LoginWith2fa、LoginWith2faEmail中获取用户的逻辑,确保每次获取的是同一个用户实例。当前代码用FirstOrDefaultAsync按ID查询,需确认用户ID大小写处理完全一致,同时检查登录流程中用户的SecurityStamp是否被意外修改——若SecurityStamp变化,Identity会判定用户会话无效,直接导致2FA验证失败。

  • 邮箱2FA的令牌问题

    1. 在LoginWith2faEmail的GET方法中,生成令牌后需确认是否实际执行了邮件发送逻辑——当前代码仅生成token变量但未调用发送接口,用户收到的可能是过期旧令牌;
    2. 检查邮箱令牌有效期:默认Email令牌有效期为15分钟,若验证时已过期会导致失败,可通过配置TokenOptions.EmailTokenProvider调整时长;
    3. 确认TwoFactorSignInAsync使用的提供器名称"Email"与生成令牌时的参数一致,且Identity已正确注册Email令牌提供器(当前AddDefaultTokenProviders()已包含,但需排查是否存在自定义覆盖情况)。
  • Authenticator App验证问题

    1. 检查用户的Authenticator密钥是否在添加邮箱2FA后被意外修改,可直接查看数据库中ApplicationUser的AuthenticatorKey字段,确认与用户App中配置的密钥一致;
    2. 验证服务器与用户设备的时间同步性:TOTP令牌依赖时间同步,若两者时间差超过30秒,会直接导致验证失败;
    3. 在TwoFactorAuthenticatorSignInAsync调用前,打印脱敏后的verificationCode和用户AuthenticatorKey,用TOTP算法手动计算令牌,对比是否与用户输入一致。
  • 登录状态与SignInManager上下文
    从密码登录到2FA步骤中,SignInManager需保留正确的用户登录状态——PasswordSignInAsync返回RequiresTwoFactor后,SignInManager会缓存用户信息,后续2FA验证依赖该缓存。若在2FA步骤中重新查询数据库获取用户,可能导致缓存失效。建议在LoginWith2fa和LoginWith2faEmail中改用await _signInManager.GetTwoFactorAuthenticationUserAsync()获取用户实例。

  • 错误日志与调试细节

    1. 增加日志输出:验证前打印脱敏后的verificationCode、用户ID、当前服务器时间;验证后打印SignInResult的完整状态(如IsNotAllowed、IsLockedOut等,不要仅判断成功/失败);
    2. 捕获并分析TwoFactorSignInAsync和TwoFactorAuthenticatorSignInAsync的返回结果,Identity的SignInResult包含更多错误状态,比如IsNotAllowed可能是用户被禁用,但当前代码仅处理了Succeeded和IsLockedOut两种情况。

相关代码

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;
});

内容的提问来源于stack exchange,提问作者Raluca Micu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 01:49:51