Ser*_*eit 16 azure asp.net-identity entity-framework-core appveyor asp.net-core-2.2
I have an ASP.NET Core 2.2 application running on multiple instances on an Azure Web App; it uses EF Core 2.2 and ASP.NET Identity.
Everything works fine except the Password Reset flow where a user receives a link with token per e-mail and needs to choose a new password by clicking on that link. It works perfectly locally, but on Azure it always fails with an "Invalid Token" error.
The tokens are HTML encoded and decoded as necessary; and I have checks in place to ensure they match those on the database; URL encoding is not the issue.
I've configured DataProtection to store the keys to an Azure Blob storage, but to no avail.
The keys are stored in the blob store all right, but I still get an "Invalid Token" error.
Here's my set up on 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
}
Run Code Online (Sandbox Code Playgroud)
And here is the AddDataProtection method:
/// <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);
}
Run Code Online (Sandbox Code Playgroud)
I've also tried persisting the keys to the DbContext, but the result is the same: keys are stored, but I still get anInvalid token message when attempting a password reset, Every. Single. Time.
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);
}
Run Code Online (Sandbox Code Playgroud)
Once the user requests a password reset, they receive an e-mail with a link and a token; here's how I attempt to reset the user's password after the user clicks on that link:
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;
}
Run Code Online (Sandbox Code Playgroud)
As I state in the code comments, the token received in the request matches the one generated in the database, but ResetPasswordAsync does it's own token validation, and that fails.
Any help would still be appreciated
看来您的令牌是用不同的方式生成的。你能试试这个吗?生成新令牌:
var code = await UserManager.GeneratePasswordResetTokenAsync(resetRequest.UserId);
Run Code Online (Sandbox Code Playgroud)
并重置密码:
var resetResult = await userManager.ResetPasswordAsync(resetRequest.UserId, code, password);
Run Code Online (Sandbox Code Playgroud)
另一种情况是令牌的 HTML 编码不正确:
token = HttpUtility.UrlDecode(token) ;
Run Code Online (Sandbox Code Playgroud)
下一种情况是 userManager 对于每个请求都必须是单例(或至少是 tokenProvider 类)。
手动令牌处理,以防由于将令牌存储到私有变量而导致令牌提供者的不同实例:
private readonly Dictionary<string, IUserTwoFactorTokenProvider<TUser>> _tokenProviders =
new Dictionary<string, IUserTwoFactorTokenProvider<TUser>>();
Run Code Online (Sandbox Code Playgroud)
可能会实现下一个代码:
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;
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
539 次 |
| 最近记录: |