Repository: auth0/auth0.js
Pull request #615 (2017-12-29 18:48:49)

Validate current window origin and redirecturi origin to prevent mismatches


Patch:
diff --git a/src/helper/object.js b/src/helper/object.js
index 0f7597d..468f623 100644
--- a/src/helper/object.js
+++ b/src/helper/object.js
@@ -4,6 +4,7 @@
 
 var assert = require('./assert');
 var objectAssign = require('./object-assign');
+var windowHelper = require('./window');
 
 function pick(object, keys) {
   return keys.reduce(function(prev, key) {
@@ -117,6 +118,16 @@ function toCamelCase(object, exceptions) {
   }, {});
 }
 
+function getOriginFromUrl(url) {
+  if (!url) {
+    return undefined;
+  }
+  var doc = windowHelper.getDocument();
+  var anchor = doc.createElement('a');
+  anchor.href = url;
+  return anchor.origin;
+}
+
 module.exports = {
   toSnakeCase: toSnakeCase,
   toCamelCase: toCamelCase,
@@ -124,5 +135,6 @@ module.exports = {
   merge: merge,
   pick: pick,
   getKeysNotIn: getKeysNotIn,
-  extend: extend
+  extend: extend,
+  getOriginFromUrl: getOriginFromUrl
 };
diff --git a/src/helper/window.js b/src/helper/window.js
index 3444b06..2899c15 100644
--- a/src/helper/window.js
+++ b/src/helper/window.js
@@ -10,8 +10,19 @@ function getWindow() {
   return global.window;
 }
 
+function getOrigin() {
+  var location = global.window.location;
+  var origin = location.origin;
+  if (!origin) {
+    origin =
+      location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
+  }
+  return origin;
+}
+
 module.exports = {
   redirect: redirect,
   getDocument: getDocument,
-  getWindow: getWindow
+  getWindow: getWindow,
+  getOrigin: getOrigin
 };
diff --git a/src/web-auth/index.js b/src/web-auth/index.js
index 23c794c..8d6b0f0 100644
--- a/src/web-auth/index.js
+++ b/src/web-auth/index.js
@@ -377,6 +377,7 @@ WebAuth.prototype.checkSession = function(options, cb) {
     .merge(this.baseOptions, [
       'clientID',
       'responseType',
+      'redirectUri',
       'scope',
       'audience',
       '_csrf',
@@ -387,7 +388,7 @@ WebAuth.prototype.checkSession = function(options, cb) {
     .with(options);
 
   if (params.responseType === 'code') {
-    return cb(new Error("responseType can't be `code`"));
+    return cb({ error: 'error', error_description: "responseType can't be `code`" });
   }
 
   if (!options.nonce) {
diff --git a/src/web-auth/web-message-handler.js b/src/web-auth/web-message-handler.js
index 0523839..3e4cc0d 100644
--- a/src/web-auth/web-message-handler.js
+++ b/src/web-auth/web-message-handler.js
@@ -1,5 +1,6 @@
 var IframeHandler = require('../helper/iframe-handler');
 var objectHelper = require('../helper/object');
+var windowHelper = require('../helper/window');
 
 function runWebMessageFlow(authorizeUrl, options, callback) {
   var handler = new IframeHandler({
@@ -11,8 +12,10 @@ function runWebMessageFlow(authorizeUrl, options, callback) {
     timeout: options.timeout,
     eventValidator: {
       isValid: function(eventData) {
-        return eventData.event.data.type === 'authorization_response' 
-          && options.state === eventData.event.data.response.state;
+        return (
+          eventData.event.data.type === 'authorization_response' &&
+          options.state === eventData.event.data.response.state
+        );
       }
     },
     timeoutCallback: function() {
@@ -33,6 +36,20 @@ WebMessageHandler.prototype.run = function(options, cb) {
   var _this = this;
   options.responseMode = 'web_message';
   options.prompt = 'none';
+
+  var currentOrigin = windowHelper.getOrigin();
+  var redirectUriOrigin = objectHelper.getOriginFromUrl(options.redirectUri);
+  if (currentOrigin !== redirectUriOrigin) {
+    return cb({
+      error: 'origin_mismatch',
+      error_description: "The redirectUri's origin (" +
+        redirectUriOrigin +
+        ") should match the window's origin (" +
+        currentOrigin +
+        ').'
+    });
+  }
+
   runWebMessageFlow(this.webAuth.client.buildAuthorizeUrl(options), options, function(
     err,
     eventData


Test patch:
diff --git a/test/helper/object.test.js b/test/helper/object.test.js
index 2c70c47..915405d 100644
--- a/test/helper/object.test.js
+++ b/test/helper/object.test.js
@@ -3,6 +3,7 @@ var stub = require('sinon').stub;
 
 var objectAssign = require('../../src/helper/object-assign');
 var objectHelper = require('../../src/helper/object');
+var windowHelper = require('../../src/helper/window');
 
 describe('helpers', function() {
   describe('getKeysNotIn', function() {
@@ -515,4 +516,28 @@ describe('helpers', function() {
       });
     });
   });
+  describe('getOriginFromUrl', function() {
+    it('should return undefined if there is no url', function() {
+      expect(objectHelper.getOriginFromUrl()).to.be(undefined);
+      expect(objectHelper.getOriginFromUrl('')).to.be(undefined);
+      expect(objectHelper.getOriginFromUrl(null)).to.be(undefined);
+    });
+    it('should use an anchor to parse the url and return the origin', function() {
+      var anchor = {
+        origin: 'https://test.com'
+      };
+      stub(windowHelper, 'getDocument', function() {
+        return {
+          createElement: function createElement(e) {
+            expect(e).to.be('a');
+            return anchor;
+          }
+        };
+      });
+      var url = 'https://test.com/example';
+      expect(objectHelper.getOriginFromUrl(url)).to.be('https://test.com');
+      expect(anchor.href).to.be(url);
+      windowHelper.getDocument.restore();
+    });
+  });
 });
diff --git a/test/helper/window.test.js b/test/helper/window.test.js
index 33a8020..2f95de2 100644
--- a/test/helper/window.test.js
+++ b/test/helper/window.test.js
@@ -22,4 +22,14 @@ describe('helpers window', function() {
     var _window = windowHelper.getWindow();
     expect(_window).to.eql({ document: { body: {} }, location: '' });
   });
+  describe('getOrigin', function() {
+    it('should use window.location.origin when available', function() {
+      global.window = { location: { origin: 'origin' } };
+      expect(windowHelper.getOrigin()).to.be('origin');
+    });
+    it('should build current origin when location.origin is not available', function() {
+      global.window = { location: { protocol: 'http:', hostname: 'hostname', port: 30 } };
+      expect(windowHelper.getOrigin()).to.be('http://hostname:30');
+    });
+  });
 });
diff --git a/test/web-auth/web-auth.test.js b/test/web-auth/web-auth.test.js
index 16f5b40..e0502cf 100644
--- a/test/web-auth/web-auth.test.js
+++ b/test/web-auth/web-auth.test.js
@@ -1623,6 +1623,12 @@ describe('auth0.WebAuth', function() {
       stub(TransactionManager.prototype, 'process', function(params) {
         return Object.assign({}, params, { from: 'transaction-manager' });
       });
+      stub(windowHelper, 'getOrigin', function() {
+        return 'https://test-origin.com';
+      });
+      stub(objectHelper, 'getOriginFromUrl', function() {
+        return 'https://test-origin.com';
+      });
     });
     afterEach(function() {
       TransactionManager.prototype.process.restore();
@@ -1632,10 +1638,27 @@ describe('auth0.WebAuth', function() {
       if (WebAuth.prototype.validateAuthenticationResponse.restore) {
         WebAuth.prototype.validateAuthenticationResponse.restore();
       }
+      windowHelper.getOrigin.restore();
+      objectHelper.getOriginFromUrl.restore();
     });
     it('throws an error if responseType is code', function() {
       this.auth0.checkSession({ responseType: 'code' }, function(err) {
-        expect(err.message).to.be.eql("responseType can't be `code`");
+        expect(err).to.be.eql({
+          error: 'error',
+          error_description: "responseType can't be `code`"
+        });
+      });
+    });
+    it('throws an error if there is an origin mismatch between current window and redirectUrl', function() {
+      objectHelper.getOriginFromUrl.restore();
+      stub(objectHelper, 'getOriginFromUrl', function() {
+        return 'some-other-origin';
+      });
+      this.auth0.checkSession({}, function(err) {
+        expect(err).to.be.eql({
+          error: 'origin_mismatch',
+          error_description: "The redirectUri's origin (some-other-origin) should match the window's origin (https://test-origin.com)."
+        });
       });
     });
     it('inits IframeHandler with correct params', function(done) {
@@ -1714,6 +1737,7 @@ describe('auth0.WebAuth', function() {
         expect(options).to.be.eql({
           clientID: '...',
           responseType: 'token',
+          redirectUri: 'http://page.com/callback',
           from: 'transaction-manager',
           responseMode: 'web_message',
           prompt: 'none'