ASP.NET CORE 身份:在一定时间后不能保护令牌



我们有一个使用Identity来处理用户身份的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);
        });

在发送链接的电子邮件以确认用户电子邮件时,将令牌以这种方式获取:

var token = await userContext.GenerateEmailConfirmationTokenAsync(user);

该链接工作正常,但即使链接的持续时间验证设置为生产72小时。链接出现无效。

确认电子邮件(在用户单击链接之后(时,负责执行此操作的控制器基本上是利用ConfirmEmailAsync方法:

 var result = await UserManager.ConfirmEmailAsync(user, token);

事实证明,在一个或两个小时内result.Succeededtrue(实际上是有点随机的,有时它仅在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有关或未过期。

Protector.Unprotect的实现是由KeyRingBasedDataProtector类提供的,对我来说有点伏都教。

然而,似乎错误在这里完全发生:

IAuthenticatedEncryptor encryptorByKeyId = currentKeyRing.GetAuthenticatedEncryptorByKeyId(guid, out isRevoked);
if (encryptorByKeyId == null)
{
    this._logger.KeyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed(guid);
    throw Error.Common_KeyNotFound(guid);
}

考虑到这是我不应该触摸的东西(身份的内部类别(,我想知道为什么在一定持续时间后,有时同样的令牌有时会不会受到保护。对我来说似乎真的是Voodoo。唯一似乎不确定性的是KeyRingProvider,但我太确定这与我的问题有何关系。

我还在ms github上发布了我的问题

并意识到代码库实际上缺乏关键存储提供商的配置。

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

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

从那以后,一切都像魅力一样工作。

相关内容

  • 没有找到相关文章

最新更新