首页 > 解决方案 > ASP.NET Core + Identity:令牌在一定时间后无法解除保护

问题描述

我们有一个 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.Succeededtrue在一两个小时内(持续时间实际上有点随机,有时它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]is 的类型,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.UnprotectKeyRingBasedDataProtector提供的,对我来说有点巫毒。

但是,错误似乎恰好发生在此处

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

考虑到这是我不应该接触的东西(内部身份类别),我想知道为什么相同的令牌有时会在一段时间后无法不受保护。对我来说,这似乎真的是巫术。唯一似乎不确定的是,KeyRingProvider但我太确定这与我的问题有什么关系。

标签: c#asp.net-coreasp.net-identitytokenidentity

解决方案


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

并意识到代码库实际上缺少密钥存储提供程序的配置。

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

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

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


推荐阅读