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.

  • Pingback: Sitecore 9.1 to Shibboleth via Saml2 | Paul's Sitecore Musings on 2019-11-01
  • Xavi Navarro on 2015-08-11

    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!

    • Anders Abel on 2015-08-12

      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 :-(

      • Sanjay on 2015-11-02

        Hi Andres,

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

        Regards
        Sanjay

      • Anders Abel on 2015-11-15

        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.

  • Anthony on 2015-10-09

    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!

  • Martin on 2015-11-11

    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?

    • Anders Abel on 2015-11-15

      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.

  • Alex on 2016-07-08

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

  • kurre on 2016-09-15

    Good work. Thanks for sharing

  • Jas on 2017-02-02

    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

  • Leave a Reply

    Your name as it will be displayed on the posted comment.
    Your e-mail address will not be published. It is only used if I want to get in touch during comment moderation.
    Your name will be a link to this address.
Software Development is a Job – Coding is a Passion

I'm Anders Abel, an independent systems architect and developer in Stockholm, Sweden.

profile for Anders Abel at Stack Overflow, Q&A for professional and enthusiast programmers

Code for most posts is available on my GitHub account.

Popular Posts

Archives

Series

Powered by WordPress with the Passion for Coding theme.