ASP.NET Core 2.2 - сброс пароля не работает в Azure (неверный токен)

У меня есть приложение ASP.NET Core 2.2, работающее в нескольких экземплярах в веб-приложении Azure; он использует EF Core 2.2 и ASP.NET Identity.

Все работает отлично, за исключением потока сброса пароля, где пользователь получает ссылку с токеном по электронной почте и должен выбрать новый пароль, нажав на эту ссылку. Он отлично работает локально, но в Azure он всегда завершается с ошибкой "Invalid Token".

Токены HTML кодируются и декодируются по мере необходимости; и у меня есть проверки, чтобы убедиться, что они совпадают с данными в базе данных; Кодировка URL не является проблемой.

Я настроил DataProtection для хранения ключей в хранилище DataProtection Azure, но безрезультатно. Все ключи хранятся в хранилище BLOB-объектов, но я все еще получаю ошибку "Invalid Token".

Вот мои настройки на Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // This needs to happen before "AddMvc"
    // Code for this method shown below
    AddDataProtecion(services);

    services.AddDbContext<MissDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    var sp = services.BuildServiceProvider();

    services.ConfigureApplicationCookie(x =>
    {
        x.Cookie.Name = ".MISS.SharedCookie";
        x.ExpireTimeSpan = TimeSpan.FromHours(8);
        // We need to set the cookie DataProtectionProvider to ensure it will get stored in the azure blob storage
        x.DataProtectionProvider = sp.GetService<IDataProtectionProvider>();
    });

    services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddEntityFrameworkStores<MissDbContext>()
        .AddDefaultTokenProviders();


    // https://tech.trailmax.info/2017/07/user-impersonation-in-asp-net-core/
    services.Configure<SecurityStampValidatorOptions>(options => 
    {
        options.ValidationInterval = TimeSpan.FromMinutes(10);
        options.OnRefreshingPrincipal = context =>
        {
            var originalUserIdClaim = context.CurrentPrincipal.FindFirst("OriginalUserId");
            var isImpersonatingClaim = context.CurrentPrincipal.FindFirst("IsImpersonating");
            if (isImpersonatingClaim?.Value == "true" && originalUserIdClaim != null)
            {
                context.NewPrincipal.Identities.First().AddClaim(originalUserIdClaim);
                context.NewPrincipal.Identities.First().AddClaim(isImpersonatingClaim);
            }
            return Task.FromResult(0);
        };
    });

     // some more initialisations here
}

А вот и метод AddDataProtection:

/// <summary>
/// Add Data Protection so that cookies don't get invalidated when swapping slots.
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
void AddDataProtecion(IServiceCollection services)
{
    var sasUrl = Configuration.GetValue<string>("DataProtection:SaSUrl");
    var containerName = Configuration.GetValue<string>("DataProtection:ContainerName");
    var applicationName = Configuration.GetValue<string>("DataProtection:ApplicationName");
    var blobName = Configuration.GetValue<string>("DataProtection:BlobName");
    var keyIdentifier = Configuration.GetValue<string>("DataProtection:KeyVaultIdentifier");

    if (sasUrl == null || containerName == null || applicationName == null || blobName == null)
        return;

    var storageUri = new Uri($"{sasUrl}");

    var blobClient = new CloudBlobClient(storageUri);

    var container = blobClient.GetContainerReference(containerName);
    container.CreateIfNotExistsAsync().GetAwaiter().GetResult();

    applicationName = $"{applicationName}-{Environment.EnvironmentName}";
    blobName = $"{applicationName}-{blobName}";

    services.AddDataProtection()
        .SetApplicationName(applicationName)
        .PersistKeysToAzureBlobStorage(container, blobName);
}

Я также пытался сохранить ключи к DbContext, но результат тот же: ключи сохранены, но я все равно получаю сообщение о Invalid token при попытке сброса пароля, Every. Не замужем. Время.

метод запроса сброса пароля

