ASP.NET Core + Identity: token 在一定时间后无法解除保护
ASP.NET Core + Identity: the token cannot be unprotected after a certain time
我们有一个 ASP.NET 核心应用程序使用身份来处理用户识别。当用户想要确认他/她的电子邮件时出现错误。
在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);
});
当发送带有 link 的电子邮件以确认用户电子邮件时,令牌是这样获取的:
var token = await userContext.GenerateEmailConfirmationTokenAsync(user);
link 工作正常,但即使 link 的持续时间验证在生产中设置为 72 小时。 link 似乎无效。
确认电子邮件时(在用户单击 link 之后),负责执行此操作的控制器基本上是在利用 ConfirmEmailAsync
方法:
var result = await UserManager.ConfirmEmailAsync(user, token);
原来 result.Succeeded
在一两个小时内是 true
(持续时间实际上有点随机,有时仅在 45 分钟后 returns false
无需对用户进行任何更改或修改)然后开始 return 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
,但我很确定这与我的问题有何关系。
我也在 MS Github
上发布了我的问题
并意识到代码库实际上缺少 key storage providers 的配置。
我最终在 Startup.cs
中的 ConfigureServices
中添加了安全路径作为数据保护配置的一部分:
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(@"our-secret-path-goes-here"));
}
从那以后,一切都很顺利。
我们有一个 ASP.NET 核心应用程序使用身份来处理用户识别。当用户想要确认他/她的电子邮件时出现错误。
在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);
});
当发送带有 link 的电子邮件以确认用户电子邮件时,令牌是这样获取的:
var token = await userContext.GenerateEmailConfirmationTokenAsync(user);
link 工作正常,但即使 link 的持续时间验证在生产中设置为 72 小时。 link 似乎无效。
确认电子邮件时(在用户单击 link 之后),负责执行此操作的控制器基本上是在利用 ConfirmEmailAsync
方法:
var result = await UserManager.ConfirmEmailAsync(user, token);
原来 result.Succeeded
在一两个小时内是 true
(持续时间实际上有点随机,有时仅在 45 分钟后 returns false
无需对用户进行任何更改或修改)然后开始 return 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
,但我很确定这与我的问题有何关系。
我也在 MS Github
上发布了我的问题并意识到代码库实际上缺少 key storage providers 的配置。
我最终在 Startup.cs
中的 ConfigureServices
中添加了安全路径作为数据保护配置的一部分:
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(@"our-secret-path-goes-here"));
}
从那以后,一切都很顺利。