Repository: auth0/auth0.js (js)

Issue:
localStorage is spammed with state/nonce combos
Every time before `auth0.js` performs a redirect to the `/authorize` endpoint, `state` and `nonce` are generated and stored in `localStorage` for verification upon return. If the user lands on the Auth0 hosted login page but does not login, the stored `state/nonce` combo won't be removed.

This means that `localStorage` can fill up with lots of unused values, especially during development.
```
com.auth0.auth.3z-j16G6OFMF_v-nt_QUZS5VV~vf.fGD:	{"nonce":"55yFu~kZUv0KT6azRtCIyf_c3dx-GAmJ"}
```
![image](https://cloud.githubusercontent.com/assets/8770194/24415185/ec2b9ea8-13e0-11e7-94d3-8b26c2d499c0.png)

Is there a reason why this could not be done via a single `localStorage` key, with JSON data?
Or possibly that in addition to `nonce`, a `issuedAt` field could be used for cleaning up old `state/nonce` combos?

Pull request:
Start using cookies instead of localStorage by default
fix #402 #816 

In some cases, there's no way to old local storage entries because we don't have enough information:

- if the auth result wasn't successful
- if the cross origin auth was successful, but it didn't use the cross auth callback page

Because of that, cookies are a better fit for storage, since it has built-in expiration dates.

There's a new option `__tryLocalStorageFirst` that restores the previous behavior if you find any compatibility issues (please report them if you find it) or prefer to use localStorage for some reason.

This is not considered a breaking change because there shouldn't be any behavioral changes between versions.

Patch:
diff --git a/src/authentication/index.js b/src/authentication/index.js
index 34aef35..971300a 100644
--- a/src/authentication/index.js
+++ b/src/authentication/index.js
@@ -4,7 +4,7 @@ import qs from 'qs';
 import RequestBuilder from '../helper/request-builder';
 import objectHelper from '../helper/object';
 import assert from '../helper/assert';
-import ssodata from '../helper/ssodata';
+import SSODataStorage from '../helper/ssodata';
 import windowHelper from '../helper/window';
 import responseHandler from '../helper/response-handler';
 import parametersWhitelist from '../helper/parameters-whitelist';
@@ -81,6 +81,7 @@ function Authentication(auth0, options) {
   this.warn = new Warn({
     disableWarnings: !!options._disableDeprecationWarnings
   });
+  this.ssodataStorage = new SSODataStorage(this.baseOptions);
 }
 
 /**
@@ -393,7 +394,7 @@ Authentication.prototype.getSSOData = function(withActiveDirectories, cb) {
   }
   assert.check(cb, { type: 'function', message: 'cb parameter is not valid' });
   var clientId = this.baseOptions.clientID;
-  var ssodataInformation = ssodata.get() || {};
+  var ssodataInformation = this.ssodataStorage.get() || {};
 
   this.auth0.checkSession(
     {
diff --git a/src/helper/ssodata.js b/src/helper/ssodata.js
index 81d9f98..a316e32 100644
--- a/src/helper/ssodata.js
+++ b/src/helper/ssodata.js
@@ -1,18 +1,22 @@
-import storage from './storage';
+import Storage from './storage';
 
-export default {
-  set: function(connection, sub) {
-    var ssodata = {
-      lastUsedConnection: connection,
-      lastUsedSub: sub
-    };
-    storage.setItem('auth0.ssodata', JSON.stringify(ssodata));
-  },
-  get: function() {
-    var ssodata = storage.getItem('auth0.ssodata');
-    if (!ssodata) {
-      return;
-    }
-    return JSON.parse(ssodata);
+function SSODataStorage(options) {
+  this.storage = new Storage(options);
+}
+
+SSODataStorage.prototype.set = function(connection, sub) {
+  var ssodata = {
+    lastUsedConnection: connection,
+    lastUsedSub: sub
+  };
+  this.storage.setItem('auth0.ssodata', JSON.stringify(ssodata));
+};
+SSODataStorage.prototype.get = function() {
+  var ssodata = this.storage.getItem('auth0.ssodata');
+  if (!ssodata) {
+    return;
   }
+  return JSON.parse(ssodata);
 };
+
+export default SSODataStorage;
diff --git a/src/helper/storage.js b/src/helper/storage.js
index 85f013f..7fde881 100644
--- a/src/helper/storage.js
+++ b/src/helper/storage.js
@@ -1,26 +1,23 @@
 import StorageHandler from './storage/handler';
-var storage;
-var getStorage = function() {
-  if (!storage) {
-    storage = new StorageHandler();
-  }
-  return storage;
-};
 
-export default {
-  getItem: function(key) {
-    var value = getStorage().getItem(key);
-    try {
-      return JSON.parse(value);
-    } catch (_) {
-      return value;
-    }
-  },
-  removeItem: function(key) {
-    return getStorage().removeItem(key);
-  },
-  setItem: function(key, value, options) {
-    var json = JSON.stringify(value);
-    return getStorage().setItem(key, json, options);
+function Storage(options) {
+  this.handler = new StorageHandler(options);
+}
+
+Storage.prototype.getItem = function(key) {
+  var value = this.handler.getItem(key);
+  try {
+    return JSON.parse(value);
+  } catch (_) {
+    return value;
   }
 };
+Storage.prototype.removeItem = function(key) {
+  return this.handler.removeItem(key);
+};
+Storage.prototype.setItem = function(key, value, options) {
+  var json = JSON.stringify(value);
+  return this.handler.setItem(key, json, options);
+};
+
+export default Storage;
diff --git a/src/helper/storage/handler.js b/src/helper/storage/handler.js
index c7b01c4..df6a7ee 100644
--- a/src/helper/storage/handler.js
+++ b/src/helper/storage/handler.js
@@ -3,9 +3,12 @@ import DummyStorage from './dummy';
 import CookieStorage from './cookie';
 import Warn from '../warn';
 
-function StorageHandler() {
+function StorageHandler(options) {
   this.warn = new Warn({});
   this.storage = new CookieStorage();
+  if (options.__tryLocalStorageFirst !== true) {
+    return;
+  }
   try {
     // some browsers throw an error when trying to access localStorage
     // when localStorage is disabled.
diff --git a/src/web-auth/cross-origin-authentication.js b/src/web-auth/cross-origin-authentication.js
index a55ade6..71bf1f3 100644
--- a/src/web-auth/cross-origin-authentication.js
+++ b/src/web-auth/cross-origin-authentication.js
@@ -5,7 +5,7 @@ import objectHelper from '../helper/object';
 import RequestBuilder from '../helper/request-builder';
 import WebMessageHandler from './web-message-handler';
 import responseHandler from '../helper/response-handler';
-import storage from '../helper/storage';
+import Storage from '../helper/storage';
 import * as times from '../helper/times';
 
 function CrossOriginAuthentication(webAuth, options) {
@@ -13,6 +13,7 @@ function CrossOriginAuthentication(webAuth, options) {
   this.baseOptions = options;
   this.request = new RequestBuilder(options);
   this.webMessageHandler = new WebMessageHandler(webAuth);
+  this.storage = new Storage(options);
 }
 
 function getFragment(name) {
@@ -90,7 +91,7 @@ CrossOriginAuthentication.prototype.login = function(options, cb) {
         .merge(options)
         .with({ loginTicket: data.body.login_ticket });
       var key = createKey(_this.baseOptions.rootUrl, data.body.co_id);
-      storage.setItem(key, data.body.co_verifier, { expires: times.MINUTES_15 });
+      _this.storage.setItem(key, data.body.co_verifier, { expires: times.MINUTES_15 });
       if (popupMode) {
         _this.webMessageHandler.run(
           authorizeOptions,
@@ -102,7 +103,7 @@ CrossOriginAuthentication.prototype.login = function(options, cb) {
     });
 };
 
-function tryGetVerifier(theWindow, key) {
+function tryGetVerifier(storage, key) {
   try {
     var verifier = storage.getItem(key);
     storage.removeItem(key);
@@ -120,13 +121,14 @@ function tryGetVerifier(theWindow, key) {
 CrossOriginAuthentication.prototype.callback = function() {
   var targetOrigin = decodeURIComponent(getFragment('origin'));
   var theWindow = windowHelper.getWindow();
+  var _this = this;
 
   theWindow.addEventListener('message', function(evt) {
     if (evt.data.type !== 'co_verifier_request') {
       return;
     }
     var key = createKey(evt.origin, evt.data.request.id);
-    var verifier = tryGetVerifier(theWindow, key);
+    var verifier = tryGetVerifier(_this.storage, key);
 
     evt.source.postMessage(
       {
diff --git a/src/web-auth/index.js b/src/web-auth/index.js
index 1b03a43..8f5480f 100644
--- a/src/web-auth/index.js
+++ b/src/web-auth/index.js
@@ -6,7 +6,7 @@ import qs from 'qs';
 import PluginHandler from '../helper/plugins';
 import windowHelper from '../helper/window';
 import objectHelper from '../helper/object';
-import ssodata from '../helper/ssodata';
+import SSODataStorage from '../helper/ssodata';
 import TransactionManager from './transaction-manager';
 import Authentication from '../authentication';
 import Redirect from './redirect';
@@ -107,7 +107,7 @@ function WebAuth(options) {
 
   this.baseOptions.jwksURI = this.baseOptions.overrides && this.baseOptions.overrides.__jwks_uri;
 
-  this.transactionManager = new TransactionManager(this.baseOptions.transaction);
+  this.transactionManager = new TransactionManager(this.baseOptions);
 
   this.client = new Authentication(this.baseOptions);
   this.redirect = new Redirect(this, this.baseOptions);
@@ -115,6 +115,7 @@ function WebAuth(options) {
   this.crossOriginAuthentication = new CrossOriginAuthentication(this, this.baseOptions);
   this.webMessageHandler = new WebMessageHandler(this);
   this._universalLogin = new HostedPages(this, this.baseOptions);
+  this.ssodataStorage = new SSODataStorage(this.baseOptions);
 }
 
 /**
@@ -245,7 +246,7 @@ WebAuth.prototype.validateAuthenticationResponse = function(options, parsedHash,
       if (payload) {
         sub = payload.sub;
       }
-      ssodata.set(transaction.lastUsedConnection, sub);
+      _this.ssodataStorage.set(transaction.lastUsedConnection, sub);
     }
     return cb(null, buildParseHashResponse(parsedHash, appState, payload));
   };
diff --git a/src/web-auth/popup.js b/src/web-auth/popup.js
index b25b013..c747961 100644
--- a/src/web-auth/popup.js
+++ b/src/web-auth/popup.js
@@ -17,7 +17,7 @@ function Popup(webAuth, options) {
   this.client = webAuth.client;
   this.webAuth = webAuth;
 
-  this.transactionManager = new TransactionManager(this.baseOptions.transaction);
+  this.transactionManager = new TransactionManager(this.baseOptions);
   this.crossOriginAuthentication = new CrossOriginAuthentication(webAuth, this.baseOptions);
   this.warn = new Warn({
     disableWarnings: !!options._disableDeprecationWarnings
diff --git a/src/web-auth/transaction-manager.js b/src/web-auth/transaction-manager.js
index 30e25f4..0a3b18b 100644
--- a/src/web-auth/transaction-manager.js
+++ b/src/web-auth/transaction-manager.js
@@ -1,13 +1,14 @@
 import random from '../helper/random';
-import storage from '../helper/storage';
+import Storage from '../helper/storage';
 import * as times from '../helper/times';
 
 var DEFAULT_NAMESPACE = 'com.auth0.auth.';
 
 function TransactionManager(options) {
-  options = options || {};
-  this.namespace = options.namespace || DEFAULT_NAMESPACE;
-  this.keyLength = options.keyLength || 32;
+  var transaction = options.transaction || {};
+  this.namespace = transaction.namespace || DEFAULT_NAMESPACE;
+  this.keyLength = transaction.keyLength || 32;
+  this.storage = new Storage(options);
 }
 
 TransactionManager.prototype.process = function(options) {
@@ -45,7 +46,7 @@ TransactionManager.prototype.generateTransaction = function(
   state = state || random.randomString(this.keyLength);
   nonce = nonce || (generateNonce ? random.randomString(this.keyLength) : null);
 
-  storage.setItem(
+  this.storage.setItem(
     this.namespace + state,
     {
       nonce: nonce,
@@ -64,13 +65,13 @@ TransactionManager.prototype.generateTransaction = function(
 TransactionManager.prototype.getStoredTransaction = function(state) {
   var transactionData;
 
-  transactionData = storage.getItem(this.namespace + state);
+  transactionData = this.storage.getItem(this.namespace + state);
   this.clearTransaction(state);
   return transactionData;
 };
 
 TransactionManager.prototype.clearTransaction = function(state) {
-  storage.removeItem(this.namespace + state);
+  this.storage.removeItem(this.namespace + state);
 };
 
 export default TransactionManager;
diff --git a/src/web-auth/username-password.js b/src/web-auth/username-password.js
index 7162bd3..66ac700 100644
--- a/src/web-auth/username-password.js
+++ b/src/web-auth/username-password.js
@@ -9,7 +9,7 @@ import TransactionManager from './transaction-manager';
 function UsernamePassword(options) {
   this.baseOptions = options;
   this.request = new RequestBuilder(options);
-  this.transactionManager = new TransactionManager(this.baseOptions.transaction);
+  this.transactionManager = new TransactionManager(this.baseOptions);
 }
 
 UsernamePassword.prototype.login = function(options, cb) {


Test patch:
diff --git a/test/authentication/authentication.test.js b/test/authentication/authentication.test.js
index 3e0cd49..8da12dc 100644
--- a/test/authentication/authentication.test.js
+++ b/test/authentication/authentication.test.js
@@ -8,7 +8,7 @@ import request from 'superagent';
 
 import RequestBuilder from '../../src/helper/request-builder';
 import windowHelper from '../../src/helper/window';
-import storage from '../../src/helper/storage';
+import Storage from '../../src/helper/storage';
 import Authentication from '../../src/authentication';
 import WebAuth from '../../src/web-auth';
 
@@ -279,7 +279,7 @@ describe('auth0.authentication', function() {
           responseType: 'code',
           _sendTelemetry: false
         });
-        stub(storage, 'getItem', function(key) {
+        stub(Storage.prototype, 'getItem', function(key) {
           expect(key).to.be('auth0.ssodata');
           return JSON.stringify({
             lastUsedConnection: 'lastUsedConnection',
@@ -289,7 +289,7 @@ describe('auth0.authentication', function() {
         });
       });
       after(function() {
-        storage.getItem.restore();
+        Storage.prototype.getItem.restore();
       });
       beforeEach(function() {
         stub(windowHelper, 'getWindow', function() {
diff --git a/test/helper/ssodata.test.js b/test/helper/ssodata.test.js
index 37a9b62..3d7b5a5 100644
--- a/test/helper/ssodata.test.js
+++ b/test/helper/ssodata.test.js
@@ -1,30 +1,34 @@
 import expect from 'expect.js';
 import { stub } from 'sinon';
 
-import storage from '../../src/helper/storage';
-import ssodata from '../../src/helper/ssodata';
+import Storage from '../../src/helper/storage';
+import SSODataStorage from '../../src/helper/ssodata';
 
 describe('helpers', function() {
+  var ssodata;
+  beforeEach(function() {
+    ssodata = new SSODataStorage({});
+  });
   describe('ssodata', function() {
     afterEach(function() {
-      if (storage.setItem.restore) {
-        storage.setItem.restore();
+      if (Storage.prototype.setItem.restore) {
+        Storage.prototype.setItem.restore();
       }
-      if (storage.getItem.restore) {
-        storage.getItem.restore();
+      if (Storage.prototype.getItem.restore) {
+        Storage.prototype.getItem.restore();
       }
     });
     describe('get', function() {
       it('when there is data', function() {
         var expectedObject = { foo: 'bar' };
-        stub(storage, 'getItem', function() {
+        stub(Storage.prototype, 'getItem', function() {
           return JSON.stringify(expectedObject);
         });
         var data = ssodata.get();
         expect(data).to.be.eql(expectedObject);
       });
       it('when there is no data', function() {
-        stub(storage, 'getItem', function() {
+        stub(Storage.prototype, 'getItem', function() {
           return undefined;
         });
         var data = ssodata.get();
@@ -33,7 +37,7 @@ describe('helpers', function() {
     });
     describe('set', function() {
       it('sets ssodata', function(done) {
-        stub(storage, 'setItem', function(key, value) {
+        stub(Storage.prototype, 'setItem', function(key, value) {
           expect(key).to.be('auth0.ssodata');
           expect(JSON.parse(value)).to.be.eql({
             lastUsedConnection: 'connection',
diff --git a/test/helper/storage-handler.test.js b/test/helper/storage-handler.test.js
index 98e0e52..b68ac96 100644
--- a/test/helper/storage-handler.test.js
+++ b/test/helper/storage-handler.test.js
@@ -28,8 +28,12 @@ describe('helpers storage handler', function() {
   afterEach(function() {
     windowHandler.getWindow.restore();
   });
-  it('should use localStorage by default', function() {
-    var handler = new StorageHandler();
+  it('should use CookieStorage by default', function() {
+    var handler = new StorageHandler({});
+    expect(handler.storage).to.be.a(CookieStorage);
+  });
+  it('should use localStorage when __tryLocalStorageFirst is true', function() {
+    var handler = new StorageHandler({ __tryLocalStorageFirst: true });
     expect(handler.storage).to.be.a(MockLocalStorage);
   });
   describe('when using localStorage', function() {
@@ -55,30 +59,30 @@ describe('helpers storage handler', function() {
     });
     it('calls getItem correctly', function() {
       var key = 'foo';
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
       expect(handler.getItem(key)).to.be('test');
       expect(getItemStub.firstCall.args).to.be.eql([key]);
     });
     it('calls setItem correctly', function() {
       var key = 'foo';
       var value = 'bar';
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
       handler.setItem(key, value, {});
       expect(setItemSpy.firstCall.args).to.be.eql([key, value, {}]);
     });
     it('calls removeItem correctly', function() {
       var key = 'foo';
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
       handler.removeItem(key);
       expect(removeItemSpy.firstCall.args).to.be.eql([key]);
     });
   });
   describe('should use cookie storage', function() {
-    it('when localstorage is not available', function() {
+    it('when __tryLocalStorageFirst is true but localStorage is not available', function() {
       windowHandler.getWindow.restore();
       stub(windowHandler, 'getWindow', function(message) {});
 
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
       expect(handler.storage).to.be.a(CookieStorage);
     });
     it('when localstorage throws an error', function() {
@@ -91,14 +95,13 @@ describe('helpers storage handler', function() {
         };
       });
 
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
       expect(handler.storage).to.be.a(CookieStorage);
     });
-
     it('when localstorage fails with getItem', function() {
       spy(CookieStorage.prototype, 'getItem');
 
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
 
       expect(handler.storage).to.be.a(MockLocalStorage);
       handler.getItem('pepe');
@@ -108,11 +111,10 @@ describe('helpers storage handler', function() {
 
       CookieStorage.prototype.getItem.restore();
     });
-
     it('when localstorage fails with setItem', function() {
       spy(CookieStorage.prototype, 'setItem');
 
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
 
       expect(handler.storage).to.be.a(MockLocalStorage);
       handler.setItem('some', 'value', { options: true });
@@ -126,11 +128,10 @@ describe('helpers storage handler', function() {
 
       CookieStorage.prototype.setItem.restore();
     });
-
     it('when localstorage fails with removeItem', function() {
       spy(CookieStorage.prototype, 'removeItem');
 
-      var handler = new StorageHandler();
+      var handler = new StorageHandler({ __tryLocalStorageFirst: true });
 
       expect(handler.storage).to.be.a(MockLocalStorage);
       handler.removeItem('some');
@@ -142,8 +143,8 @@ describe('helpers storage handler', function() {
     });
   });
 
-  it('should failover to dummy', function() {
-    var handler = new StorageHandler();
+  it('should failover to from localStorage to CookieStorage to DummyStorage', function() {
+    var handler = new StorageHandler({ __tryLocalStorageFirst: true });
 
     expect(handler.storage).to.be.a(MockLocalStorage);
     handler.failover();
diff --git a/test/helper/storage.test.js b/test/helper/storage.test.js
index 028bd61..6cca82b 100644
--- a/test/helper/storage.test.js
+++ b/test/helper/storage.test.js
@@ -1,11 +1,14 @@
 import expect from 'expect.js';
 import { stub, spy } from 'sinon';
 
-import windowHandler from '../../src/helper/window';
 import StorageHandler from '../../src/helper/storage/handler';
-import storage from '../../src/helper/storage';
+import Storage from '../../src/helper/storage';
 
 describe('helpers storage', function() {
+  var storage;
+  beforeEach(function() {
+    storage = new Storage({});
+  });
   describe('setItem', function() {
     beforeEach(function() {
       spy(StorageHandler.prototype, 'setItem');
diff --git a/test/web-auth/cross-origin-authentication.test.js b/test/web-auth/cross-origin-authentication.test.js
index fb8fbf1..ce37a05 100644
--- a/test/web-auth/cross-origin-authentication.test.js
+++ b/test/web-auth/cross-origin-authentication.test.js
@@ -2,15 +2,11 @@ import expect from 'expect.js';
 import { stub, spy } from 'sinon';
 import request from 'superagent';
 
-import storage from '../../src/helper/storage';
-import IframeHandler from '../../src/helper/iframe-handler';
+import Storage from '../../src/helper/storage';
 import * as times from '../../src/helper/times';
 import RequestMock from '../mock/request-mock';
 
-import TransactionManager from '../../src/web-auth/transaction-manager';
-import SilentAuthenticationHandler from '../../src/web-auth/silent-authentication-handler';
 import CrossOriginAuthentication from '../../src/web-auth/cross-origin-authentication';
-import WebAuth from '../../src/web-auth';
 import windowHelper from '../../src/helper/window';
 import WebMessageHandler from '../../src/web-auth/web-message-handler';
 
@@ -30,11 +26,11 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
       global.window = {};
     });
     beforeEach(function() {
-      spy(storage, 'setItem');
+      spy(Storage.prototype, 'setItem');
     });
     afterEach(function() {
       request.post.restore();
-      storage.setItem.restore();
+      Storage.prototype.setItem.restore();
       this.webAuthSpy.authorize = spy();
       if (windowHelper.redirect.restore) {
         windowHelper.redirect.restore();
@@ -279,7 +275,7 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
         realm: 'email'
       });
     });
-    it('should call /co/authenticate and save the verifier in sessionStorage', function() {
+    it('should call /co/authenticate and save the verifier in storage', function() {
       stub(request, 'post', function(url) {
         expect(url).to.be('https://me.auth0.com/co/authenticate');
         return new RequestMock({
@@ -308,8 +304,8 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
         password: '123456',
         anotherOption: 'foobar'
       });
-      expect(storage.setItem.callCount).to.be(1);
-      expect(storage.setItem.firstCall.args).to.be.eql([
+      expect(Storage.prototype.setItem.callCount).to.be(1);
+      expect(Storage.prototype.setItem.firstCall.args).to.be.eql([
         'co/verifier/https%3A%2F%2Fme.auth0.com/co_id',
         'co_verifier',
         { expires: times.MINUTES_15 }
@@ -421,10 +417,6 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
           redirectUri: 'https://page.com/callback'
         }
       );
-      stub(storage, 'getItem', function(key) {
-        expect(key).to.be('co/verifier/https%3A%2F%2Fme.auth0.com/co_id');
-        return 'co_verifier';
-      });
       global.window = {
         addEventListener: spy(),
         parent: {
@@ -436,10 +428,15 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
       };
     });
     beforeEach(function() {
-      spy(storage, 'removeItem');
+      spy(Storage.prototype, 'removeItem');
+      stub(Storage.prototype, 'getItem', function(key) {
+        expect(key).to.be('co/verifier/https%3A%2F%2Fme.auth0.com/co_id');
+        return 'co_verifier';
+      });
     });
     afterEach(function() {
-      storage.removeItem.restore();
+      Storage.prototype.getItem.restore();
+      Storage.prototype.removeItem.restore();
     });
     it('should call parent.postMessage on load', function() {
       this.co.callback();
@@ -462,9 +459,9 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
           }
         };
         theCall.args[1](evt);
-        expect(storage.removeItem.called).to.be(false);
+        expect(Storage.prototype.removeItem.called).to.be(false);
       });
-      it('should remove item from sessionStorage', function() {
+      it('should remove item from storage', function() {
         this.co.callback();
         var onMessageHandler = global.window.addEventListener.getCall(0).args[1];
         var evt = {
@@ -480,7 +477,7 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
           }
         };
         onMessageHandler(evt);
-        var theCall = storage.removeItem.getCall(0);
+        var theCall = Storage.prototype.removeItem.getCall(0);
         expect(theCall.args[0]).to.be('co/verifier/https%3A%2F%2Fme.auth0.com/co_id');
       });
       it('should send the verifier response', function() {
@@ -506,9 +503,9 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
         });
         expect(theCall.args[1]).to.be('https://me.auth0.com');
       });
-      it('should send empty verifier in the response when sessionStorage can not be accessed', function() {
-        storage.getItem.restore();
-        stub(storage, 'getItem', function() {
+      it('should send empty verifier in the response when storage can not be accessed', function() {
+        Storage.prototype.getItem.restore();
+        stub(Storage.prototype, 'getItem', function() {
           throw new Error('');
         });
         this.co.callback();
@@ -532,7 +529,6 @@ describe('auth0.WebAuth.crossOriginAuthentication', function() {
           response: { verifier: '' }
         });
         expect(theCall.args[1]).to.be('https://me.auth0.com');
-        storage.getItem.restore();
       });
     });
   });
diff --git a/test/web-auth/popup.test.js b/test/web-auth/popup.test.js
index fb2e38f..3e0fd38 100644
--- a/test/web-auth/popup.test.js
+++ b/test/web-auth/popup.test.js
@@ -8,7 +8,7 @@ import request from 'superagent';
 
 import PopupHandler from '../../src/helper/popup-handler';
 import windowHandler from '../../src/helper/window';
-import storage from '../../src/helper/storage';
+import Storage from '../../src/helper/storage';
 import WebAuth from '../../src/web-auth';
 import CrossOriginAuthentication from '../../src/web-auth/cross-origin-authentication';
 import TransactionManager from '../../src/web-auth/transaction-manager';
@@ -94,11 +94,11 @@ describe('auth0.WebAuth.popup', function() {
         expect(url).to.be(
           'https://me.auth0.com/authorize?client_id=...&response_type=id_token&redirect_uri=http%3A%2F%2Fpage.com%2Fcallback&connection=the_connection&state=123&nonce=456&scope=openid'
         );
-        storage.setItem.restore();
+        Storage.prototype.setItem.restore();
         TransactionManager.prototype.process.restore();
         done();
       });
-      stub(storage, 'setItem', function() {});
+      stub(Storage.prototype, 'setItem', function() {});
       stub(TransactionManager.prototype, 'process', function(options) {
         return options;
       });
diff --git a/test/web-auth/transaction-manager.test.js b/test/web-auth/transaction-manager.test.js
index 98c7778..9c6b632 100644
--- a/test/web-auth/transaction-manager.test.js
+++ b/test/web-auth/transaction-manager.test.js
@@ -2,13 +2,12 @@ import expect from 'expect.js';
 import { stub, spy } from 'sinon';
 
 import TransactionManager from '../../src/web-auth/transaction-manager';
-import objectHelper from '../../src/helper/object';
 import random from '../../src/helper/random';
-import storage from '../../src/helper/storage';
+import Storage from '../../src/helper/storage';
 
 context('TransactionManager', function() {
   beforeEach(function() {
-    this.tm = new TransactionManager();
+    this.tm = new TransactionManager({});
   });
   context('process', function() {
     beforeEach(function() {
@@ -77,18 +76,18 @@ context('TransactionManager', function() {
       stub(random, 'randomString', function() {
         return 'randomString';
       });
-      stub(storage, 'setItem');
+      stub(Storage.prototype, 'setItem');
     });
     afterEach(function() {
       random.randomString.restore();
-      storage.setItem.restore();
+      Storage.prototype.setItem.restore();
     });
     it('always stores random state', function() {
       var result = this.tm.generateTransaction('appState', null, null, null, false);
       expect(result).to.be.eql({ state: 'randomString', nonce: null });
-      expect(storage.setItem.calledOnce).to.be(true);
-      expect(storage.setItem.lastCall.args[0]).to.be('com.auth0.auth.randomString');
-      expect(storage.setItem.lastCall.args[1]).to.be.eql({
+      expect(Storage.prototype.setItem.calledOnce).to.be(true);
+      expect(Storage.prototype.setItem.lastCall.args[0]).to.be('com.auth0.auth.randomString');
+      expect(Storage.prototype.setItem.lastCall.args[1]).to.be.eql({
         nonce: null,
         appState: 'appState',
         state: 'randomString',
@@ -98,9 +97,9 @@ context('TransactionManager', function() {
     it('only stores random nonce when generateNonce is true', function() {
       var result = this.tm.generateTransaction('appState', null, null, null, true);
       expect(result).to.be.eql({ state: 'randomString', nonce: 'randomString' });
-      expect(storage.setItem.calledOnce).to.be(true);
-      expect(storage.setItem.lastCall.args[0]).to.be('com.auth0.auth.randomString');
-      expect(storage.setItem.lastCall.args[1]).to.be.eql({
+      expect(Storage.prototype.setItem.calledOnce).to.be(true);
+      expect(Storage.prototype.setItem.lastCall.args[0]).to.be('com.auth0.auth.randomString');
+      expect(Storage.prototype.setItem.lastCall.args[1]).to.be.eql({
         nonce: 'randomString',
         appState: 'appState',
         state: 'randomString',
@@ -110,9 +109,9 @@ context('TransactionManager', function() {
     it('uses nonce/state when provided', function() {
       var result = this.tm.generateTransaction('appState', 'state', 'nonce', null);
       expect(result).to.be.eql({ state: 'state', nonce: 'nonce' });
-      expect(storage.setItem.calledOnce).to.be(true);
-      expect(storage.setItem.lastCall.args[0]).to.be('com.auth0.auth.state');
-      expect(storage.setItem.lastCall.args[1]).to.be.eql({
+      expect(Storage.prototype.setItem.calledOnce).to.be(true);
+      expect(Storage.prototype.setItem.lastCall.args[0]).to.be('com.auth0.auth.state');
+      expect(Storage.prototype.setItem.lastCall.args[1]).to.be.eql({
         nonce: 'nonce',
         appState: 'appState',
         state: 'state',
@@ -122,9 +121,9 @@ context('TransactionManager', function() {
     it('uses lastUsedConnection when provided', function() {
       var result = this.tm.generateTransaction('appState', 'state', 'nonce', 'lastUsedConnection');
       expect(result).to.be.eql({ state: 'state', nonce: 'nonce' });
-      expect(storage.setItem.calledOnce).to.be(true);
-      expect(storage.setItem.lastCall.args[0]).to.be('com.auth0.auth.state');
-      expect(storage.setItem.lastCall.args[1]).to.be.eql({
+      expect(Storage.prototype.setItem.calledOnce).to.be(true);
+      expect(Storage.prototype.setItem.lastCall.args[0]).to.be('com.auth0.auth.state');
+      expect(Storage.prototype.setItem.lastCall.args[1]).to.be.eql({
         nonce: 'nonce',
         appState: 'appState',
         state: 'state',
@@ -134,14 +133,14 @@ context('TransactionManager', function() {
   });
   context('getStoredTransaction', function() {
     beforeEach(function() {
-      stub(storage, 'getItem', function(state) {
+      stub(Storage.prototype, 'getItem', function(state) {
         expect(state).to.be('com.auth0.auth.state');
         return { from: 'storage' };
       });
       spy(TransactionManager.prototype, 'clearTransaction');
     });
     afterEach(function() {
-      storage.getItem.restore();
+      Storage.prototype.getItem.restore();
       TransactionManager.prototype.clearTransaction.restore();
     });
     it('returns transaction data from storage', function() {
@@ -155,15 +154,15 @@ context('TransactionManager', function() {
   });
   context('clearTransaction', function() {
     beforeEach(function() {
-      stub(storage, 'removeItem');
+      stub(Storage.prototype, 'removeItem');
     });
     afterEach(function() {
-      storage.removeItem.restore();
+      Storage.prototype.removeItem.restore();
     });
     it('removes data from storage', function() {
       this.tm.clearTransaction('state');
-      expect(storage.removeItem.calledOnce).to.be(true);
-      expect(storage.removeItem.lastCall.args[0]).to.be('com.auth0.auth.state');
+      expect(Storage.prototype.removeItem.calledOnce).to.be(true);
+      expect(Storage.prototype.removeItem.lastCall.args[0]).to.be('com.auth0.auth.state');
     });
   });
 });
diff --git a/test/web-auth/web-auth.test.js b/test/web-auth/web-auth.test.js
index 6c60cb4..6202297 100644
--- a/test/web-auth/web-auth.test.js
+++ b/test/web-auth/web-auth.test.js
@@ -5,7 +5,7 @@ import request from 'superagent';
 import IdTokenVerifier from 'idtoken-verifier';
 
 import windowHelper from '../../src/helper/window';
-import ssodata from '../../src/helper/ssodata';
+import SSODataStorage from '../../src/helper/ssodata';
 import Warn from '../../src/helper/warn';
 
 import RequestMock from '../mock/request-mock';
@@ -266,13 +266,13 @@ describe('auth0.WebAuth', function() {
         state: 'foo',
         appState: null
       });
-      spy(ssodata, 'set');
+      spy(SSODataStorage.prototype, 'set');
       stub(IdTokenVerifier.prototype, 'validateAccessToken', function(at, alg, atHash, cb) {
         cb(null);
       });
     });
     afterEach(function() {
-      ssodata.set.restore();
+      SSODataStorage.prototype.set.restore();
       if (IdTokenVerifier.prototype.validateAccessToken.restore) {
         IdTokenVerifier.prototype.validateAccessToken.restore();
       }
@@ -569,8 +569,11 @@ describe('auth0.WebAuth', function() {
                 '#state=foo&access_token=VjubIMBmpgQ2W2&token_type=Bearer&refresh_token=kajshdgfkasdjhgfas'
             },
             function() {
-              expect(ssodata.set.calledOnce).to.be.ok();
-              expect(ssodata.set.firstCall.args).to.be.eql(['lastUsedConnection', undefined]);
+              expect(SSODataStorage.prototype.set.calledOnce).to.be.ok();
+              expect(SSODataStorage.prototype.set.firstCall.args).to.be.eql([
+                'lastUsedConnection',
+                undefined
+              ]);
               expect(TransactionManager.prototype.getStoredTransaction.calledOnce).to.be.ok();
               done();
             }
@@ -584,8 +587,8 @@ describe('auth0.WebAuth', function() {
               nonce: 'wEOe7-LC8nl1AuHp7nucF_5LMVPVkMBY'
             },
             function() {
-              expect(ssodata.set.calledOnce).to.be.ok();
-              expect(ssodata.set.firstCall.args).to.be.eql([
+              expect(SSODataStorage.prototype.set.calledOnce).to.be.ok();
+              expect(SSODataStorage.prototype.set.firstCall.args).to.be.eql([
                 'lastUsedConnection',
                 'auth0|59fbe11937039b263a8b29a2'
               ]);
@@ -2316,7 +2319,7 @@ describe('auth0.WebAuth', function() {
       stub(TransactionManager.prototype, 'process', function(params) {
         return Object.assign({}, params, { from: 'transaction-manager' });
       });
-      spy(TransactionManager.prototype, 'clearTransaction');
+      stub(TransactionManager.prototype, 'clearTransaction', function() {});
       stub(windowHelper, 'getOrigin', function() {
         return 'https://test-origin.com';
       });