public async Task RequestPasswordReset(string emailAddress, string ip, Request httpRequest) 
{
    var user = await _userManager.FindByEmailAsync(emailAddress);

    var resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);

    var resetRequest = new PasswordResetRequest
    {
        CreationDate = DateTime.Now,
        ExpirationDate = DateTime.Now.AddDays(1),
        UserId = user.Id,
        Token = resetToken,
        IP = ip
    };

    _context.PasswordResetRequests.Add(resetRequest);
    await _context.SaveChangesAsync();

    await SendPasswordResetEmail(user, resetRequest, httpRequest);
}

Метод сброса пароля

Как только пользователь запрашивает сброс пароля, он получает электронное письмо со ссылкой и токеном; вот как я пытаюсь сбросить пароль пользователя после того, как пользователь нажимает на эту ссылку:

public async Task<IdentityResult> ResetPassword(string token, string password) 
{
    // NO PROBLEM HERE - The received token matches with the one in the Db
    var resetRequest = await _context.PasswordResetRequests
        .AsNoTracking()
        .FirstOrDefaultAsync(x => x.Token == token);

    var user = await _userManager.FindByIdAsync(resetRequest.UserId);

    // PROBLEM - This method returns "Invalid Token"
    var result = await _userManager.ResetPasswordAsync(user, resetRequest.Token, password);

    if (result.Succeeded)
        await SendPasswordChangedEmail(user);

    return result;
}

Как я заявляю в комментариях к коду, токен, полученный в запросе, совпадает с токеном, сгенерированным в базе данных, но ResetPasswordAsync выполняет собственную проверку токена, и это не удается.

Любая помощь все равно будет оценена

Ответ 1

Это швы, ваш токен был сгенерирован другим способом. Не могли бы вы попробовать это? создать новый токен:

var code = await UserManager.GeneratePasswordResetTokenAsync(resetRequest.UserId);

и сбросить пароль:

var resetResult = await userManager.ResetPasswordAsync(resetRequest.UserId, code, password);

другой случай - неправильная кодировка HTML для токена:

token = HttpUtility.UrlDecode(token) ;

Следующий случай - userManager должен быть одноэлементным (или хотя бы классом tokenProvider) для каждого запроса.

это ссылка на исходный код https://github.com/aspnet/Identity/blob/rel/2.0.0/src/Microsoft.Extensions.Identity.Core/UserManager.cs#L29

ручная обработка токенов в случае, если для экземпляров токенов существуют разные экземпляры из-за сохранения токенов в закрытой переменной:

private readonly Dictionary<string, IUserTwoFactorTokenProvider<TUser>> _tokenProviders =
            new Dictionary<string, IUserTwoFactorTokenProvider<TUser>>();

Следующий код может быть реализован:

  public override async Task<bool> VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, string token)
        {
            ThrowIfDisposed();
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }
            if (tokenProvider == null)
            {
                throw new ArgumentNullException(nameof(tokenProvider));
            }
//should be overriden
// if (!_tokenProviders.ContainsKey(tokenProvider))
//           {
//              throw new 
//NotSupportedException(string.Format(CultureInfo.CurrentCulture, 
//Resources.NoTokenProvider, tokenProvider));
//          }
// Make sure the token is valid
//        var result = await _tokenProviders[tokenProvider].ValidateAsync(purpose, token, this, user);

  //          if (!result)
  //        {
  //          Logger.LogWarning(9, "VerifyUserTokenAsync() failed with //purpose: {purpose} for user {userId}.", purpose, await GetUserIdAsync(user));
       //    }
var resetRequest = await _context.PasswordResetRequests
        .AsNoTracking()
        .FirstOrDefaultAsync(x => x.Token == token);
            if (resetRequest == null )
            {
                return IdentityResult.Failed(ErrorDescriber.InvalidToken());
            }

            // Make sure the token is valid
            var result = resetRequest.IsValid();

            if (!result)
            {
                Logger.LogWarning(9, "VerifyUserTokenAsync() failed with purpose: {purpose} for user {userId}.", purpose, await GetUserIdAsync(user));
            }
            return result;
        }