Asp.Net Identity Сгенерируйте маркер сброса пароля из одного удостоверения пула приложений и проверьте на другом - PullRequest
0 голосов
/ 19 ноября 2018

У нас есть веб-сайт для клиентов и бэк-офис для создания пользователей.Создание нового пользователя с приветственным письмом со сбросом пароля работает без ошибок при запуске обоих приложений на 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, но без кода ошибки.Есть ли что-нибудь вокруг этого или нам нужно самим создавать и проверять токены?

enter image description here

1 Ответ

0 голосов
/ 20 ноября 2018

Это оказалось немного сложнее. Нашел несколько ссылок, но они использовали 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

Жетоны генерируются из 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

...