c# - Asp.Net Identity 从一个应用程序池标识生成密码重置令牌并在另一个应用程序池标识上进行验证
问题描述
我们有一个面向客户的网站和一个用于创建用户的后台。在我们的开发人员机器上的 IIS Express 上运行这两个应用程序时,使用带有密码重置的欢迎电子邮件创建新用户可以完美地工作。但是,当我们部署应用程序并且应用程序托管在具有不同应用程序池标识的不同 IIS 服务器上时,它会停止工作。
我们已经能够在同一台服务器上离线复制错误,但使用不同的应用程序池标识。如果我们切换以便应用程序在 IIS 中使用相同的应用程序池标识,那么一切都会重新开始工作。
后台:
applicationDbContext = new ApplicationDbContext();
userManager = new ApplicationUserManager(new ApplicationUserStore(applicationDbContext), applicationDbContext);
var createdUser = userManager.FindByEmail(newUser.Email);
var provider = new DpapiDataProtectionProvider("Application.Project");
userManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, int>(provider.Create("ASP.NET Identity"));
var token = userManager.GeneratePasswordResetToken(createdUser.Id);
客户门户:
var applicationDbContext = new ApplicationDbContext();
userManager = new ApplicationUserManager(new ApplicationUserStore(applicationDbContext), applicationDbContext);
var user = await userManager.FindByEmailAsync(model.Email);
if (user == null)
{
return GetErrorResult(IdentityResult.Failed());
}
var provider = new DpapiDataProtectionProvider("Application.Project");
userManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, int>(provider.Create("ASP.NET Identity"));
//This code fails with different Application Pool Identities
if (!await userManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, userManager, user))
{
return GetErrorResult(IdentityResult.Failed());
}
var result = await userManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);
IdentityResult 说Succeeded
false 但没有错误代码。无论如何,还是我们需要自己实现令牌生成和验证?
解决方案
事实证明这有点棘手。找到了一些参考资料,但它们MachineKey
在同一台服务器上使用。我希望它完全跨越不同的服务器和用户。
跨 Asp.NET Core 和 Framework 的数据保护提供程序(生成密码重置链接)
由于我没有收到错误代码,我开始在 ASP.NET Core IdentityValidateAsync
的帮助下实现自己的代码。DataProtectionTokenProvider.cs
这门课真的帮我找到了解决办法。
https://github.com/aspnet/Identity/blob/master/src/Identity/DataProtectionTokenProvider.cs
我最终遇到以下错误:
密钥在指定状态下无效。
令牌是SecurityStamp
在使用时生成的,DataProtectorTokenProvider<TUser, TKey>
但很难深入挖掘。但是,鉴于Application Pool Identity
在单个服务器上更改时验证失败,表明实际的保护机制看起来像这样:
System.Security.Cryptography.ProtectedData.Protect(userData, entropy, DataProtectionScope.CurrentUser);
鉴于如果所有站点也使用相同的Application Pool Identity
点,它也可以工作。它也可以DataProtectionProvider
与protectionDescriptor
"LOCAL=user"
.
new DataProtectionProvider("LOCAL=user")
https://docs.microsoft.com/en-us/previous-versions/aspnet/dn613280(v%3dvs.108)
在阅读DpapiDataProtectionProvider
(DPAPI 代表数据保护应用程序编程接口)时,描述说:
用于提供源自数据保护 API 的数据保护服务。当您的应用程序不是由 ASP.NET 托管并且所有进程都以相同的域身份运行时,它是数据保护的最佳选择。
Create 方法的用途描述为:
用于确保受保护数据的附加熵可能仅出于正确目的而不受保护。
https://docs.microsoft.com/en-us/previous-versions/aspnet/dn253784(v%3dvs.113)
鉴于此信息,我认为尝试使用由Microsoft
.
我最终实现了我自己的IUserTokenProvider<TUser, TKey>
,IDataProtectionProvider
并IDataProtector
把它做好了。
我选择IDataProtector
使用证书来实现,因为我可以相对容易地在服务器之间传输这些证书。我也可以从运行网站X509Store
的中获取它,因此应用程序本身中不会存储任何密钥。Application Pool Identity
public class CertificateProtectorTokenProvider<TUser, TKey> : IUserTokenProvider<TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
private IDataProtector protector;
public CertificateProtectorTokenProvider(IDataProtector protector)
{
this.protector = protector;
}
public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser, TKey> manager, TUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var ms = new MemoryStream();
using (var writer = new BinaryWriter(ms, new UTF8Encoding(false, true), true))
{
writer.Write(DateTimeOffset.UtcNow.UtcTicks);
writer.Write(Convert.ToInt32(user.Id));
writer.Write(purpose ?? "");
string stamp = null;
if (manager.SupportsUserSecurityStamp)
{
stamp = await manager.GetSecurityStampAsync(user.Id);
}
writer.Write(stamp ?? "");
}
var protectedBytes = protector.Protect(ms.ToArray());
return Convert.ToBase64String(protectedBytes);
}
public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser, TKey> manager, TUser user)
{
try
{
var unprotectedData = protector.Unprotect(Convert.FromBase64String(token));
var ms = new MemoryStream(unprotectedData);
using (var reader = new BinaryReader(ms, new UTF8Encoding(false, true), true))
{
var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
var expirationTime = creationTime + TimeSpan.FromDays(1);
if (expirationTime < DateTimeOffset.UtcNow)
{
return false;
}
var userId = reader.ReadInt32();
var actualUser = await manager.FindByIdAsync(user.Id);
var actualUserId = Convert.ToInt32(actualUser.Id);
if (userId != actualUserId)
{
return false;
}
var purp = reader.ReadString();
if (!string.Equals(purp, purpose))
{
return false;
}
var stamp = reader.ReadString();
if (reader.PeekChar() != -1)
{
return false;
}
if (manager.SupportsUserSecurityStamp)
{
return stamp == await manager.GetSecurityStampAsync(user.Id);
}
return stamp == "";
}
}
catch (Exception e)
{
// Do not leak exception
}
return false;
}
public Task NotifyAsync(string token, UserManager<TUser, TKey> manager, TUser user)
{
throw new NotImplementedException();
}
public Task<bool> IsValidProviderForUserAsync(UserManager<TUser, TKey> manager, TUser user)
{
throw new NotImplementedException();
}
}
public class CertificateProtectionProvider : IDataProtectionProvider
{
public IDataProtector Create(params string[] purposes)
{
return new CertificateDataProtector(purposes);
}
}
public class CertificateDataProtector : IDataProtector
{
private readonly string[] _purposes;
private X509Certificate2 cert;
public CertificateDataProtector(string[] purposes)
{
_purposes = purposes;
X509Store store = null;
store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certificateThumbprint = ConfigurationManager.AppSettings["CertificateThumbprint"].ToUpper();
cert = store.Certificates.Cast<X509Certificate2>()
.FirstOrDefault(x => x.GetCertHashString()
.Equals(certificateThumbprint, StringComparison.InvariantCultureIgnoreCase));
}
public byte[] Protect(byte[] userData)
{
using (RSA rsa = cert.GetRSAPrivateKey())
{
// OAEP allows for multiple hashing algorithms, what was formermly just "OAEP" is
// now OAEP-SHA1.
return rsa.Encrypt(userData, RSAEncryptionPadding.OaepSHA1);
}
}
public byte[] Unprotect(byte[] protectedData)
{
// GetRSAPrivateKey returns an object with an independent lifetime, so it should be
// handled via a using statement.
using (RSA rsa = cert.GetRSAPrivateKey())
{
return rsa.Decrypt(protectedData, RSAEncryptionPadding.OaepSHA1);
}
}
}
客户网站重置:
var provider = new CertificateProtectionProvider();
var protector = provider.Create("ResetPassword");
userManager.UserTokenProvider = new CertificateProtectorTokenProvider<ApplicationUser, int>(protector);
if (!await userManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
{
return GetErrorResult(IdentityResult.Failed());
}
var result = await userManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);
后台:
var createdUser = userManager.FindByEmail(newUser.Email);
var provider = new CertificateProtectionProvider();
var protector = provider.Create("ResetPassword");
userManager.UserTokenProvider = new CertificateProtectorTokenProvider<ApplicationUser, int>(protector);
var token = userManager.GeneratePasswordResetToken(createdUser.Id);
有关正常工作原理的更多信息DataProtectorTokenProvider<TUser, TKey>
:
推荐阅读
- ansible - 如何在 URI 模块 GET 注册循环输出中检索值?
- excel - 自动过滤一张表中的值大于另一张表中的变量
- excel - 按单元格值过滤多个数据透视表相同的工作表
- angular - AWS S3 和 Angular,文件未更新
- javascript - 如何在我的升级系统中识别 GuildMember?
- html - CSS - 如何使我的文本遵循我的边框半径
- php - PHP Post 值为空
- r - 如何让 R 停止将 R 包下载到 onedrive
- sql - 如何在不使用 IF 的情况下创建新表并检查它是否存在(如果存在则删除所有内容)
- kotlin - 如何在 Kotlin 中编写一个打开然后关闭应用程序的测试