Asp.net

OWIN 自託管 CookieAuthentication 和舊版 .NET 4.0 應用程序/FormsAuthenticationTicket

  • July 6, 2015

我有兩個有界上下文

  1. ASP.NET 4.0 MVC/WebForms 應用程序
  2. OWIN 自託管 w/ ASP.NET Web API 2

前者是現有的成熟產品,但是,它缺乏架構 (SmartUI) 導致程式碼庫難以維護,對可擴展性和可伸縮性的擔憂現在更加明顯。

我們通過引入一個新的後端應用程序來迭代地解決這個問題 - 可通過 OWIN/WebAPI 服務公開。

目前,我們只希望在新應用程序中利用 cookie 身份驗證。最初,我認為使用基於 FormsAuthenticationTicket 的現有 cookie 身份驗證/驗證會輕而易舉。顯然這不是真的。

在我們的 WebForms 應用程序中,我們使用 MachineKey 來指定我們的解密密鑰和驗證密鑰來支持我們的網路農場。在 .NET4 中,如果我沒記錯的話,預設算法是 AES。我認為如果預設值不夠,利用這些資訊來建構我們自己的 TicketDataFormat 會很簡單。

首先學到的東西:

  • 如果您使用 OWIN 自託管,則預設 TicketDataFormat 使用 DPAPI 而不是ASP.NET IIS MachineKey。
  • 在 .NET 4.5 中,Microsoft 使 MVC/WebForms MachineKey 管道更具可擴展性。您可以用自己的實現替換它,而不僅僅是更改算法。

理想情況下,我們不打算將我們的主應用程序更新到 .NET 4.5 來取代 cookie 加密。有誰知道將 OWIN 的 CookieAuthentication 與現有的 FormsAuthenticationTicket 集成的方法?

我們嘗試創建 custom: 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 中保持會話滑動同步。

相關文章

在 OWIN 託管的 SignalR 實現中接受 ASP.NET 表單身份驗證 cookie?

關於是否可以在 app.config 中使用 <machineKey> 元素,最初存在一些混淆。進一步的原型設計表明,我可以使用以下程式碼在兩個有界上下文之間成功共享一個 FormsAuthenticationTicket。

理想情況下,我們將實施一個適當的授權伺服器來啟用 OpenID Connect、Forms、WS-Fed 等,並讓兩個應用程序都使用不記名令牌進行操作。然而,這在短期內運作良好。希望這可以幫助!

我已經測試並驗證了兩個應用程序的成功加密/解密,formsauthticket 超時滑動。您應該注意ticketCompatibilityMode 的web.config formsAuthentication 設置。


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&lt;AuthenticationTicket&gt;(
               new FormsAuthenticationTicketSerializer(), 
               new FormsAuthenticationTicketDataProtector(), 
               new HexEncoder())
       });

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

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&lt;AuthenticationTicket&gt;
{
   public Byte[] Serialize(AuthenticationTicket model)
   {
       var userTicket = new FormsAuthenticationTicket(
           2,
           model.Identity.GetClaimValue&lt;String&gt;(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&lt;String&gt;(CustomClaim.SiteId),
               model.Identity.GetClaimValue&lt;String&gt;(CustomClaim.SiteKey),
               model.Identity.GetClaimValue&lt;String&gt;(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);
   }
}

引用自:https://stackoverflow.com/questions/23251550