How to Track your Users over Several Domains?

October 14, 2016Charles BOCHET5 min read

thumbnail

Track users over different domains is a recurrent issue while developing a substantial web solution. Use cases are countless:

  • authenticate customers over different websites (Google-like single sign-on);
  • cross-sell based on what they have visited previously on other websites;
  • customize user experience;
  • analytics.

Let's say we are trying to build a single authentication between two domains: my-account.com and webmail.com.
We are considering the following scenario, following a specific user named Jack:

User tracking scenario

How can we get webmail.com to know that its users are already logged in on my-account.com?

Setting cross-domain cookies?

The first approach that comes to mind is to set a cookie on my-account.com users' web browser as soon as they are authenticated on this website and to use these cookies later on webmail.com.
At first glance, this solution seems staight-forward: setting a cookie is easy and can be achieve within a few lines of codes using PHP or JS.

<?php
setcookie("loggedIn", true);

Unfortunately, here we encounter our first problem:
There is an important web concept called 'Same origin policy' that prevents one website to access another website resources through user's browser.
Amongst other things, this rule specify that cookies are specific to a given domain. Nor webmail.com is able to read my-account.com related cookies, nor my-account.com is capable of writing its own on webmail.com.
And this for user security purposes: you don't want a malicious page to get access my-bank-account.com session cookies.

So, how do we deal with this problem? How does Google, Microsoft & Co deal with it?

The wrong way: using AJAX requests (CORS)

OK, cookies are tied to a specific domain. Why not using a custom-made AJAX request from my-account.com on webmail.com and forcing webmail.com to settle its own cookie?

Using Ajax Requests

We can implement this interaction within a few line on both sides:

On my-account.com:

var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", "http://webmail.com/set-authentication-cookie.php", true);
xmlHttp.send(null);

set-authentication-cookie.php on webmail.com :

<?php
setcookie("loggedIn", true);

And... this is unsuccessful! After visiting my-account.com, no cookie is set on webmail.com.
We should have a look at JS console:

XMLHttpRequest cannot load http://webmail.com/set-authentication-cookie.php.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://account.com' is therefore not allowed access.

Browsers are a lot smarter than we first thought.
As we said, requests are restricted by the same-origin policy that prevents web browsers from executing code coming from another domain.
However this prevents legitimate behaviors between domains too even if domains are known and trusted.

What about CORS ?

Cross-origin resource sharing (CORS) is a W3C system that enables a given domain to request some resources (stylesheets, images, css, scripts...) from another domain.
CORS are built on top of XMLHttpRequest(). It actually softens same-origin policy to enable cross-domain requests. Of course, it needs some coordination between servers.

We need to add the following attribute to AJAX script:

xmlHttp.withCredentials = true

And specific headers to target PHP page:

header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Origin: my-account.com');

As soon as you do so, we should be able to perform our AJAX request:

my-account.com:

var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", "http://webmail.com/set-authentication-cookie.php", true);
xmlHttp.withCredentials = true;
xmlHttp.send(null);

webmail.com:

<?php
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Origin: http://my-account.com');
setcookie("loggedIn", true);

And... seems to works! As we navigate on my-account.com, a loggedIn cookie is set on webmail.com for later use.

Time for bad news

This approach is effective as long as third-party cookies are allowed in client configuration.
If Firefox and Chrome authorize third-party cookies by default, IE9+/Edge and Safari are far more susceptible.

Safari use enforced cookies policy that disable third-party cookies, plain and simple.

Internet Explorer (as usual) follows its own rules. It refuses such cookies unless you are using a P3P policy.
P3P is an old, deserted and unfinished (Firefox has given up supporting it) W3C spec and I won't recommend using it.

As we can't obviously ask our users to modify their settings for us, we are therefore at an impasse.

Futhermore, it doesn't matter what technical solution we choose: AJAX request, tag, JS script, iframe...
Even if we find a workaround, it won't be a sustainable solution since browsers will likely fix it later.

The right way: using HTTP Redirections

Asynchronous requests don't seem to do the trick. What about synchronous ones?
We need to perform two consecutive redirections and set cookies for each domain when needed:

Using redirections

This approach is transparent and painless for the user. It requires no Javascript and don't rely upon browser configuration:

index.php on my-account.com:

<?php
if (!isset($_COOKIE['alreadyRedirected'])) {
setcookie("alreadyRedirected", true);
header('Location: http://webmail.com/set-authentication-cookie.php');
}

set-authentication-cookie.php on my-account.com:

<?php
setcookie("loggedIn", true);
header('Location: http://account.com/index.php');

This time, loggedIn cookie is set and available on webmail.com once user has visited my-account.com page.
Best solutions are often simpler than we think, aren't they?

Conclusions

CORS requests is a powerful tool to perform cross-domain requests.
Nevertheless, it's not suitable for implementing cross-domain or third-party cookies because of some browsers default settings (Safari and IE/Edge).

HTTP redirections turns out to be the easiest and the most effective way of creating a single sign-on system.
This approach can easily be generalized: have fun tracking users over multiple websites!

Charles BOCHET

Charles BOCHET

Web Developer at Theodo