ASP.NET Core + Identity: токен не может быть незащищен через определенное время - PullRequest
0 голосов
/ 20 мая 2019

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

1 Ответ

0 голосов
/ 22 мая 2019

Я также разместил свои проблемы на MS Github

И понял, что кодовой базе фактически не хватает конфигурации для ключевых провайдеров хранения .

Я закончил тем, что добавил безопасный путь как часть конфигурации защиты данных в ConfigureServices в Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(@"our-secret-path-goes-here"));
}

И с тех пор все работает как шарм.

...