OWIN 自託管 CookieAuthentication 和舊版 .NET 4.0 應用程序/FormsAuthenticationTicket
我有兩個有界上下文:
- ASP.NET 4.0 MVC/WebForms 應用程序
- 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 中保持會話滑動同步。
相關文章
關於是否可以在 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<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); } }