Sergey Maskalik

Sergey Maskalik's blog

Focusing on improving tiny bit every day

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

Simple Made Easy by Rich Hickey.
Rich Hickey is one of the most thoughful people I know and his talks are full of wisdom any programmer can appreciate.

LINQ-to-SQL: the multi-tier story story by Steven Sanderson.
This is a great post on how to manage lifecycle of your Linq-to-SQL DataContext in the layered architecture using Unit of Work pattern and HttpContext.Current.Items -

Truly Understanding Viewstate by Dave Reed - a must read for any Web Forms developer

A must watch: Single Page Application Presentations by Steven Sanderson

Selective Unit Testing – Costs and Benefits by Steven Sanderson