5

I am trying to use a custom ITempDataProvider provider to store TempData in a browser's cookie instead of session state. However, everything works fine except that I am unable to remove the cookie from the Response stream after reading it.

Any ideas?
Thanks!

public class CookieTempDataProvider : ITempDataProvider
    {
        internal const string TempDataCookieKey = "__ControllerTempData";
        HttpContextBase _httpContext;

        public CookieTempDataProvider(HttpContextBase httpContext)
        {
            if (httpContext == null)
            {
                throw new ArgumentNullException("httpContext");
            }
            _httpContext = httpContext;
        }

        public HttpContextBase HttpContext
        {
            get
            {
                return _httpContext;
            }
        }

        protected virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
        {
            HttpCookie cookie = _httpContext.Request.Cookies[TempDataCookieKey];
            if (cookie != null && !string.IsNullOrEmpty(cookie.Value))
            {
                IDictionary<string, object> deserializedTempData = DeserializeTempData(cookie.Value);

                // Remove cookie                
                cookie.Expires = DateTime.MinValue;
                cookie.Value = string.Empty;
                _httpContext.Request.Cookies.Remove(TempDataCookieKey);

                if (_httpContext.Response != null && _httpContext.Response.Cookies != null)
                {
                    HttpCookie responseCookie = _httpContext.Response.Cookies[TempDataCookieKey];
                    if (responseCookie != null)
                    {
                        // Remove cookie
                        cookie.Expires = DateTime.MinValue;
                        cookie.Value = string.Empty;
                        _httpContext.Response.Cookies.Remove(TempDataCookieKey);

                    }
                }

                return deserializedTempData;
            }

            return new Dictionary<string, object>();
        }

        protected virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
        {

            string cookieValue = SerializeToBase64EncodedString(values);  
            var cookie = new HttpCookie(TempDataCookieKey);
            cookie.HttpOnly = true;
            cookie.Value = cookieValue;

            _httpContext.Response.Cookies.Add(cookie);
        }

        public static IDictionary<string, object> DeserializeTempData(string base64EncodedSerializedTempData)
        {
            byte[] bytes = Convert.FromBase64String(base64EncodedSerializedTempData);
            var memStream = new MemoryStream(bytes);
            var binFormatter = new BinaryFormatter();
            return binFormatter.Deserialize(memStream, null) as IDictionary<string, object> /*TempDataDictionary : This returns NULL*/;
        }

        public static string SerializeToBase64EncodedString(IDictionary<string, object> values)
        {
            MemoryStream memStream = new MemoryStream();
            memStream.Seek(0, SeekOrigin.Begin);
            var binFormatter = new BinaryFormatter();
            binFormatter.Serialize(memStream, values);
            memStream.Seek(0, SeekOrigin.Begin);
            byte[] bytes = memStream.ToArray();
            return Convert.ToBase64String(bytes);
        }

        IDictionary<string, object> ITempDataProvider.LoadTempData(ControllerContext controllerContext)
        {
            return LoadTempData(controllerContext);
        }

        void ITempDataProvider.SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
        {
            SaveTempData(controllerContext, values);
        }
    }
  • 2
    Be really careful what you store in the client's cookie. Generally it's a bad idea to keep stuff there. Not trying to judge, but it's doing this sort of stuff that led to [DotNetNuke suffering badly](http://www.youtube.com/watch?v=yghiC_U2RaM) from the recent [oracle padding exploit](http://weblogs.asp.net/scottgu/archive/2010/09/18/important-asp-net-security-vulnerability.aspx). – Matt Kocaj Sep 28 '10 at 03:28
  • Agree with @cottsak. Also, it's sent with *every request.* Every image. Every script. Every... – Craig Stuntz Sep 28 '10 at 13:12
  • @cottsak and @Craig: I am trying to store only display notifications like "your message has been sent". I am not storing any sensitive data in there. –  Sep 29 '10 at 02:17
  • 1
    @Nazaf: Why do you want to store messages in a cookie? That's not what cookies are for. Can't you just return the message as part of the regular response/page? Or get the message as a response to a ajax request? – Matt Kocaj Sep 29 '10 at 02:51
  • @cottsak Because I need the message to persist across multiple page requests which is what TempData is exactly for. By default, TempData uses Session State which is disabled on my site. so the only performant way is using a cookie. I hope you have an idea of what I need now!! –  Sep 29 '10 at 03:27
  • Perhaps you could persist the message in the database somewhere as opposed to the cookie? Creating the behavior of the Session? – Matt Kocaj Sep 30 '10 at 00:24
  • @cottsak Database is very expensive for this simple operation! –  Sep 30 '10 at 04:33
  • Maybe you're right. Maybe there are perfectly good reasons to put certain data into a cookie. But not like this IMHO! You've implemented the whole temp data provider. God help you if someone else uses that code on your project and they don't know what to/not to save in the cookie. It's just such a bad idea to be saving stuff in cookies hey. And if ppl are putting TempData-like things in there then you potentially have real big problems. – Matt Kocaj Sep 30 '10 at 06:50
  • 1
    Never trust user input. Never trust requests. Never trust content from the client.. especially cookies. – Matt Kocaj Sep 30 '10 at 06:51
  • Your DB is slower than sending a request over the wire? You should focus on solving the real problems. Also, why turn off session state and then immediately reinvent it? – Craig Stuntz Sep 30 '10 at 14:22
  • @cottsak @Craig Stuntz Imagine a 1000 users receiving some notification from DB. you waste 1000 roundtrips for showing some text. Session State can consume a lot of memory RAM on server. Turning it off has really improved performance on my site. –  Oct 02 '10 at 03:20
  • @cottsak I am only storing text in that cookie, nothing else. It will be HTML encoded and shown to the same user. For sensitive data, I store them in the Cache on the server side. –  Oct 02 '10 at 03:25

