Cookies 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. } } |
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 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.
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!
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 :-(
Hi Andres,
Where have you written the above code into. for me it says there is no Oveeride to Task method
Regards
Sanjay
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.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!
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?
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.
I decided to not use session and cookies through HttpContext.Current.Response.Cookies
and everything work well now.
Good work. Thanks for sharing
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
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.
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