ASP.NET Core 2.2-密码重置不适用于Azure(无效令牌)

时间:2019-05-21 11:38:20

标签: azure entity-framework-core asp.net-identity asp.net-core-2.2 appveyor

我有一个ASP.NET Core 2.2应用程序,在Azure Web App的多个实例上运行;它使用EF Core 2.2ASP.NET Identity

一切正常,除了密码重置流程,在该流程中,用户收到每封电子邮件的令牌链接,并且需要通过单击该链接来选择新密码。它可以在本地完美运行,但是在Azure上,它始终会失败,并显示“无效令牌”错误

令牌在必要时经过HTML编码和解码;并且我有检查以确保它们与数据库中的匹配; URL编码不是问题。

我已配置DataProtection将密钥存储到Azure Blob存储,但无济于事。 密钥已正确存储在blob存储区中,但仍然出现“无效令牌”错误

这是我在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's 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的密钥,但是结果是相同的:密钥已存储,但是在尝试重设密码Every时仍然收到Invalid token消息。单。时间。

请求密码重置方法

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本身是令牌验证,并且失败。


更新

好的,情节变浓了。

如果我从Visual Studio 2019将应用程序发布到Azure,则可以毫无问题地重置密码。 当我通过AppVeyor发布时(这是在提交GitHub之后发生的,并且是所需选项),给定的令牌与数据库中的令牌不匹配,即,以下代码失败:

var resetRequest = await _context.PasswordResetRequests
    .AsNoTracking()
    .FirstOrDefaultAsync(x => x.Token == token);

所以我猜我的dotnet publish命令不完全正确吗?

从AppVeyor发布命令

- dotnet publish --output AppOutput\DeployWeb --configuration Release --framework netcoreapp2.2 --runtime win-x86 --self-contained false

deploy:
- provider: WebDeploy
  server: https://xxxxxx.scm.azurewebsites.net/msdeploy.axd
  website: xxxxxx__test
  username: '$xxxxxx__test'
  password:
    secure: BlaBlaBla123123==
  do_not_use_checksum: true
  remove_files: true
  app_offline: true
  aspnet_core: true
  aspnet_core_force_restart: true
  artifact: Web
  on:
    branch: master
    configuration: Release

此项目的Web Deploy.pubxml

为进行比较,这是Visual Studio 2019使用的部署设置

<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>MSDeploy</WebPublishMethod>
    <ResourceId>/subscriptions/113d52ca-4022-47bd-948e-a8080cd1fda1/resourceGroups/XXXXXX/providers/Microsoft.Web/sites/xxxxxx/slots/test</ResourceId>
    <ResourceGroup>XXXXXX</ResourceGroup>
    <PublishProvider>AzureWebSite</PublishProvider>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish>http://xxxxxx.azurewebsites.net</SiteUrlToLaunchAfterPublish>
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <ProjectGuid>7bcb4479-aa7d-47a1-a376-93acf08d7500</ProjectGuid>
    <MSDeployServiceURL>xxxxxx.scm.azurewebsites.net:443</MSDeployServiceURL>
    <DeployIisAppPath>xxxxxx</DeployIisAppPath>
    <RemoteSitePhysicalPath />
    <SkipExtraFilesOnServer>True</SkipExtraFilesOnServer>
    <MSDeployPublishMethod>WMSVC</MSDeployPublishMethod>
    <EnableMSDeployBackup>True</EnableMSDeployBackup>
    <UserName>$xxxxxx__test</UserName>
    <_SavePWD>True</_SavePWD>
    <_DestinationType>AzureWebSite</_DestinationType>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <SelfContained>false</SelfContained>
    <_IsPortable>true</_IsPortable>
    <InstallAspNetCoreSiteExtension>False</InstallAspNetCoreSiteExtension>
    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
  </PropertyGroup>
</Project>

和FWIW,这是我的global.json

{
    "sdk": {
        "version": "2.2.102"
    }
}

我意识到这个问题已经超出了ASP.NET Core Identity的范畴,但是仍然可以得到任何帮助

1 个答案:

答案 0 :(得分:0)

它表明您的令牌是通过其他方式生成的。 你可以试试这个吗? 生成新令牌:

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;
        }