I have two bounded contexts:
The former is an existing well-established product, however, its lack of architecture (SmartUI) has led to an difficult-to-maintain codebase with concerns of extensibility and scalability now more glaringly visible.
We are iteratively addressing this issue by introducing a new backend application - exposable via OWIN/WebAPI services.
Currently we're only looking to leverage cookie authentication in the new application. Originally, I thought it would be a breeze to use existing cookie auth/validation based upon FormsAuthenticationTicket. Evidently this is not true.
In our WebForms application, we make use of MachineKey to designate our decryptionKey and validationKey to support our web farm. In .NET4, the default algorithm is AES if I'm not mistaken. I assumed it would be simple to leverage this information to build our own TicketDataFormat if the default wouldn't suffice.
First things learned:
Ideally, we're not looking to update our main application to .NET 4.5 to replace cookie encryption. Does anyone know of a way to integrate OWIN's CookieAuthentication with an existing FormsAuthenticationTicket?
We tried creating custom:
IDataProtector
, SecureDataFormat<AuthenticationTicket>
, IDataSerializer<AuthenticationTicket>
implementations.
The IDataSerializer would be responsible for translation between FormsAuthenticationTicket and AuthenticationTicket.
Unfortunately, I can't find accurate information regarding Microsoft's ticket encrpytion. Here is our example idea for 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;
}
}
This assumes Microsoft prepends the IV to the byte array. This also assumes the MachineKey is the AES key used. However, I have read that MS uses the MachineKey for a key derivation function - taking into account other settings like AppIsolation, AppVirtualLocation, AppId, etc. Basically, this was a shot in the dark and I need some light!
Our Current Approach
We're currently prototyping using a secondary cookie to establish identity for the new application context alongside the existing .ASPXAUTH. Unfortunately, this means keeping session sliding in sync in both AuthenticationTicket and FormsAuthenticationTicket.
Related Posts
Accepting ASP.NET Forms Authentication cookies in an OWIN-hosted SignalR implementation?
NET (OWIN) defines an abstraction between . NET web servers and web applications. OWIN decouples the web application from the server, which makes OWIN ideal for self-hosting a web application in your own process, outside of IIS.
ASP.NET Web API can be either be hosted in IIS or in a separate host process. The former approach is usually appropriate when the Web API is part of a web application and one or more web applications are going to consume it.
With that we have now built a Web API service that runs without IIS and created a standalone HTML page that can use it. We could now host this HTML page on IIS, Nginx, Apache or whatever Web Server we choose, it would still be able to communicate with the Web API service.
There was some initial confusion on whether I could use the <machineKey> element within app.config. Further prototyping has shown that I can successfully share a single FormsAuthenticationTicket between both bounded contexts with the following code.
Ideally, we will implement a proper authorization server to enable OpenID Connect, Forms, WS-Fed, etc and have both applications operate off bearer tokens. However, this is working nicely in the short-term. Hope this helps!
I have tested and verified successful encryption/decryption with both applications, sliding of formsauthticket timeout. You should be mindful of your web.config formsAuthentication setting for 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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With