3 Answers3

4

There is a better solution by Brock Allen on GitHub that uses encryption, 2 forms of serialization, and compression to protect and optimize the cookies.

https://github.com/brockallen/CookieTempData

Here is a link to the blog about it:

http://brockallen.com/2012/06/11/cookie-based-tempdata-provider/

He also has a good technique using IControllerFactory to ensure every controller is supplied with an instance of ITempDataProvider.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
3

Hi I too had the same issue and it was an issue with the implementation of CookieTempDataProvider.

So I modified the code a bit and now it works perfectly.

When it reads the data from the cookie, it removes it from both the request and response. But add another cookie with an empty value in the SaveData function which is called when the request processing is completed.

Points to note : If you want to remove a cookie, you have to set the timeout value and send it back to the client and then the browser will remove it. We cannot do it otherwise from the code a the cookie is handled by the browser

And I found out that setting the expiration to DateTime.MinValue does not expire the cookie in chrome (don't know about the other browsers) so I set it to 2001-01-01 :)

Here is the working code

public class CookieTempDataProvider : ITempDataProvider
{
    internal const string TempDataCookieKey = "__ControllerTempData";
    HttpContextBase _httpContext;

    public CookieTempDataProvider(HttpContextBase httpContext)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException("httpContext");
        }
        _httpContext = httpContext;
    }

    public HttpContextBase HttpContext
    {
        get
        {
            return _httpContext;
        }
    }

    protected virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
    {
        if (_httpContext.Request.Cookies.AllKeys.Contains(TempDataCookieKey)) //we need this because
        //Cookies[TempDataCookieKey] will create the cookie if it does not exist
        {
            HttpCookie cookie = _httpContext.Request.Cookies[TempDataCookieKey];
            if (cookie != null && !string.IsNullOrEmpty(cookie.Value))
            {
                IDictionary<string, object> deserializedTempData = DeserializeTempData(cookie.Value);

                // Remove cookie                
                cookie.Expires = new DateTime(2000, 1, 1);
                cookie.Value = string.Empty;
                _httpContext.Request.Cookies.Remove(TempDataCookieKey);

                if (_httpContext.Response != null && _httpContext.Response.Cookies != null)
                {
                    HttpCookie responseCookie = _httpContext.Response.Cookies[TempDataCookieKey];
                    if (responseCookie != null)
                    {
                        // Remove cookie
                        cookie.Expires = new DateTime(2000, 1, 1);
                        cookie.Value = string.Empty;
                        _httpContext.Response.Cookies.Remove(TempDataCookieKey);

                    }
                }

                return deserializedTempData;
            }
        }
        return new Dictionary<string, object>();
    }

    protected virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
    {
        if (values != null && values.Count > 0)
        {
            //there are values to set, so add the cookie. But no need to expire it as we need the browser to send the 
            //cookie back with the next request
            string cookieValue = SerializeToBase64EncodedString(values);
            var cookie = new HttpCookie(TempDataCookieKey);
            cookie.HttpOnly = true;
            cookie.Value = cookieValue;

            _httpContext.Response.Cookies.Add(cookie);
        }
        else
        {
            //Still we need to add the cookie with the expiration set, to make the client browser remove the cookie from the request.
            //Otherwise the browser will continue to send the cookie with the response

            //Also we need to do this only if the requet had a tempdata cookie

            if (_httpContext.Request.Cookies.AllKeys.Contains(TempDataCookieKey))
            {
                {
                    HttpCookie cookie = _httpContext.Request.Cookies[TempDataCookieKey];

                    // Remove the request cookie                
                    cookie.Expires = new DateTime(2000, 1, 1);
                    cookie.Value = string.Empty;
                    _httpContext.Request.Cookies.Remove(TempDataCookieKey);

                    var rescookie = new HttpCookie(TempDataCookieKey);
                    rescookie.HttpOnly = true;
                    rescookie.Value = "";
                    rescookie.Expires = new DateTime(2000, 1, 1); //so that the browser will remove the cookie when it receives the request
                    _httpContext.Response.Cookies.Add(rescookie);
                }
            }
        }
    }

    public static IDictionary<string, object> DeserializeTempData(string base64EncodedSerializedTempData)
    {
        byte[] bytes = Convert.FromBase64String(base64EncodedSerializedTempData);
        var memStream = new MemoryStream(bytes);
        var binFormatter = new BinaryFormatter();
        return binFormatter.Deserialize(memStream, null) as IDictionary<string, object> /*TempDataDictionary : This returns NULL*/;
    }

    public static string SerializeToBase64EncodedString(IDictionary<string, object> values)
    {
        MemoryStream memStream = new MemoryStream();
        memStream.Seek(0, SeekOrigin.Begin);
        var binFormatter = new BinaryFormatter();
        binFormatter.Serialize(memStream, values);
        memStream.Seek(0, SeekOrigin.Begin);
        byte[] bytes = memStream.ToArray();
        return Convert.ToBase64String(bytes);
    }

    IDictionary<string, object> ITempDataProvider.LoadTempData(ControllerContext controllerContext)
    {
        return LoadTempData(controllerContext);
    }

    void ITempDataProvider.SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
    {
        SaveTempData(controllerContext, values);
    }
}
Amila
  • 2,779
  • 1
  • 27
  • 31
