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

ASP.NET Identity双因素认证:过期TOTP仍可验证成功求助

问题原因

你遇到的问题是因为ASP.NET Identity默认的EmailTokenProvider(对应TokenOptions.DefaultEmailProvider)存在两个默认行为:

  • 验证时允许时间偏移窗口(默认兼容前后各一个TOTP周期),导致过期不久的token仍能通过校验;
  • 没有内置的token一次性使用机制,只要token在有效时间窗口内,即使是旧token也能重复验证通过。
解决方案

下面提供两种可行的修复方式,按需选择:

方式一:自定义严格的EmailTokenProvider

通过继承默认的EmailTokenProvider,重写生成和验证逻辑,严格控制过期时间并强制token一次性使用:

1. 实现自定义TokenProvider

public class StrictEmailTokenProvider<TUser> : EmailTokenProvider<TUser> where TUser : class
{
    // 自定义过期时间(这里设为3分钟)
    private const int TokenExpirationMinutes = 3;

    public override async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
    {
        var token = await base.GenerateAsync(purpose, manager, user);
        // 将token的生成时间(UTC)存储到用户的认证token中
        await manager.SetAuthenticationTokenAsync(user, Name, purpose, DateTime.UtcNow.ToString("o"));
        return token;
    }

    public override async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
    {
        // 先通过基类验证token格式有效性
        var baseValid = await base.ValidateAsync(purpose, token, manager, user);
        if (!baseValid) return false;

        // 获取存储的token生成时间
        var creationTimeStr = await manager.GetAuthenticationTokenAsync(user, Name, purpose);
        if (!DateTime.TryParse(creationTimeStr, out var creationTime)) return false;

        // 检查是否超过过期时间
        if (DateTime.UtcNow.Subtract(creationTime).TotalMinutes > TokenExpirationMinutes)
            return false;

        // 验证通过后删除存储的时间,确保token只能用一次
        await manager.RemoveAuthenticationTokenAsync(user, Name, purpose);
        return true;
    }
}

2. 注册自定义Provider

在Program.cs(或Startup.cs)的Identity服务配置中,添加自定义的TokenProvider:

services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    // 注册自定义Provider,命名为"StrictEmail"
    .AddTokenProvider<StrictEmailTokenProvider<IdentityUser>>("StrictEmail");

3. 修改生成和验证代码

生成token时使用自定义Provider:

var token = await _userManager.GenerateTwoFactorTokenAsync(user, "StrictEmail");

验证时也对应使用:

var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
var signIn = await _signInManager.TwoFactorSignInAsync("StrictEmail", code, false, false);

if (signIn.Succeeded)
{
   // 后续逻辑
}

方式二:手动管理Token的有效性和使用状态

如果不想自定义Provider,可以在业务层手动存储token的生成时间和使用状态,验证前先做校验:

1. 生成Token时存储信息

生成token后,将token、用户ID、过期时间存储到数据库或缓存(比如Redis):

var token = await _userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
// 示例:用自定义的TokenStore存储,键为用户ID,值包含token和过期时间
await _tokenStore.SaveUserTokenAsync(user.Id, token, DateTime.UtcNow.AddMinutes(3));

2. 验证时先检查有效性

在调用TwoFactorSignInAsync前,先验证token是否未过期且未被使用:

var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
var tokenInfo = await _tokenStore.GetUserTokenAsync(user.Id, code);

// 检查token是否存在、未过期
if (tokenInfo == null || tokenInfo.ExpiresAt < DateTime.UtcNow)
{
    // 处理无效/过期token的逻辑(比如返回错误提示)
    return;
}

// 调用Identity的验证方法
var signIn = await _signInManager.TwoFactorSignInAsync(TokenOptions.DefaultEmailProvider, code, false, false);
if (signIn.Succeeded)
{
    // 验证通过后删除存储的token,避免重复使用
    await _tokenStore.RemoveUserTokenAsync(user.Id, code);
    // 后续逻辑
}

内容的提问来源于stack exchange,提问作者Pawan Nogariya

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 06:50:11