This project has moved. For the latest updates, please go here.

Background

Katana layers the OWIN abstraction on top of the System.Web object model. For instance the OWIN key owin.ResponseHeaders returns a wrapper around the HttpContext.Response.Headers collection. This works for most headers, but it does not work for the set-cookie header in some scenarios.

Problem

In OWIN, the response headers collection is the primary storage location for response cookies. System.Web however stores response cookies in a separate HttpContext.Response.Cookies collection and then writes them out to the Response.Headers collection just before sending the response. This can cause a conflict if OWIN if both approaches are used on the same request, as the Response.Cookies collection will overwrite any cookies set via the OWIN response headers. See https://katanaproject.codeplex.com/workitem/197 for additional details.

Common Causes

In Katana it's common to write cookies out using the CookieAuthenticationMiddlware or the OwinResponse.Cookies API. In System.Web the SessionStateModule will set a response cookie if the user session is created on that request.

No cure-all

Unfortunately there is no general purpose solution to the problem. The set-cookie header from OWIN cannot be reliably re-parsed and redirected through System.Web's Response.Cookies collection. Nor can the OWIN components write directly to System.Web's Response.Cookies collection by default as this would compromise their platform independence. There are some scenario specific workarounds that can be used at the application level.

Workarounds

Workarounds fall into two categories. One is to re-configure System.Web so it avoids using the Response.Cookies collection and overwriting the OWIN cookies. The other approach is to re-configure the affected OWIN components so they write cookies directly to System.Web's Response.Cookies collection.
  • Ensure session is established prior to authentication: The conflict between System.Web and Katana cookies is per request, so it may be possible for the application to establish the session on some request prior to the authentication flow. This should be easy to do when the user first arrives, but it may be harder to guarantee later when the session or auth cookies expire and/or need to be refreshed.
  • Disable the SessionStateModule - If the application is not relying on session information, but the session module is still setting a cookie that causes the above conflict, then you may consider disabling the session state module.
  • Reconfigure the CookieAuthenticationMiddleware to write directly to System.Web's cookie collection.
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                // ...
                CookieManager = new SystemWebCookieManager()
            });

    public class SystemWebCookieManager : ICookieManager
    {
        public string GetRequestCookie(IOwinContext context, string key)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
            var cookie = webContext.Request.Cookies[key];
            return cookie == null ? null : cookie.Value;
        }

        public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);

            bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
            bool pathHasValue = !string.IsNullOrEmpty(options.Path);
            bool expiresHasValue = options.Expires.HasValue;
            
            var cookie = new HttpCookie(key, value);
            if (domainHasValue)
            {
                cookie.Domain = options.Domain;
            }
            if (pathHasValue)
            {
                cookie.Path = options.Path;
            }
            if (expiresHasValue)
            {
                cookie.Expires = options.Expires.Value;
            }
            if (options.Secure)
            {
                cookie.Secure = true;
            }
            if (options.HttpOnly)
            {
                cookie.HttpOnly = true;
            }

            webContext.Response.AppendCookie(cookie);
        }

        public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }
            
            AppendResponseCookie(
                context,
                key,
                string.Empty,
                new CookieOptions
                {
                    Path = options.Path,
                    Domain = options.Domain,
                    Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
                });
        }
    }

The Future

This issue has already been addressed in the next version of Katana (ASP.NET 5) where the storage of response cookies has been standardized to always store them in the response header collection.

Last edited Apr 3, 2015 at 11:52 PM by Tratcher, version 1

Comments

whoever Jun 23, 2016 at 2:41 AM 
Not easy to find here but it works. Thanks.