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
相关产品推荐
相关产品推荐

