У нас есть приложение 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]
относится к типу 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
, но я слишком уверен, как это связано с моей проблемой.