Sergey Maskalik

Sergey Maskalik's blog

In the pursuit of mastery

Browsers cache pages by default in order to quickly serve the content when back button is clicked. There are times however when you don’t want that functionality. For example if your user logs out you don’t want them to be able to press back button and navigate back to member page again. Or other scenarios where javascript updates shopping cart on the page, but when you hit back button on the page browser serves up content from cache which doesn’t have the new item count in the cart.

Originally I’ve stumbled upon method below, but soon to discovered that it only works for some browsers including Internet Explorer.

Page.Response.Cache.SetCacheability(HttpCacheability.ServerAndNoCache);

Which results in the following header attribute, which is enough for IE but not enough for other browsers like Firefox and Chrome.

Cache-Control:no-cache

After doing more research on the browser cache headers I found this comprehensive guide that explains that no-cache does not necessarily tells the browser not to cache results:

The “no-cache” directive, according to the RFC, tells the browser that it should revalidate with the server before serving the page from the cache. … In practice, IE and Firefox have started treating the no-cache directive as if it instructs the browser not to even cache the page. We started observing this behavior about a year ago. We suspect that this change was prompted by the widespread (and incorrect) use of this directive to prevent caching.directive to prevent caching.

We have used that header incorrectly just like the article said. And that’s why we are seeing the random results for different browsers because originally the “no-cache” header is not the correct one to instruct browser not to cache the content. I’m guilty of this myself since when I first encountered this problem I quickly found the solution online and was happy to move on with my life, without trying to fully understand the intent of the Cache Control headers.

After reading through and understanding what each header suppose to do it was clear that the correct header to instruct browsers not to cache pages is the Cache Control: No-store. In addition it’s a good practice to include Cache Control: No-cache just to be on the safe side. And for sensitive content I think it’s also good to tell proxies not to cache the http result for example account areas. And to do that we need to set Cache-Control: private.

In ASP.NET we have the following method

Response.Cache.SetNoStore();

Which sets headers to Cache-Control:private, no-store. That takes care almost everything and the only thing left to do is to append the recommended no-cache to be on the safe side

Response.Cache.AppendCacheExtension("no-cache");

And now we have the desired headers: Cache-Control:private, no-store, no-cache

Please let me know if you find any situations where above code doesn’t work. I’d like nail this problem once and for all.

Cheers

Update 04-12-2012

I’ve read another good resource on caching and think that adding expires header would also be beneficial for some other edge cases. So it wouldn’t hurt to add to this to your code as well:

Response.Expires = 0;

That brings us up to the three call that we need to make to make sure the page is not being cached:

Response.Cache.SetNoStore();
Response.Cache.AppendCacheExtension("no-cache");
Response.Expires = 0;

Unfortunately Factual api’s doesn’t have a .net client library. But it’s not a big deal, to get started all we need is to create a signed 2 legged oauth web request and hack query string together.

On the quest for simplest way to create a 2 legged oauth requests I stumbled upon Google.Data.Client library which already takes care of hashing and creating signatures for your request. All we have to do is to extend the OAuthAuthenticator class and make one override.

First you need to install Google’s library by going to your NuGet console and

Install-Package Google.GData.Client

Then create a new class that inherits from Google.GData.Client.OAuthAuthenticator:

    using System.Net;
    using Google.GData.Client;

    public class OAuth2LeggedAuthenticator : OAuthAuthenticator
    {
        public OAuth2LeggedAuthenticator(string applicationName, string consumerKey, string consumerSecret) : base(applicationName, consumerKey, consumerSecret)
        {
        }

        public override void ApplyAuthenticationToRequest(HttpWebRequest request)
        {
            base.ApplyAuthenticationToRequest(request);
            string header = OAuthUtil.GenerateHeader(request.RequestUri, ConsumerKey, ConsumerSecret, null, null, request.Method);
            request.Headers.Add(header);
        }
    }