2

Here is an example of a working solution without lots of excess code. It uses Json.NET for serializing, which is faster than BinaryFormatter + Base64Encoding and also produces a much shorter string (=less http overhead).

public class CookieTempDataProvider : ITempDataProvider
{
    const string cookieKey = "temp";

    public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
    {
        var cookie = controllerContext.HttpContext.Request.Cookies[cookieKey];   

        if (cookie != null) {
            return JsonConvert.DeserializeObject<IDictionary<string, object>>(cookie.Value);
        }

        return null;
    }

    // Method is called after action execution. The dictionary mirrors the contents of TempData.
    // If there are any values in the dictionary, save it in a cookie. If the dictionary is empty,
    // remove the cookie if it exists.
    public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
    {
        var ctx = controllerContext.HttpContext;

        if (values.Count > 0) {
            var cookie = new HttpCookie(cookieKey)
            {
                HttpOnly = true,
                Value = JsonConvert.SerializeObject(values)
            };

            ctx.Response.Cookies.Add(cookie);
        } else if (ctx.Request.Cookies[cookieKey] != null) {

            // Expire cookie to remove it from browser.
            ctx.Response.Cookies[cookieKey].Expires = DateTime.Today.AddDays(-1);
        }
    }
}
Mikael
  • 500
  • 2
  • 15