OWIN Self-Host CookieAuthentication & Legacy.NET 4.0 Приложение/FormsAuthenticationTicket

У меня есть два ограниченные контексты:

  • Приложение ASP.NET 4.0 MVC/WebForms
  • OWIN Self-Hosted с ASP.NET Web API 2

Первый - это хорошо зарекомендовавший себя продукт, однако его недостаток архитектуры (SmartUI) привел к созданию сложной в обслуживании кодовой базы с учетом расширяемости и масштабируемости, теперь более заметной.

Мы итерационно решая эту проблему, введя новое бэкэнд-приложение - можно разоблачить через службы OWIN/WebAPI.

В настоящее время мы стараемся использовать аутентификацию cookie в новом приложении. Первоначально я думал, что было бы просто использовать существующий файл cookie для проверки подлинности cookie, основанный на FormsAuthenticationTicket. Очевидно, это неверно.

В нашем приложении WebForms мы используем MachineKey для обозначения нашего ключа decryptionKey и validationKey для поддержки нашей веб-фермы. В .NET4 по умолчанию алгоритм AES, если я не ошибаюсь. Я предположил, что было бы просто использовать эту информацию для создания нашего собственного TicketDataFormat, если по умолчанию этого будет недостаточно.

Изученные вещи:

  • Если вы самостоятельно используете OWIN, по умолчанию TicketDataFormat использует DPAPI и не ASP.NET IIS MachineKey.
  • В .NET 4.5 Microsoft сделала конвейер MachineKey MVC/WebForms более расширяемым. Вы можете заменить его своей собственной реализацией, а не просто изменять алгоритм.

В идеале мы не хотим обновлять наше основное приложение до .NET 4.5, чтобы заменить шифрование файлов cookie. Кто-нибудь знает способ интеграции OWIN CookieAuthentication с существующим FormsAuthenticationTicket?

Мы попытались создать обычай: IDataProtector, SecureDataFormat<AuthenticationTicket>, IDataSerializer<AuthenticationTicket>. IDataSerializer будет отвечать за перевод между FormsAuthenticationTicket и AuthenticationTicket.

К сожалению, я не могу найти точную информацию о лицензировании билетов на Microsoft. Вот пример нашего примера для IDataProtector:

public byte[] Unprotect(byte[] protectedData)
{
    using (var crypto = new AesCryptoServiceProvider())
    {
        byte[] result = null;
        const Int32 blockSize = 16;
        crypto.KeySize = 192;
        crypto.Key = "<MachineKey>".ToBytesFromHexadecimal();
        crypto.IV = protectedData.Take(blockSize).ToArray();
        crypto.Padding = PaddingMode.None; // This prevents a padding exception thrown.

        using (var decryptor = crypto.CreateDecryptor(crypto.Key, crypto.IV))
        using (var msDecrypt = new MemoryStream(protectedData.Skip(blockSize).Take(protectedData.Length - blockSize).ToArray()))
        {
            using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
            {
                result = new byte[protectedData.Length - blockSize];
                csDecrypt.Read(result, 0, result.Length);
            }
        }

        return result;
    }
}

Это предполагает, что Microsoft добавляет IV в массив байтов. Это также предполагает, что MachineKey является используемым ключом AES. Тем не менее, я прочитал, что MS использует MachineKey для функции деривации ключей - принимая во внимание другие настройки, такие как AppIsolation, AppVirtualLocation, AppId и т.д. В принципе, это был снимок в темноте, и мне нужно немного света!

Наш текущий подход

В настоящее время мы создаем прототипы с использованием вторичного файла cookie, чтобы установить личность для нового контекста приложения вместе с существующим .ASPXAUTH. К сожалению, это означает, что сеанс скольжения синхронизируется как в AuthenticationTicket, так и в FormsAuthenticationTicket.

Связанные сообщения

Принятие файлов cookie с использованием форм ASP.NET в реализации SignalR с поддержкой OWIN?

Ответ 1

Возникла некоторая путаница в том, могу ли я использовать элемент <machineKey> в app.config. Дальнейшее прототипирование показало, что я могу успешно обмениваться одним FormsAuthenticationTicket между обоими ограниченными контекстами со следующим кодом.

В идеале мы создадим надлежащий сервер авторизации, чтобы включить OpenID Connect, Forms, WS-Fed и т.д., и оба приложения работают с токенами-носителями. Однако это хорошо работает в краткосрочной перспективе. Надеюсь, это поможет!

Я тестировал и проверял успешное шифрование/дешифрование с обоими приложениями, скользящее timeouthticket timeout. Вы должны помнить свои настройки web.configAuthentication для метода ticketCompatibilityMode.


appBuilder.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            CookieName = FormsAuthentication.FormsCookieName,
            CookieDomain = FormsAuthentication.CookieDomain,
            CookiePath = FormsAuthentication.FormsCookiePath,
            CookieSecure = CookieSecureOption.SameAsRequest,
            AuthenticationMode = AuthenticationMode.Active,
            ExpireTimeSpan = FormsAuthentication.Timeout,
            SlidingExpiration = true,
            AuthenticationType = "Forms",
            TicketDataFormat = new SecureDataFormat<AuthenticationTicket>(
                new FormsAuthenticationTicketSerializer(), 
                new FormsAuthenticationTicketDataProtector(), 
                new HexEncoder())
        });

<!-- app.config for OWIN Host - Only used for compatibility with existing auth ticket. -->
<authentication mode="Forms">
  <forms domain=".hostname.com" protection="All" ... />
</authentication>
<machineKey validationKey="..." decryptionKey="..." validation="SHA1" />

public class HexEncoder : ITextEncoder
{
    public String Encode(Byte[] data)
    {
        return data.ToHexadecimal();
    }

    public Byte[] Decode(String text)
    {
        return text.ToBytesFromHexadecimal();
    }
}

public class FormsAuthenticationTicketDataProtector : IDataProtector
{
    public Byte[] Protect(Byte[] userData)
    {
        FormsAuthenticationTicket ticket;
        using (var memoryStream = new MemoryStream(userData))
        {
            var binaryFormatter = new BinaryFormatter();
            ticket = binaryFormatter.Deserialize(memoryStream) as FormsAuthenticationTicket;
        }

        if (ticket == null)
        {
            return null;
        }

        try
        {
            var encryptedTicket = FormsAuthentication.Encrypt(ticket);

            return encryptedTicket.ToBytesFromHexadecimal();
        }
        catch
        {
            return null;
        }
    }

    public Byte[] Unprotect(Byte[] protectedData)
    {
        FormsAuthenticationTicket ticket;
        try
        {
            ticket = FormsAuthentication.Decrypt(protectedData.ToHexadecimal());
        }
        catch
        {
            return null;
        }

        if (ticket == null)
        {
            return null;
        }

        using (var memoryStream = new MemoryStream())
        {
            var binaryFormatter = new BinaryFormatter();
            binaryFormatter.Serialize(memoryStream, ticket);

            return memoryStream.ToArray();
        }
    }
}

public class FormsAuthenticationTicketSerializer : IDataSerializer<AuthenticationTicket>
{
    public Byte[] Serialize(AuthenticationTicket model)
    {
        var userTicket = new FormsAuthenticationTicket(
            2,
            model.Identity.GetClaimValue<String>(CustomClaim.UserName),
            new DateTime(model.Properties.IssuedUtc.Value.UtcDateTime.Ticks, DateTimeKind.Utc),
            new DateTime(model.Properties.ExpiresUtc.Value.UtcDateTime.Ticks, DateTimeKind.Utc),
            model.Properties.IsPersistent,
            String.Format(
                "AuthenticationType={0};SiteId={1};SiteKey={2};UserId={3}",
                model.Identity.AuthenticationType,
                model.Identity.GetClaimValue<String>(CustomClaim.SiteId),
                model.Identity.GetClaimValue<String>(CustomClaim.SiteKey),
                model.Identity.GetClaimValue<String>(CustomClaim.UserId)),
            FormsAuthentication.FormsCookiePath);

        using (var dataStream = new MemoryStream())
        {
            var binaryFormatter = new BinaryFormatter();
            binaryFormatter.Serialize(dataStream, userTicket);

            return dataStream.ToArray();
        }
    }

    public AuthenticationTicket Deserialize(Byte[] data)
    {
        using (var dataStream = new MemoryStream(data))
        {
            var binaryFormatter = new BinaryFormatter();
            var ticket = binaryFormatter.Deserialize(dataStream) as FormsAuthenticationTicket;
            if (ticket == null)
            {
                return null;
            }

            var userData = ticket.UserData.ToNameValueCollection(';', '=');
            var authenticationType = userData["AuthenticationType"];
            var siteId = userData["SiteId"];
            var siteKey = userData["SiteKey"];
            var userId = userData["UserId"];

            var claims = new[]
            {
                CreateClaim(CustomClaim.UserName, ticket.Name),
                CreateClaim(CustomClaim.UserId, userId),
                CreateClaim(CustomClaim.AuthenticationMethod, authenticationType),
                CreateClaim(CustomClaim.SiteId, siteId),
                CreateClaim(CustomClaim.SiteKey, siteKey)
            };

            var authTicket = new AuthenticationTicket(new UserIdentity(claims, authenticationType), new AuthenticationProperties());
            authTicket.Properties.IssuedUtc = new DateTimeOffset(ticket.IssueDate);
            authTicket.Properties.ExpiresUtc = new DateTimeOffset(ticket.Expiration);
            authTicket.Properties.IsPersistent = ticket.IsPersistent;

            return authTicket;
        }
    }

    private Claim CreateClaim(String type, String value)
    {
        return new Claim(type, value, ClaimValueTypes.String, CustomClaim.Issuer);
    }
}