And here is sample of the Factual service that uses that new class to make requests

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Net;
    using System.Linq;
    using Newtonsoft.Json.Linq;
    using RestaurantRep.Data.Model;

    namespace RestaurantRep.Data.Services
    {
        public class FactualService
        {
            private const string OAuthKey = "youroauthkey";
            private const string OAuthSecret = "yoursecretkey";
            private const string factualApiUrl = "http://api.v3.factual.com";

            private readonly OAuth2LeggedAuthenticator _factualAuthenticator;

            public FactualService()
            {
                _factualAuthenticator = new OAuth2LeggedAuthenticator("RestaurantRep", OAuthKey, OAuthSecret);
            }

            //sample query limit=50&filters={\"name\":\"Bar Hayama\"}"
            public HttpWebRequest CreateFactualRestaurantRequest(string query)
            {
                var requestUrl = new Uri(string.Format("{0}/t/restaurants-us/read{1}", factualApiUrl, query));
                return _factualAuthenticator.CreateHttpWebRequest("GET", requestUrl);
            }
    ...

And here is the ASP.NET Async Controller which queries Factual Api

    using System.IO;
    using System.Net;
    using System.Web;
    using System.Web.Mvc;
    using RestaurantRep.Data.Services;


    namespace RestaurantRep.Web.Controllers
    {
        public class FactualController : AsyncController
        {
            private readonly FactualService _service;
            public FactualController()
            {
                _service = new FactualService();
            }

            public void RestaurantsAsync()
            {
                AsyncManager.OutstandingOperations.Increment();

                var request = _service.CreateFactualRestaurantRequest(HttpUtility.UrlDecode(Request.Url.Query));
                request.BeginGetResponse(asyncResult =>
                {
                    using (WebResponse response = request.EndGetResponse(asyncResult))
                    {
                        using (var reader = new StreamReader(response.GetResponseStream()))
                        {
                            var jsonResult = reader.ReadToEnd();
                            AsyncManager.Parameters["restaurants"] = jsonResult;
                            AsyncManager.OutstandingOperations.Decrement();
                        }
                    }
                }, null);

            }

            public ContentResult RestaurantsCompleted(string restaurants)
            {
                return new ContentResult { Content = restaurants, ContentType = "application/json" };
            }
        }

As always, please let me know if you have questions.

Summary

You have probably seen stackoverflow’s 3rd party authentication in action: when you visit one of their sister sites that you have never visited before it automatically knows who you are. That’s possible with using 3rd party domain to store global encrypted session information and cross domain communication mechanism. For storage we can use either cookies and html5 localStorage and for communication we will look at using postMessage.

Without getting into the security aspect of the global authentication, I want to show the cross domain communication mechanism that makes this possible. We will use unsecure personalization data, like user’s first name for demonstration purposes.

Browser support for localStorage

localStorage browser support

Most modern browsers support localStorage except IE6 and IE7, pretty much exact same storage with postMessage

Browser support for postMessage:

postMessagebrowsersuport

Browser Usage

That brings us to the question, how many users are still using IE6 and IE7 and after we look at the figures decide if we can leave with those numbers or go a painful route.

As of February 2012 IE6 and IE7 are used by 3.6% of all users, and that number decreasing pretty fast, it dropped 15% from previous months which was at 4.2%.
So if you have a mission critical operation where you must support 100% of browsers then you have to go longer route and figure out your way through a tretcherous road of 3rd party cookies and additional cross domain communication scripts that support older browsers. This blog post has a nice write up about that.

It will probably take you 3 time as long to make it work for those 3.6% of users. So if you can I would save yourself a major headache and write clean code for modern browsers.

When I first started I was actually going to use cookies for storage, but later on I’ve discovered that IE blocks 3rd party content and you have go your way out to a pretty complex workaround. And there is also a reliability issue I sometimes experienced.

Storing data with 3rd party domain

We’ll take a look at authentication example. Once you login to bob.com, it stores a some piece of information on frank.com storage. So when you visit another site bobsister.com, it can also read information from frank.com as long as frank has bobsister.com in the list of valid sites. This is of course is not designed to be secure, but it is also possible by encrypting messages stored in localStorage. Making it secure is a topic for another blog post.

storage write example

Upon authentication we add an iframe that will write Name to 3rd party page, this can be done server side as well as with javascript

var src = "https://www.frank.com/auth/global/write.aspx?name=Alice";
$("body").append(
  "<iframe id='global-auth-frame' style='display:none' src='" +
    src +
    "'></iframe>"
);

or

public HtmlGenericControl CreateGlobalIdentityTag(string name)
{
    var frame = new HtmlGenericControl("iframe");
    frame.Attributes.Add("src", "https://www.frank.com/auth/global/write.aspx?name=" + name);
    frame.Attributes.Add("height","1");
    frame.Attributes.Add("width", "1");
    frame.Attributes.Add("frameborder","0");
    frame.Attributes.Add("scrolling","no");
    return frame;
}

Write page

<%@ Page Language="C#" AutoEventWireup="false" CodeBehind="Write.aspx.cs" Inherits="Sample.Write" EnableViewState="false" %>

<html>
<head><title></title></head>
<body>
        <asp:PlaceHolder runat="server" ID="plhWriteGlobalSession" Visible="False">
            <script type="text/javascript">
                function hasLocalStorage() {
                    try {
                        return 'localStorage' in window && window['localStorage'] !== null;
                    } catch (e) {
                        return false;
                    }
                }

                function save(keyPrefix, value) {
                    if (!hasLocalStorage()) { return; }

                    // clear old keys with this prefix, if any
                    for (var i = 0; i < localStorage.length; i++) {
                        if ((localStorage.key(i) || '').indexOf(keyPrefix) > -1) {
                            localStorage.removeItem(localStorage.key(i));
                        }
                    }

                    // save under this version
                    localStorage[keyPrefix] = value;
                };

                save('<%= StorageName %>' + '-name', '<%=Name %>');
        </script>
    </asp:PlaceHolder>
</body>
</html>

And a code behind that check’s if request came from trusted domain

public partial class Write : System.Web.UI.Page
{
    public string StorageName { get; set; }
    public string Name { get; set; }

    protected override void OnLoad(System.EventArgs e)
    {
        //Check if iframe UrlReferrer is valid
        if (Request.UrlReferrer != null && AuthorizedDomain(Request.UrlReferrer.Authority))
        {
            plhWriteGlobalSession.Visible = true;
        }
        else
        {
            return;
        }

        Name = Request.QueryString["name"];
        if (string.IsNullOrEmpty(Name))
            return;

        StorageName = "GlobalName";
        plhWriteGlobalSession.Visible = true;

        base.OnLoad(e);
    }

    private static bool AuthorizedDomain(string uri)
    {
        string[] authorizedDomains = new []
                                            {
                                                "bob.com",
                                                "bobsister.com"
                                            };
        return authorizedDomains.Contains(uri);
    }
}

Reading data from 3rd party domain

To read data from 3rd party we create an iframe similar to the write example.

And our read page will look like this.

<%@ Page Language="C#" AutoEventWireup="false" CodeBehind="Read.aspx.cs" Inherits="Sample.Read" EnableViewState="false" %>

<html>
<head>
<title></title>
</head>
<body>
    <script type="text/javascript">
        function hasLocalStorage() {
            try {
                return 'localStorage' in window && window['localStorage'] !== null;
            } catch (e) {
                return false;
            }
        }

        function load(keyPrefix) {
            if (!hasLocalStorage()) { return null; }
            return localStorage[keyPrefix];
        }

        function serialize(obj) {
            var str = [];
            for (var p in obj)
                str.push(p + "=" + obj[p]);
            return str.join("&");
        }

    </script>

    <%-- Not authorized domain is making request--%>
    <asp:Panel runat="server" ID="pnlNotAuthorized" Visible="false">
        <script type='text/javascript'>
            if(top.postMessage != 'undefined' && top.postMessage != null){
                top.postMessage('Not one of the authorized domains', "<%= Referrer %>");
            }
        </script>
    </asp:Panel>

    <%-- Authorized --%>
    <asp:Panel runat="server" ID="pnlAuthorized" Visible="false">
        <script type='text/javascript'>
            if (!hasLocalStorage()) {
                if (top.postMessage != 'undefined' && top.postMessage != null) {
                    top.postMessage('No Local Storage', "<%= Referrer %>");
                }
            }

            var storageKey = '<%= StorageKey%>';
            var data = { Referrer:  '<%= String.Format("{0}://{1}",Referrer.Scheme, Referrer.Authority) %>', Name : load(storageKey + '-name')};
            if (top.postMessage != 'undefined' && top.postMessage != null && data.Name != 'undefined' && data.Name != null) {
                top.postMessage(serialize(data), data.Referrer);
            }
        </script>
    </asp:Panel>


</body>
</html>

If the domain making the 3rd party request is not authorized we display a not authorized panel which post “Not authorized message”. Since postMessage is designed to use a string message, I’ve added a simple serialization method which formats object as a query string. I decided not to use outside libraries to serialize data to keep the number of requests to one and make this as fast as possible.

And the code behind:

public partial class Read : System.Web.UI.Page
{
    public string Name { get; set; }
    public Uri Referrer { get; set; }

    protected override void OnLoad(EventArgs e)
    {
        if(Request.UrlReferrer == null) return;

        Referrer = Request.UrlReferrer;

        if (AuthorizedDomain(Referrer.Authority))
        {
            pnlAuthorized.Visible = true;
        }
        else
        {
            pnlNotAuthorized.Visible = true;
        }


        base.OnLoad(e);
    }

    ...
}

The final piece is to subscribe our client side page page to receive Alice’s name once the 3rd party global data is read cross domain. Once we receive that data we can display it to the user, and/or save it in the cookie so it server side will have it available on the next request.

function deSerialize(text) {
  var result = {};
  var pairs = text.split("&");
  for (var i = 0; i < pairs.length; i++) {
    var keyValuePair = pairs[i].split("=");
    result[keyValuePair[0]] = keyValuePair[1];
  }
  return result;
}

$(window).bind("message", function (event) {
  var e = event.originalEvent;
  if (e.origin !== "https://www.frank.com") {
    log("not authorized domain");
    return;
  }

  var data = deSerialize(e.data);
  if (data.Name && data.Name != "") {
    //Display name on the page, etc..

    log("Name is found " + data.Name);
  } else {
    log("No Global Session Found");
    setGlobalSessionCookie(false);
  }
});

function log(text) {
  if (window.console) {
    console.log("INFO: " + text);
  }
}

I also learned the localStorage is based on the domain, so if you are going to be reading from secure pages make sure that you store and read from https domain because localStorage is different for secure and unsecure domains.

And there you have it.

Now you know how cross domain storage/communication is achieved with modern browsers. Let me know if you have any questions, and of course feedback is always greatly appreciated!

Cheers