Это оказалось немного сложнее. Нашел несколько ссылок, но они использовали MachineKey
на том же сервере. Я хотел, чтобы это было на разных серверах и пользователях в целом.
Поставщик защиты данных в Asp.NET Core и Framework (сгенерировать ссылку для сброса пароля)
Поскольку я не получил код ошибки, я начал с реализации собственной ValidateAsync
с помощью DataProtectionTokenProvider.cs
для ASP.NET Core Identity. Этот класс действительно помог мне найти решение.
https://github.com/aspnet/Identity/blob/master/src/Identity/DataProtectionTokenProvider.cs
Я закончил со следующей ошибкой:
Ключ недействителен для использования в указанном состоянии.
![enter image description here](https://i.stack.imgur.com/ZDkpr.png)
Жетоны генерируются из 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)
https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotector?view=netframework-4.7.2
https://docs.microsoft.com/en-us/uwp/api/windows.security.cryptography.dataprotection.dataprotectionprovider
При чтении о 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>
:
https://stackoverflow.com/a/53390287/3850405