Asp.Net 身份从一个应用程序池身份生成密码重置令牌并在另一个上验证
Asp.Net Identity Generate Password Reset Token from one Application Pool Identity and Verify on another
我们有一个面向客户的网站和一个用于创建用户的后台办公室。当 运行 在我们的开发人员计算机上的 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
错误但没有错误代码。无论如何,还是我们需要自己实现令牌生成和验证?
这有点棘手。找到了一些参考资料,但他们在同一台服务器上使用了 MachineKey
。我希望它能够跨越不同的服务器和用户。
Data Protection provider across Asp.NET Core and Framework (generate password reset link)
因为我没有收到错误代码,所以我开始在 DataProtectionTokenProvider.cs
的帮助下为 ASP.NET 核心身份实现我自己的 ValidateAsync
。这个 Class 真的帮我找到了解决办法。
https://github.com/aspnet/Identity/blob/master/src/Identity/DataProtectionTokenProvider.cs
我遇到了以下错误:
Key not valid for use in specified state.
令牌是在使用 DataProtectorTokenProvider<TUser, TKey>
时从 SecurityStamp
生成的,但很难深入挖掘。然而,鉴于在单个服务器上更改 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 代表数据保护应用程序编程接口)的描述时说:
Used to provide the data protection services that are derived from the
Data Protection API. It is the best choice of data protection when you
application is not hosted by ASP.NET and all processes are running as
the same domain identity.
Create方法的用途描述为:
Additional entropy used to ensure protected data may only be
unprotected for the correct purposes.
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>
工作原理的更多信息:
我们有一个面向客户的网站和一个用于创建用户的后台办公室。当 运行 在我们的开发人员计算机上的 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
错误但没有错误代码。无论如何,还是我们需要自己实现令牌生成和验证?
这有点棘手。找到了一些参考资料,但他们在同一台服务器上使用了 MachineKey
。我希望它能够跨越不同的服务器和用户。
Data Protection provider across Asp.NET Core and Framework (generate password reset link)
因为我没有收到错误代码,所以我开始在 DataProtectionTokenProvider.cs
的帮助下为 ASP.NET 核心身份实现我自己的 ValidateAsync
。这个 Class 真的帮我找到了解决办法。
https://github.com/aspnet/Identity/blob/master/src/Identity/DataProtectionTokenProvider.cs
我遇到了以下错误:
Key not valid for use in specified state.
令牌是在使用 DataProtectorTokenProvider<TUser, TKey>
时从 SecurityStamp
生成的,但很难深入挖掘。然而,鉴于在单个服务器上更改 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 代表数据保护应用程序编程接口)的描述时说:
Used to provide the data protection services that are derived from the Data Protection API. It is the best choice of data protection when you application is not hosted by ASP.NET and all processes are running as the same domain identity.
Create方法的用途描述为:
Additional entropy used to ensure protected data may only be unprotected for the correct purposes.
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>
工作原理的更多信息: