Skip to content
This repository was archived by the owner on Oct 8, 2024. It is now read-only.

Commit

Permalink
v1.0.4 update - origin/security updates + readme updates
Browse files Browse the repository at this point in the history
  • Loading branch information
ecaroth committed Aug 25, 2016
1 parent 759dfce commit 41e9cfd
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 40 deletions.
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ This library is intended for cases where you have scripts running on different d

The library leverages 2 files to achieve this - a javascript file you load/run on the page, and an HTML file that gets loaded onto that same page by the JS file. The JS & HTML files both must be served from the same domain/location (such as an s3 bucket). They leverage postMessage across the same/trusted domain to communicate and set the cookie on that domain, which can then be read/written by the same script run on any other domain you give it access to.

Authored by *Authored by* [Evan Carothers](https://github.com/ecaroth) @ [Contently](http://www.contently.com)

### Usage
Read the backstory and usage details at on the Building Contently Blog entry [Tracking people across multiple domains — when cookies just aren’t enough](https://medium.com/building-contently/tracking-people-across-multiple-domains-when-cookies-just-arent-enough-b270cc95beb1)

Usage
------

Simply include the script on any page where it's needed, create a new instance of xDomainCookie, and leverage the get/set functions:
````
<script src="http://my.s3bucket.com/xdomain_cookie.js"></script>

```html
<script src="//my.s3bucket.com/xdomain_cookie.js"></script>
<script>
var xd_cookie = xDomainCookie( 'https://my.s3bucket.com' );
var xd_cookie = xDomainCookie( '//my.s3bucket.com' );
xd_cookie.get( 'cookie_name', function(cookie_val){
//cookie val will contain value of cookie as fetched from local val (if present) else from iframe (if set), else null
if(!cookie_val){
Expand All @@ -23,8 +28,15 @@ Simply include the script on any page where it's needed, create a new instance o
</script>
```

Usage Notes
------

_Please Note_ that it's important for the `xdomain_cookie.js` file to be served from the same domain _and_ protocol as the path passed in for the iframe creation (when creating `xDomainCookie`). You can setup the script to use whichever page the protocol of the main window is using by specifying `//` as the protocol prefix (instead of explicit `https://` or `http://`, assuming the webserver hosting the `xdomain_cookie.html` file supports that procolol). It's also OK to serve both the script and iframe path over HTTPS in all instances, regardless of if the main page is loaded over HTTPS.

### API
This script should work in all modern desktop and mobile browsers that support the postMessage API (IE 8+, Chrome, FF, etc).

API
------

##### xDomainIframe( iframe_domain, namespace, xdomain_only )
Create a new instance of the xDomainIframe object that creates the iframe in the page and is ready for usage
Expand All @@ -33,7 +45,7 @@ Create a new instance of the xDomainIframe object that creates the iframe in the

`namespace` (string,optional) a namespace to use for postMessage passing - prevents collission if you are running multiple instances of this lib on the page... usually not needed

`xdomain_only` (boolean, optional, default false) if the cookie should _only_ be set on the xdomain site, not locally.. meaning that the xdomain version acts as the source of truth for the cookie value and eliminates local caching
`xdomain_only` (boolean, optional, default false) if the cookie should _only_ be set on the xdomain site, not locally.. meaning that the xdomain version acts as the source of truth for the cookie value and eliminates local caching. _PLEASE NOTE_ that this flag can provide specific intended behavior for different use cases. See the _Cross Domain ONLY Cookies_ section further down the readme for more info


#####.set( cookie_name, cookie_value, expires_days )
Expand All @@ -55,7 +67,17 @@ Get the value of the xdomain (& local) cookie with complete callback. _NOTE: thi

`expires_days` (int, optional) # of days to use for setting/re-upping cookie expiration (default is 30)

### Testing

Cross Domain ONLY Cookies
------

By default the `xDomainCookies` class is configured to set and use a local cookie as a caching mechanism to allow the callback for `.get()` to return as fast as possible. This is based on the fact that you are setting a piece of information that _should not change_ on any domains you are using the xDomainCookie on, as if you change the cookie from a single domain and it's cached locally at another domain, that local cache will prevent the updated value from being returned by the `get()` callback on that specific domain.

For use cases where you are setting a cookie value that should not change (such as something simple like a user ID), allowing the local cookie cache to function is useful and ideal. If, however, you are using advanced data types (such as a serialzed JSON object that has a property that can be updated from multiple domains, and needs to always have the most updated values accessible), then you should pass in `true` for the _xdomain_only_ param when creating a new `xDomainIframe` instance. This means that the local cookie cache isn't used, and the iframe must fully lead before the callback to `get()` will fire, but will guarantee that any interaction with the cookie data will always use up-to-date values.


Testing
------

There's a full test suite that leverages zombie/connect to mock & test the library behavior across multiple domains in multiple different situations. There is also a pre-build development setup to load/test in local environments in the library. Both of these rely on npm packages, so be sure to do an `npm install` in the root dir before running.

Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"name": "xdomain-cookies",
"version": "1.0.3",
"version": "1.0.4",
"repository": {
"type": "git",
"url": "git+https://github.com/contently/xdomain-cookies"
},
"description": "JS class for cross-domain shared cookie (via iframe shim)",
"scripts": {
"test": "node_modules/.bin/mocha test/test_suite.js",
Expand Down
27 changes: 16 additions & 11 deletions src/xdomain_cookie.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<!DOCTYPE html>
<HEAD>
<script>
"use strict";
window.IframeShim = (function(){

//orign & namespace data is passed in via urlencoded json object in url hash
var _hash_data = JSON.parse(decodeURIComponent(window.location.hash.substr(1))),
_namespace = _hash_data.namespace,
_origin = _hash_data.origin;
_window_origin = _hash_data.window_origin,
_iframe_origin = _hash_data.iframe_origin;

//basic cookie getter function (returns object of all cookies key/val)
function _get_local_cookies( cookie_name ){
var name = cookie_name + "=",
ca = document.cookie.split(';'),
function _get_local_cookies(){
var ca = document.cookie.split(';'),
cookies = {};
for(var i=0; i<ca.length; i++) {
var c = ca[i].trim(),
Expand All @@ -31,13 +32,17 @@

//listen for incoming postMessage requests (to write cookie, incoming from local page that loaded iframe)
window.addEventListener('message', function(event){
//NOTE - we must filter messages here to verify that it's the specific message/type we are looking for, and not from another window or script

var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
if (origin !== _iframe_origin) return; //incoming message not from iframe page

//We must filter messages here to verify that it's the specific message/type we are looking for, and not from another script
var data = null;
try{
var data = JSON.parse(event.data);
}catch(e){
var data = null;
}
if(data && typeof data=='object' && 'msg_type' in data && data.msg_type=='xdsc_write' && 'namespace' in data && data.namespace === _namespace){
data = JSON.parse(event.data);
}catch(e){}

if(data && typeof data==='object' && 'msg_type' in data && data.msg_type==='xdsc_write' && 'namespace' in data && data.namespace === _namespace){
_set_local_cookie( data.cookie_name, data.cookie_val, parseInt(data.expires_days,10) );
//ping down to page again to update values of xdomain cookie data
_send_xdomain_cookie_data_to_page();
Expand All @@ -51,7 +56,7 @@
namespace: _namespace
};
//postmessage to parent window w/ data
window.parent.postMessage(JSON.stringify(msg), _origin);
window.parent.postMessage(JSON.stringify(msg), _window_origin);
}

//initialization - ping parent window w/ cookie payload right away so it an hit callbacks asap
Expand Down
42 changes: 24 additions & 18 deletions src/xdomain_cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//iframe_path = full TLD (and optional path) to location where iframe_shared_cookie.html is served from, and domain cookie will be set on
//namespace = namespace to use when identifying that postMessage calls incoming are for our use

if( iframe_path.substr(0,2)=='//' ) iframe_path = (window.location.protocol=='https:'?'https:':'http:')+iframe_path; //verify protocol is present & used
if( iframe_path.substr(0,2)==='//' ) iframe_path = (window.location.protocol==='https:'?'https:':'http:')+iframe_path; //verify protocol is present & used

var _namespace = namespace || 'xdsc', //namespace for the shared cookie in case there are multiple instances on one page - prevents postMessage collision
_load_wait_ms = iframe_load_timeout_ms || (1000*6), //wait 6 seconds if no other overloaded wait time specified
Expand All @@ -15,19 +15,22 @@
_xdomain_cookie_data = {}, //shared cookie data set by the iframe after load/ready
_id = new Date().getTime(), //identifier to use for iframe in case there are multiple on the page
_default_expires_days = 30, //default expiration days for cookies when re-uppded
_xdomain_only = xdomain_only===true; //should we ONLY use xdomain cookies (and avoid local cache)
_xdomain_only = !!xdomain_only; //should we ONLY use xdomain cookies (and avoid local cache)

//function called on inbound post message - filter/verify that message is for our consumption, then set ready data an fire callbacks
function _inbound_postmessage( event ){

var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
if (origin !== iframe_path) return; //incoming message not from iframe

if(typeof event.data !== 'string') return; //expected json string encoded payload
var data = null;
try{
var data = JSON.parse(event.data);
}catch(e){
var data = null;
}
data = JSON.parse(event.data);
}catch(e){}

if(!data) return;
if(typeof data==='object' && !(data instanceof Array) && 'msg_type' in data && data.msg_type=='xdsc_read' && 'namespace' in data && data.namespace === _namespace){
if(typeof data==='object' && !(data instanceof Array) && 'msg_type' in data && data.msg_type==='xdsc_read' && 'namespace' in data && data.namespace === _namespace){
//NOTE - the only thing iframe postMessages to us is when it's initially loaded, and it includes payload of all cookies set on iframe domain
_xdomain_cookie_data = data.cookies;
_iframe_ready = true;
Expand Down Expand Up @@ -124,18 +127,20 @@
//re-up the cookie
_set_xdomain_cookie_value( cookie_name, cookie_val, expires_days );

if(typeof callback == 'function') callback( cookie_val );
if(typeof callback === 'function') callback( cookie_val );
}

//see if local cookie is set - if so, no need to wait for iframe to fetch cookie
var _existing_local_cookie_val = _get_local_cookie( cookie_name );
if(_existing_local_cookie_val){
//set onready call to write-through cookie once iframe is ready, then call callback directly
_on_iframe_ready_or_error( function( is_err ){
_cb( !is_err, _existing_local_cookie_val );
});
return callback( _existing_local_cookie_val );
}
if(!_xdomain_only){
//see if local cookie is set - if so, no need to wait for iframe to fetch cookie
var _existing_local_cookie_val = _get_local_cookie( cookie_name );
if(_existing_local_cookie_val){
//set onready call to write-through cookie once iframe is ready, then call callback directly
_on_iframe_ready_or_error( function( is_err ){
_cb( !is_err, _existing_local_cookie_val );
});
return callback( _existing_local_cookie_val );
}
}

//no local cookie is set/present, so bind CB to iframe ready/error callback so it's pinged a soon as we hit a ready state from iframe
_on_iframe_ready_or_error(function( is_err ){
Expand All @@ -158,7 +163,8 @@
ifr.id = 'xdomain_cookie_'+_id;
var data = {
namespace: _namespace,
origin: window.location.origin
window_origin: window.location.origin,
iframe_origin: iframe_path
};
ifr.src = iframe_path+'/xdomain_cookie.html#'+encodeURIComponent(JSON.stringify(data));
document.body.appendChild( ifr );
Expand Down
111 changes: 108 additions & 3 deletions test/test_suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,114 @@ describe("Iframe shared cookie",function(){
this.server.close(done);
});

describe('Across multiple domains, xdomain_only on domain 2 only, no cookies set initially', function() {

var TEMP_NEWVAL = 'new_val';

before(function(){
this.browser = new Browser();
this.browser.deleteCookies();
})

before(function(done) {
this.browser.visit('http://'+HTML_DOMAIN_1+'/test_page.html',function(){
setTimeout(done, 1000); //wait for postmessage
});
});
before(function(done) {
//open new tab with alternate domain name (that loads same shared iframe)
this.browser.open()
this.browser.visit('http://'+HTML_DOMAIN_2+'/test_page.html#xdomain_only',function(){
setTimeout(done, 1000); //wait for postmessage
});
});

it('get/set cookie, local cookie set for domain 2 only and iframe cookie set',function(){

//tab @ HTML_DOMAIN_2 should have been preset from visit to HTML_DOMAIN_1
expect( this.browser.window.location.href ).to.equal( 'http://'+HTML_DOMAIN_2+'/test_page.html#xdomain_only' );
expect( this.browser.queryAll('iframe[src*="http://'+IFRAME_DOMAIN+'/xdomain_cookie.html"]' ).length).to.equal(1);
//verify there was no existing/returned val from .get()
expect( this.browser.evaluate(JS_VAR_EXISTING_VAL) ).to.equal( EXPECTED_UNSET_COOKIE_VAL );
//verify that final val was set correctly
expect( this.browser.evaluate(JS_VAR_FINAL_VAL) ).to.equal( EXPECTED_UNSET_COOKIE_VAL );
this.browser.tabs.current.close();

//check the tab @ HTML_DOMAIN_1 (which was visited first)
expect( this.browser.window.location.href ).to.equal( 'http://'+HTML_DOMAIN_1+'/test_page.html' );
expect( this.browser.queryAll('iframe[src*="http://'+IFRAME_DOMAIN+'/xdomain_cookie.html"]' ).length).to.equal(1);
//verify there was no existing/returned val from .get() since this page was visitied first and local cookie was ignored
expect( this.browser.evaluate(JS_VAR_EXISTING_VAL) ).to.equal( null );
//verify that final val was set correctly
expect( this.browser.evaluate(JS_VAR_FINAL_VAL) ).to.equal( EXPECTED_UNSET_COOKIE_VAL );

//check cookie values (verify local cookie for domain 1 and not for domain 2)
var local_cookie1 = this.browser.getCookie({ name: TEST_COOKIE_NAME, domain: HTML_DOMAIN_1, path: '/' });
expect( local_cookie1 ).to.equal( EXPECTED_UNSET_COOKIE_VAL );
var local_cookie2 = this.browser.getCookie({ name: TEST_COOKIE_NAME, domain: HTML_DOMAIN_2, path: '/' });
expect( local_cookie2 ).to.equal( null );

var iframe_cookie = this.browser.getCookie({ name: TEST_COOKIE_NAME, domain: IFRAME_DOMAIN, path: '/' });
expect( iframe_cookie ).to.equal( EXPECTED_UNSET_COOKIE_VAL );
this.browser.tabs.current.close();
});
});

describe('Across multiple domains, xdomain_only on both local cookie set on domain 1', function() {

var TEMP_NEWVAL = 'new_val';

before(function(){
this.browser = new Browser();
this.browser.deleteCookies();
var cookie_data = { name: TEST_COOKIE_NAME, domain: HTML_DOMAIN_1, path: '/', value: "ignored", expires:new Date((new Date().getTime())+(1000*60*10))};
this.browser.setCookie(cookie_data);
})

before(function(done) {
this.browser.visit('http://'+HTML_DOMAIN_1+'/test_page.html#xdomain_only',function(){
setTimeout(done, 1000); //wait for postmessage
});
});
before(function(done) {
//open new tab with alternate domain name (that loads same shared iframe)
this.browser.open()
this.browser.visit('http://'+HTML_DOMAIN_2+'/test_page.html#xdomain_only',function(){
setTimeout(done, 1000); //wait for postmessage
});
});

it('get/set cookie, local cookie set but ignored, cookie set in iframe',function(){

//tab @ HTML_DOMAIN_2 should have been preset from visit to HTML_DOMAIN_1
expect( this.browser.window.location.href ).to.equal( 'http://'+HTML_DOMAIN_2+'/test_page.html#xdomain_only' );
expect( this.browser.queryAll('iframe[src*="http://'+IFRAME_DOMAIN+'/xdomain_cookie.html"]' ).length).to.equal(1);
//verify there was no existing/returned val from .get()
expect( this.browser.evaluate(JS_VAR_EXISTING_VAL) ).to.equal( EXPECTED_UNSET_COOKIE_VAL );
//verify that final val was set correctly
expect( this.browser.evaluate(JS_VAR_FINAL_VAL) ).to.equal( EXPECTED_UNSET_COOKIE_VAL );
this.browser.tabs.current.close();

//check the tab @ HTML_DOMAIN_1 (which was visited first)
expect( this.browser.window.location.href ).to.equal( 'http://'+HTML_DOMAIN_1+'/test_page.html#xdomain_only' );
expect( this.browser.queryAll('iframe[src*="http://'+IFRAME_DOMAIN+'/xdomain_cookie.html"]' ).length).to.equal(1);
//verify there was no existing/returned val from .get() since this page was visitied first and local cookie was ignored
expect( this.browser.evaluate(JS_VAR_EXISTING_VAL) ).to.equal( null );
//verify that final val was set correctly
expect( this.browser.evaluate(JS_VAR_FINAL_VAL) ).to.equal( EXPECTED_UNSET_COOKIE_VAL );

//check cookie values (verify no local cookie for either domain)
var local_cookie1 = this.browser.getCookie({ name: TEST_COOKIE_NAME, domain: HTML_DOMAIN_1, path: '/' });
expect( local_cookie1 ).to.equal( "ignored" );
var local_cookie2 = this.browser.getCookie({ name: TEST_COOKIE_NAME, domain: HTML_DOMAIN_2, path: '/' });
expect( local_cookie2 ).to.equal( null );

var iframe_cookie = this.browser.getCookie({ name: TEST_COOKIE_NAME, domain: IFRAME_DOMAIN, path: '/' });
expect( iframe_cookie ).to.equal( EXPECTED_UNSET_COOKIE_VAL );
this.browser.tabs.current.close();
});
});

describe('Single domain, xdomain_only cookie', function(){

before(function(){
Expand Down Expand Up @@ -469,9 +577,6 @@ describe("Iframe shared cookie",function(){

before(function( done ){
this.browser = new Browser();
this.browser.on('error',function(err){
console.log("BROWSER ERROR",err);
});
this.browser.deleteCookies();
var _browser = this.browser;
this.browser.visit('http://'+HTML_DOMAIN_1+'/test_page.html',function(){
Expand Down

0 comments on commit 41e9cfd

Please sign in to comment.