Catching the System.Web/Owin Cookie Monster
Join the DZone community and get the full member experience.
Join For FreeCookies set through the Owin API sometimes mysteriously disappear. The problem is that deep within System.Web
, there has been a cookie monster sleeping since the dawn of time (well, at least since .NET and System.Web
was released). The monster has been sleeping for all this time, but now, with the new times arriving with Owin, the monster is awake. Being starved from the long sleep, it eats cookies set through the Owin API for breakfast. Even if the cookies are properly set, they are eaten by the monster before the Set-Cookie
headers are sent out to the client browser. This typically results in heisenbugsaffecting sign in and sign out functionality.
TL;DR
The problem is that System.Web
has its own master source of cookie information and that isn’t the Set-Cookie
header. Owin only knows about the Set-Cookie
header. A workaround is to make sure that any cookies set by Owin are also set in the HttpContext.Current.Response.Cookies
collection.
This is exactly what my Kentor.OwinCookieSaver middleware does. It should be added in to the Owin pipeline (typically in Startup.Auth.cs
), before any middleware that handles cookies.
app.UseKentorOwinCookieSaver();
The cookie saver middleware preserves cookies set by other middleware. Unfortunately it is not reliable for cookies set by the application code (such as in MVC Actions). The reason is that the System.Web
cookie handling code might be run after the application code, but before the middleware. For cookies set by the application code, the workaround by storing a dummy value in the sessions is more safe.
The Reason
The System.Web
API has been around since the dawn of .NET. Back then, it was tightly coupled to IIS and the one and only API for web applications. As the one and only, it could assume that it was the master of all information. In HttpResponse.cs there is a check whether the cookie collection is changed (adding cookies doesn’t count as a change) and in that case it wipes the existing Set-Cookie
header.
if (_cookies.Changed || needToReset) { // delete all set cookie headers headers.Remove("Set-Cookie"); // write all the cookies again for(int c = 0; c < _cookies.Count; c++) { // Write the cookies, code removed for brevity. } }
This is what a sleeping cookie monster looks like in the code. It’s sleeping, because there’s still nothing questioning the cooke collection being the master.
But that all changes when Owin was introduced. The Owin API knows nothing aboutSystem.Web.HttpContext
. In fact, that’s kind of the point with Owin, to break the dependency between .NET web applications and IIS. In Katana, cookies are (in most cases) added by a call toResponse.Cookies.Append()
which adds a new Set-Cookie
header.
Effectively we have a system with conflicting views on where the master information is stored. Owin considers the actual header to be the master while System.Web
considers the response cookie collection to be the master. Having conflicting masters is never a good idea. This is a known issue for Katana, classified as “High Impact”.
The Workaround Middleware
The conflict between the two distance relatives System.Web
and Owin is a typical family conflict. The older one is wrong, but won’t change views just because someone young appears with new facts. When mediating such a conflict it’s usually easiest to get the younger generation to work around the older. Changing System.Web
is not feasible, so the focus has to be on Owin.
The workaround middleware I’ve created checks the Set-Cookie
header and syncs its contents back to the cookie collection. By putting it before any cookie handling middleware in the pipeline it can save the cookies from the monster, before System.Web
deletes the header.
The core function of the workaround middleware is the Invoke
method.
public async override Task Invoke(IOwinContext context) { await Next.Invoke(context); var setCookie = context.Response.Headers.GetValues("Set-Cookie"); if(setCookie != null) { var cookies = CookieParser.Parse(setCookie); foreach(var c in cookies) { if(!HttpContext.Current.Response.Cookies.AllKeys.Contains(c.Name)) { HttpContext.Current.Response.Cookies.Add(c); } } } }
The logic is quite straight forward. Parse each Set-Cookie
header into a HttpCookie
object and ensure that it is present in the response cookie collection. For the applications we’ve tested it works, but it is a workaround and not a real fix. Please leave a comment below if you find situation where this workaround does not work. That’s very valuable information for others having the same issue.
A Permanent Fix
I’m also looking into fixing this permanently by contributing to the System.Web
host in Katana. The fix there would be to directly intercept any calls to set the Set-Cookie
header and add them to the cookie to the collection too. That should be a much more stable solution as it prevents the problem rather than trying to fix it afterwards.
Published at DZone with permission of Anders Abel, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments