【问题标题】:ASP.NET Core + Identity: the token cannot be unprotected after a certain timeASP.NET Core + Identity:令牌在一定时间后无法解除保护
【发布时间】: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&lt;ApplicationUser&gt; 类型,因此它对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


    【解决方案1】:

    我还在MS Github上发布了我的问题

    并意识到代码库实际上缺少key storage providers的配置。

    我最终在Startup.cs 中的ConfigureServices 中添加了一条安全路径作为数据保护配置的一部分:

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

    从那时起,一切都像魅力一样运作。

    【讨论】:

      猜你喜欢
      • 2020-09-01
      • 2023-03-21
      • 2020-12-15
      • 1970-01-01
      • 2018-08-16
      • 2016-12-11
      • 2018-09-14
      • 2015-05-07
      • 1970-01-01
      相关资源
      最近更新 更多