c# - ASP.NET Core + Identity:令牌在一定时间后无法解除保护
问题描述
我们有一个 ASP.NET Core 应用程序使用身份来处理用户标识。当用户想要确认他/她的电子邮件时,会发生一些错误。
在 中Startup.ConfigureServices(IServiceCollection services)
,设置是按照以下配置实现的:
services
.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.User.RequireUniqueEmail = true;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@-._0123456789";
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<ApplicationUser>)));
})
.AddEntityFrameworkStores<OurDataContext>()
.AddDefaultTokenProviders();
services.Configure<DataProtectionTokenProviderOptions>(options =>
{
options.Name = "Default";
var tokenDurationActivationLinkString = ConfigurationManager.Instance["TokenDurationInHoursOfActivationAccountLink"];
var tokenDurationActivationLink = double.Parse(tokenDurationActivationLinkString);
options.TokenLifespan = TimeSpan.FromHours(tokenDurationActivationLink);
});
当发送带有链接的电子邮件以确认用户电子邮件时,令牌是这样获取的:
var token = await userContext.GenerateEmailConfirmationTokenAsync(user);
该链接工作正常,但即使链接的持续时间验证在生产中设置为 72 小时。链接似乎无效。
在确认电子邮件时(在用户单击链接之后),负责执行此操作的控制器基本上是利用以下ConfirmEmailAsync
方法:
var result = await UserManager.ConfirmEmailAsync(user, token);
结果result.Succeeded
是true
在一两个小时内(持续时间实际上有点随机,有时它false
仅在 45 分钟后返回,用户没有任何更改或修改),然后开始返回false
。
我检查了ConfirmEmailAsync
正在做的事情,本质上是:
public virtual async Task<IdentityResult> ConfirmEmailAsync(
TUser user,
string token)
{
this.ThrowIfDisposed();
IUserEmailStore<TUser> store = this.GetEmailStore(true);
if ((object) user == null)
throw new ArgumentNullException(nameof (user));
if (!await this.VerifyUserTokenAsync(user, this.Options.Tokens.EmailConfirmationTokenProvider, "EmailConfirmation", token))
return IdentityResult.Failed(this.ErrorDescriber.InvalidToken());
await store.SetEmailConfirmedAsync(user, true, this.CancellationToken);
return await this.UpdateUserAsync(user);
}
并且VerifyUserTokenAsync
是这样实现的:
public virtual async Task<bool> VerifyUserTokenAsync(
TUser user,
string tokenProvider,
string purpose,
string token)
{
UserManager<TUser> manager = this;
manager.ThrowIfDisposed();
if ((object) user == null)
throw new ArgumentNullException(nameof (user));
if (tokenProvider == null)
throw new ArgumentNullException(nameof (tokenProvider));
if (!manager._tokenProviders.ContainsKey(tokenProvider))
throw new NotSupportedException(Microsoft.Extensions.Identity.Core.Resources.FormatNoTokenProvider((object) nameof (TUser), (object) tokenProvider));
bool result = await manager._tokenProviders[tokenProvider].ValidateAsync(purpose, token, manager, user);
if (!result)
{
ILogger logger = manager.Logger;
EventId eventId = (EventId) 9;
object obj = (object) purpose;
string userIdAsync = await manager.GetUserIdAsync(user);
logger.LogWarning(eventId, "VerifyUserTokenAsync() failed with purpose: {purpose} for user {userId}.", obj, (object) userIdAsync);
logger = (ILogger) null;
eventId = new EventId();
obj = (object) null;
}
return result;
}
通过反思,我检查了manager._tokenProviders[tokenProvider]
is 的类型,DataProtectorTokenProvider<ApplicationUser>
因此它的实现ValidateAsync
几乎是我们问题的实际内容:
public virtual async Task<bool> ValidateAsync(
string purpose,
string token,
UserManager<TUser> manager,
TUser user)
{
try
{
using (BinaryReader reader = new MemoryStream(this.Protector.Unprotect(Convert.FromBase64String(token))).CreateReader())
{
if (reader.ReadDateTimeOffset() + this.Options.TokenLifespan < DateTimeOffset.UtcNow)
return false;
string userId = reader.ReadString();
if (userId != await manager.GetUserIdAsync(user) || !string.Equals(reader.ReadString(), purpose))
return false;
string str1 = reader.ReadString();
if (reader.PeekChar() != -1)
return false;
if (!manager.SupportsUserSecurityStamp)
return str1 == "";
string str = str1;
return str == await manager.GetSecurityStampAsync(user);
}
}
catch
{
}
return false;
}
经过多次试验后,问题似乎是在某些时候对于同一个令牌,该行:
this.Protector.Unprotect(Convert.FromBase64String(token))
在某个时间点失败。
这是在检查令牌是否包含过期或未过期的日期时间偏移之前的方式UtcNow
。
的实现Protector.Unprotect
是KeyRingBasedDataProtector
类提供的,对我来说有点巫毒。
但是,错误似乎恰好发生在此处:
IAuthenticatedEncryptor encryptorByKeyId = currentKeyRing.GetAuthenticatedEncryptorByKeyId(guid, out isRevoked);
if (encryptorByKeyId == null)
{
this._logger.KeyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed(guid);
throw Error.Common_KeyNotFound(guid);
}
考虑到这是我不应该接触的东西(内部身份类别),我想知道为什么相同的令牌有时会在一段时间后无法不受保护。对我来说,这似乎真的是巫术。唯一似乎不确定的是,KeyRingProvider
但我太确定这与我的问题有什么关系。
解决方案
并意识到代码库实际上缺少密钥存储提供程序的配置。
我最终在 in 中添加了一个安全路径作为数据保护配置的ConfigureServices
一部分Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(@"our-secret-path-goes-here"));
}
从那时起,一切都像魅力一样运作。
推荐阅读
- python - 调用 Twitch 时 BeautifulSoup 不返回 html
- python - CS50 pset6 // cash.py // 未显示所需的适当数量的硬币
- javascript - 显示进度条vue flask
- c++ - c++ 代码块中的“this”关键字指的是什么?
- javascript - 如何在视口宽度减小的情况下将 a 从最大高度缩小到最小高度?
- python - django.db.utils.OperationalError:SQLite3 数据库中没有这样的表
- flutter - 类型不匹配:推断类型为 PluginRegistry 但应为 FlutterEngine
- javascript - 如何对重复对象键日值的数组进行排序并创建一个具有对象坐标的新数组,其中 y 将保存重复值
- html - 你可以有背景图像:线性渐变(到中心......);
- python - 在所有列都在字符串中的python数据框中添加密集排名