【发布时间】:2019-10-07 00:42:50
【问题描述】:
我们有一个 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(持续时间实际上有点随机,有时它仅在45分钟后返回false,用户没有任何更改或修改)然后开始返回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] 属于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有关的日期时间偏移量之前的方式。
The implementation of Protector.Unprotect is one provided by the KeyRingBasedDataProtector class 对我来说有点巫毒。
但似乎错误恰好发生在here:
IAuthenticatedEncryptor encryptorByKeyId = currentKeyRing.GetAuthenticatedEncryptorByKeyId(guid, out isRevoked);
if (encryptorByKeyId == null)
{
this._logger.KeyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed(guid);
throw Error.Common_KeyNotFound(guid);
}
考虑到这是我不应该接触的东西(身份的内部类),我想知道为什么同一令牌有时会在一段时间后无法不受保护。对我来说,这似乎真的是巫术。唯一似乎不确定的是KeyRingProvider,但我很确定这与我的问题有什么关系。
【问题讨论】:
标签: c# asp.net-core asp.net-identity token identity