Catching the System.Web/Owin Cookie Monster

CookieMonster-SittingCookies 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 heisenbugs affecting 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.
  }
}

Ewsleep-cookieThis 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 about System.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 to Response.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.

13 comments

  1. Thanks for the info Anders.

    So, did they release a fix for this?

    I’m currently having the same issue, and I wonder why.

    Thank you!

    1. No, there is no official fix. I still want to contribute to Katana with an official fix, but I haven’t had time to look into it :-(

      1. Hi Andres,

        Where have you written the above code into. for me it says there is no Oveeride to Task method

        Regards
        Sanjay

      2. You shouldn’t write the code with “Task” at all. That is just to show conceptually how the middleware works. Install the nuget package and call app.UseKentorOwinCookieSaver(); in your owin startup.

  2. I just wanted to say thank you for all of this information. You really helped me so much and made it very simple to understand. Thank you!

  3. Thanks for sharing this and for the good work. In my understanding in order to prevent this issue, I have to do both:

    – use kentor middleware
    – set Session[“Dummy”] = “Something” in the Session_Start event of my MVC application

    Is this correct?

    1. The most common reason for a conflict between owin and httpcontext cookies is how the session module works. If you plan to use Session, it might actually be enough to use the Session["Dummy"]="Something" workaround.

      If there is something else causing the conflict the OwinCookieSaver middleware should cover more scenarious – even though it also has some limitations.

  4. I decided to not use session and cookies through HttpContext.Current.Response.Cookies
    and everything work well now.

  5. Hi Anders

    Thanks for the information which makes it clear what causes the issues. Shame there is no permanent fix yet.

    I think I have a similar issue. In my web forms, it appears that Reportviewer is causing the same issue when run as indicated by users unable to login unless I restarts IIS after a report is run. Happens even in VS2013.

    Will the help resolve this or do you have any other suggestion?

    Thanks

    Jas

    1. It doesn’t make sense that the ReportViewer would experience this. The bug described here is a result of a conflict between the legacy cookie collection and owin’s cookie header and as far as I know, the ReportViewer does not use Owin.

      1. Hi Anders

        I went ahead and tried the suggestion to “Reconfigure the CookieAuthenticationMiddleware to write directly to System.Web’s cookie collection.” solution from

        http://katanaproject.codeplex.com/wikipage?title=System.Web%20response%20cookie%20integration%20issues&referringTitle=Documentation

        which resolved the issue:). I’m guessing it’s to do with the sessions that reportviewer may be using. For those using web forms and vb.net I also posted my code to my question at https://forums.asp.net/post/6114209.aspx

        Thanks

        Jas

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.