Repository: auth0/auth0.js
Pull request #1232 (2022-01-20 12:28:03)

Add compatibility cookie for SameSite, with option to turn it off
### Changes

#1229 introduced a change that set `sameSite` to `none` when using HTTPS. This can cause problems for older user agents that don't support this. This PR introduces a compatibility cookie where `secure` is set and `sameSite` is left blank, much like [Auth0 Server behaviour](https://auth0.com/docs/manage-users/cookies/samesite-cookie-attribute-changes) as well as in [Auth0 SPA SDK](https://auth0.github.io/auth0-spa-js/interfaces/auth0clientoptions.html#legacysamesitecookie).

See https://www.chromium.org/updates/same-site/incompatible-clients for information on which browsers are affected.

In addition, a new configuration option `legacySameSiteCookie` is introduced to control this behaviour. It defaults to `true` when not configured, which enables the legacy SameSite cookie behaviour. Setting `legacySameSiteCookie` to `false` disables this behaviour.

### References

#1229

### Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors.

- [x] This change adds unit test coverage
- [ ] This change adds integration test coverage

### Checklist

- [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [x] All tests and linters described in the [Develop section](https://github.com/auth0/auth0.js#develop) run without errors


stevehobbsdev: Note for reviewers: looks like the diff has a lot of formatting changes in, I can recommend turning off whitespace changes to dilute it a bit.

Patch:
diff --git a/src/helper/storage/cookie.js b/src/helper/storage/cookie.js
index 300c8ad..649ee25 100644
--- a/src/helper/storage/cookie.js
+++ b/src/helper/storage/cookie.js
@@ -1,14 +1,24 @@
 import Cookie from 'js-cookie';
 import objectHelper from '../object';
 import windowHandler from '../window';
-function CookieStorage() {}
+
+function buildCompatCookieKey(key) {
+  return '_' + key + '_compat';
+}
+
+function CookieStorage(options) {
+  this._options = options || {};
+}
 
 CookieStorage.prototype.getItem = function (key) {
-  return Cookie.get(key);
+  var cookie = Cookie.get(key);
+
+  return cookie || Cookie.get(buildCompatCookieKey(key));
 };
 
 CookieStorage.prototype.removeItem = function (key) {
   Cookie.remove(key);
+  Cookie.remove(buildCompatCookieKey(key));
 };
 
 CookieStorage.prototype.setItem = function (key, value, options) {
@@ -22,6 +32,12 @@ CookieStorage.prototype.setItem = function (key, value, options) {
   if (windowHandler.getWindow().location.protocol === 'https:') {
     params.secure = true;
     params.sameSite = 'none';
+
+    if (this._options.legacySameSiteCookie) {
+      // Save a compatibility cookie without sameSite='none' for browsers that don't support it.
+      var legacyOptions = objectHelper.blacklist(params, ['sameSite']);
+      Cookie.set(buildCompatCookieKey(key), value, legacyOptions);
+    }
   }
 
   Cookie.set(key, value, params);
diff --git a/src/helper/storage/handler.js b/src/helper/storage/handler.js
index df6a7ee..5f81c40 100644
--- a/src/helper/storage/handler.js
+++ b/src/helper/storage/handler.js
@@ -5,10 +5,12 @@ import Warn from '../warn';
 
 function StorageHandler(options) {
   this.warn = new Warn({});
-  this.storage = new CookieStorage();
+  this.storage = new CookieStorage(options);
+
   if (options.__tryLocalStorageFirst !== true) {
     return;
   }
+
   try {
     // some browsers throw an error when trying to access localStorage
     // when localStorage is disabled.
@@ -22,7 +24,7 @@ function StorageHandler(options) {
   }
 }
 
-StorageHandler.prototype.failover = function() {
+StorageHandler.prototype.failover = function () {
   if (this.storage instanceof DummyStorage) {
     this.warn.warning('DummyStorage: ignore failover');
     return;
@@ -35,7 +37,7 @@ StorageHandler.prototype.failover = function() {
   }
 };
 
-StorageHandler.prototype.getItem = function(key) {
+StorageHandler.prototype.getItem = function (key) {
   try {
     return this.storage.getItem(key);
   } catch (e) {
@@ -45,7 +47,7 @@ StorageHandler.prototype.getItem = function(key) {
   }
 };
 
-StorageHandler.prototype.removeItem = function(key) {
+StorageHandler.prototype.removeItem = function (key) {
   try {
     return this.storage.removeItem(key);
   } catch (e) {
@@ -55,7 +57,7 @@ StorageHandler.prototype.removeItem = function(key) {
   }
 };
 
-StorageHandler.prototype.setItem = function(key, value, options) {
+StorageHandler.prototype.setItem = function (key, value, options) {
   try {
     return this.storage.setItem(key, value, options);
   } catch (e) {
diff --git a/src/web-auth/index.js b/src/web-auth/index.js
index 8289c9e..5d286c5 100644
--- a/src/web-auth/index.js
+++ b/src/web-auth/index.js
@@ -39,6 +39,7 @@ function defaultClock() {
  * @param {String} [options.organization] the Id of an organization to log in to
  * @param {String} [options.invitation] the ID of an invitation to accept. This is available from the user invitation URL that is given when participating in a user invitation flow
  * @param {Array} [options.plugins]
+ * @param {Boolean} [options.legacySameSiteCookie] set this to `false` to disable the legacy compatibility cookie that is created for older browsers that don't support the SameSite attribute (defaults to `true`)
  * @param {Number} [options._timesToRetryFailedRequests] Number of times to retry a failed request, according to {@link https://github.com/visionmedia/superagent/blob/master/lib/request-base.js}
  * @see {@link https://auth0.com/docs/api/authentication}
  */
@@ -96,6 +97,11 @@ function WebAuth(options) {
         type: 'number',
         message: 'stateExpiration is not valid'
       },
+      legacySameSiteCookie: {
+        optional: true,
+        type: 'boolean',
+        message: 'legacySameSiteCookie option is not valid'
+      },
       _disableDeprecationWarnings: {
         optional: true,
         type: 'boolean',
@@ -155,9 +161,10 @@ function WebAuth(options) {
       ? this.baseOptions._sendTelemetry
       : true;
 
-  this.baseOptions._timesToRetryFailedRequests = options._timesToRetryFailedRequests
-    ? parseInt(options._timesToRetryFailedRequests)
-    : 0;
+  this.baseOptions._timesToRetryFailedRequests =
+    options._timesToRetryFailedRequests
+      ? parseInt(options._timesToRetryFailedRequests)
+      : 0;
 
   this.baseOptions.tenant =
     (this.baseOptions.overrides && this.baseOptions.overrides.__tenant) ||
@@ -170,6 +177,10 @@ function WebAuth(options) {
   this.baseOptions.jwksURI =
     this.baseOptions.overrides && this.baseOptions.overrides.__jwks_uri;
 
+  if (options.legacySameSiteCookie !== false) {
+    this.baseOptions.legacySameSiteCookie = true;
+  }
+
   this.transactionManager = new TransactionManager(this.baseOptions);
 
   this.client = new Authentication(this.baseOptions);
@@ -209,7 +220,7 @@ function WebAuth(options) {
  * @param {authorizeCallback} cb
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.parseHash = function(options, cb) {
+WebAuth.prototype.parseHash = function (options, cb) {
   var parsedQs;
   var err;
 
@@ -296,7 +307,7 @@ WebAuth.prototype.parseHash = function(options, cb) {
  * @param {authorizeCallback} cb
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.validateAuthenticationResponse = function(
+WebAuth.prototype.validateAuthenticationResponse = function (
   options,
   parsedHash,
   cb
@@ -330,7 +341,7 @@ WebAuth.prototype.validateAuthenticationResponse = function(
   var transactionOrganization = transaction && transaction.organization;
   var appState = options.state || (transaction && transaction.appState) || null;
 
-  var callback = function(err, payload) {
+  var callback = function (err, payload) {
     if (err) {
       return cb(err);
     }
@@ -352,109 +363,110 @@ WebAuth.prototype.validateAuthenticationResponse = function(
     return callback(null, null);
   }
 
-  return this.validateToken(parsedHash.id_token, transactionNonce, function(
-    validationError,
-    payload
-  ) {
-    if (!validationError) {
-      // Verify the organization
-      if (transactionOrganization) {
-        if (!payload.org_id) {
-          return callback(
-            error.invalidToken(
-              'Organization Id (org_id) claim must be a string present in the ID token'
-            )
-          );
+  return this.validateToken(
+    parsedHash.id_token,
+    transactionNonce,
+    function (validationError, payload) {
+      if (!validationError) {
+        // Verify the organization
+        if (transactionOrganization) {
+          if (!payload.org_id) {
+            return callback(
+              error.invalidToken(
+                'Organization Id (org_id) claim must be a string present in the ID token'
+              )
+            );
+          }
+
+          if (payload.org_id !== transactionOrganization) {
+            return callback(
+              error.invalidToken(
+                'Organization Id (org_id) claim value mismatch in the ID token; expected "' +
+                  transactionOrganization +
+                  '", found "' +
+                  payload.org_id +
+                  '"'
+              )
+            );
+          }
         }
 
-        if (payload.org_id !== transactionOrganization) {
-          return callback(
-            error.invalidToken(
-              'Organization Id (org_id) claim value mismatch in the ID token; expected "' +
-                transactionOrganization +
-                '", found "' +
-                payload.org_id +
-                '"'
-            )
-          );
+        if (!parsedHash.access_token) {
+          return callback(null, payload);
         }
+
+        // id_token's generated by non-oidc applications don't have at_hash
+        if (!payload.at_hash) {
+          return callback(null, payload);
+        }
+
+        // here we're absolutely sure that the id_token's alg is RS256
+        // and that the id_token is valid, so we can check the access_token
+        return new IdTokenVerifier().validateAccessToken(
+          parsedHash.access_token,
+          'RS256',
+          payload.at_hash,
+          function (err) {
+            if (err) {
+              return callback(error.invalidToken(err.message));
+            }
+            return callback(null, payload);
+          }
+        );
       }
 
-      if (!parsedHash.access_token) {
-        return callback(null, payload);
+      if (
+        validationError.error !== 'invalid_token' ||
+        (validationError.errorDescription &&
+          validationError.errorDescription.indexOf(
+            'Nonce (nonce) claim value mismatch in the ID token'
+          ) > -1)
+      ) {
+        return callback(validationError);
+      }
+
+      // if it's an invalid_token error, decode the token
+      var decodedToken = new IdTokenVerifier().decode(parsedHash.id_token);
+
+      // if the alg is not HS256, return the raw error
+      if (decodedToken.header.alg !== 'HS256') {
+        return callback(validationError);
       }
 
-      // id_token's generated by non-oidc applications don't have at_hash
-      if (!payload.at_hash) {
-        return callback(null, payload);
+      if ((decodedToken.payload.nonce || null) !== transactionNonce) {
+        return callback({
+          error: 'invalid_token',
+          errorDescription:
+            'Nonce (nonce) claim value mismatch in the ID token; expected "' +
+            transactionNonce +
+            '", found "' +
+            decodedToken.payload.nonce +
+            '"'
+        });
       }
 
-      // here we're absolutely sure that the id_token's alg is RS256
-      // and that the id_token is valid, so we can check the access_token
-      return new IdTokenVerifier().validateAccessToken(
+      if (!parsedHash.access_token) {
+        var noAccessTokenError = {
+          error: 'invalid_token',
+          description:
+            'The id_token cannot be validated because it was signed with the HS256 algorithm and public clients (like a browser) can’t store secrets. Please read the associated doc for possible ways to fix this. Read more: https://auth0.com/docs/errors/libraries/auth0-js/invalid-token#parsing-an-hs256-signed-id-token-without-an-access-token'
+        };
+        return callback(noAccessTokenError);
+      }
+
+      // if the alg is HS256, use the /userinfo endpoint to build the payload
+      return _this.client.userInfo(
         parsedHash.access_token,
-        'RS256',
-        payload.at_hash,
-        function(err) {
-          if (err) {
-            return callback(error.invalidToken(err.message));
+        function (errUserInfo, profile) {
+          // if the /userinfo request fails, use the validationError instead
+          if (errUserInfo) {
+            return callback(errUserInfo);
           }
-          return callback(null, payload);
+          return callback(null, profile);
         }
       );
     }
-
-    if (
-      validationError.error !== 'invalid_token' ||
-      (validationError.errorDescription &&
-        validationError.errorDescription.indexOf(
-          'Nonce (nonce) claim value mismatch in the ID token'
-        ) > -1)
-    ) {
-      return callback(validationError);
-    }
-
-    // if it's an invalid_token error, decode the token
-    var decodedToken = new IdTokenVerifier().decode(parsedHash.id_token);
-
-    // if the alg is not HS256, return the raw error
-    if (decodedToken.header.alg !== 'HS256') {
-      return callback(validationError);
-    }
-
-    if ((decodedToken.payload.nonce || null) !== transactionNonce) {
-      return callback({
-        error: 'invalid_token',
-        errorDescription:
-          'Nonce (nonce) claim value mismatch in the ID token; expected "' +
-          transactionNonce +
-          '", found "' +
-          decodedToken.payload.nonce +
-          '"'
-      });
-    }
-
-    if (!parsedHash.access_token) {
-      var noAccessTokenError = {
-        error: 'invalid_token',
-        description:
-          'The id_token cannot be validated because it was signed with the HS256 algorithm and public clients (like a browser) can’t store secrets. Please read the associated doc for possible ways to fix this. Read more: https://auth0.com/docs/errors/libraries/auth0-js/invalid-token#parsing-an-hs256-signed-id-token-without-an-access-token'
-      };
-      return callback(noAccessTokenError);
-    }
-
-    // if the alg is HS256, use the /userinfo endpoint to build the payload
-    return _this.client.userInfo(parsedHash.access_token, function(
-      errUserInfo,
-      profile
-    ) {
-      // if the /userinfo request fails, use the validationError instead
-      if (errUserInfo) {
-        return callback(errUserInfo);
-      }
-      return callback(null, profile);
-    });
-  });
+  );
 };
 
 function buildParseHashResponse(qsParams, appState, token) {
@@ -486,7 +498,7 @@ function buildParseHashResponse(qsParams, appState, token) {
  * @param {String} nonce
  * @param {validateTokenCallback} cb
  */
-WebAuth.prototype.validateToken = function(token, nonce, cb) {
+WebAuth.prototype.validateToken = function (token, nonce, cb) {
   var verifier = new IdTokenVerifier({
     issuer: this.baseOptions.token_issuer,
     jwksURI: this.baseOptions.jwksURI,
@@ -496,7 +508,7 @@ WebAuth.prototype.validateToken = function(token, nonce, cb) {
     __clock: this.baseOptions.__clock || defaultClock
   });
 
-  verifier.verify(token, nonce, function(err, payload) {
+  verifier.verify(token, nonce, function (err, payload) {
     if (err) {
       return cb(error.invalidToken(err.message));
     }
@@ -528,7 +540,7 @@ WebAuth.prototype.validateToken = function(token, nonce, cb) {
  * @see {@link https://auth0.com/docs/api/authentication#authorize-client}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.renewAuth = function(options, cb) {
+WebAuth.prototype.renewAuth = function (options, cb) {
   var handler;
   var usePostMessage = !!options.usePostMessage;
   var postMessageDataType = options.postMessageDataType || false;
@@ -577,7 +589,7 @@ WebAuth.prototype.renewAuth = function(options, cb) {
     timeout: timeout
   });
 
-  handler.login(usePostMessage, function(err, hash) {
+  handler.login(usePostMessage, function (err, hash) {
     if (typeof hash === 'object') {
       // hash was already parsed, so we just return it.
       // it's here to be backwards compatible and should be removed in the next major version.
@@ -603,7 +615,7 @@ WebAuth.prototype.renewAuth = function(options, cb) {
  * @see {@link https://auth0.com/docs/libraries/auth0js/v9#using-checksession-to-acquire-new-tokens}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.checkSession = function(options, cb) {
+WebAuth.prototype.checkSession = function (options, cb) {
   var params = objectHelper
     .merge(this.baseOptions, [
       'clientID',
@@ -664,7 +676,7 @@ WebAuth.prototype.checkSession = function(options, cb) {
  * @see   {@link https://auth0.com/docs/api/authentication#change-password}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.changePassword = function(options, cb) {
+WebAuth.prototype.changePassword = function (options, cb) {
   return this.client.dbConnection.changePassword(options, cb);
 };
 
@@ -683,7 +695,7 @@ WebAuth.prototype.changePassword = function(options, cb) {
  * @see   {@link https://auth0.com/docs/api/authentication#passwordless}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.passwordlessStart = function(options, cb) {
+WebAuth.prototype.passwordlessStart = function (options, cb) {
   var authParams = objectHelper
     .merge(this.baseOptions, [
       'responseType',
@@ -719,7 +731,7 @@ WebAuth.prototype.passwordlessStart = function(options, cb) {
  * @see   {@link https://auth0.com/docs/api/authentication#signup}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.signup = function(options, cb) {
+WebAuth.prototype.signup = function (options, cb) {
   return this.client.dbConnection.signup(options, cb);
 };
 
@@ -743,7 +755,7 @@ WebAuth.prototype.signup = function(options, cb) {
  * @see {@link https://auth0.com/docs/api/authentication#authorize-client}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.authorize = function(options) {
+WebAuth.prototype.authorize = function (options) {
   var params = objectHelper
     .merge(this.baseOptions, [
       'clientID',
@@ -792,12 +804,12 @@ WebAuth.prototype.authorize = function(options) {
  * @see   {@link https://auth0.com/docs/api-auth/grant/password}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.signupAndAuthorize = function(options, cb) {
+WebAuth.prototype.signupAndAuthorize = function (options, cb) {
   var _this = this;
 
   return this.client.dbConnection.signup(
     objectHelper.blacklist(options, ['popupHandler']),
-    function(err) {
+    function (err) {
       if (err) {
         return cb(err);
       }
@@ -839,7 +851,7 @@ WebAuth.prototype.signupAndAuthorize = function(options, cb) {
  * @param {crossOriginLoginCallback} cb Callback function called only when an authentication error, like invalid username or password, occurs. For other types of errors, there will be a redirect to the `redirectUri`.
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.login = function(options, cb) {
+WebAuth.prototype.login = function (options, cb) {
   var params = objectHelper
     .merge(this.baseOptions, [
       'clientID',
@@ -890,7 +902,7 @@ WebAuth.prototype.login = function(options, cb) {
  * @param {crossOriginLoginCallback} cb Callback function called only when an authentication error, like invalid username or password, occurs. For other types of errors, there will be a redirect to the `redirectUri`.
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.passwordlessLogin = function(options, cb) {
+WebAuth.prototype.passwordlessLogin = function (options, cb) {
   var params = objectHelper
     .merge(this.baseOptions, [
       'clientID',
@@ -940,7 +952,7 @@ WebAuth.prototype.passwordlessLogin = function(options, cb) {
  * @deprecated Use {@link crossOriginVerification} instead.
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.crossOriginAuthenticationCallback = function() {
+WebAuth.prototype.crossOriginAuthenticationCallback = function () {
   this.crossOriginVerification();
 };
 
@@ -950,7 +962,7 @@ WebAuth.prototype.crossOriginAuthenticationCallback = function() {
  * @method crossOriginVerification
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.crossOriginVerification = function() {
+WebAuth.prototype.crossOriginVerification = function () {
   this.crossOriginAuthentication.callback();
 };
 
@@ -970,7 +982,7 @@ WebAuth.prototype.crossOriginVerification = function() {
  * @see   {@link https://auth0.com/docs/api/authentication#logout}
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.logout = function(options) {
+WebAuth.prototype.logout = function (options) {
   windowHelper.redirect(this.client.buildLogoutUrl(options));
 };
 
@@ -994,7 +1006,7 @@ WebAuth.prototype.logout = function(options) {
  * @param {Function} cb
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.passwordlessVerify = function(options, cb) {
+WebAuth.prototype.passwordlessVerify = function (options, cb) {
   var _this = this;
   var params = objectHelper
     .merge(this.baseOptions, [
@@ -1025,7 +1037,7 @@ WebAuth.prototype.passwordlessVerify = function(options, cb) {
 
   params = this.transactionManager.process(params);
 
-  return this.client.passwordless.verify(params, function(err) {
+  return this.client.passwordless.verify(params, function (err) {
     if (err) {
       return cb(err);
     }
@@ -1035,7 +1047,7 @@ WebAuth.prototype.passwordlessVerify = function(options, cb) {
     }
 
     if (typeof options.onRedirecting === 'function') {
-      return options.onRedirecting(function() {
+      return options.onRedirecting(function () {
         doAuth();
       });
     }
@@ -1060,7 +1072,7 @@ WebAuth.prototype.passwordlessVerify = function(options, cb) {
  * @param {Function} [callback] An optional completion callback
  * @memberof WebAuth.prototype
  */
-WebAuth.prototype.renderCaptcha = function(element, options, callback) {
+WebAuth.prototype.renderCaptcha = function (element, options, callback) {
   return captcha.render(this.client, element, options, callback);
 };
 


Test patch:
diff --git a/test/helper/storage.cookie.test.js b/test/helper/storage.cookie.test.js
index ae6cf7e..db88cbd 100644
--- a/test/helper/storage.cookie.test.js
+++ b/test/helper/storage.cookie.test.js
@@ -4,12 +4,14 @@ import sinon from 'sinon';
 
 import CookieStorage from '../../src/helper/storage/cookie';
 import windowHandler from '../../src/helper/window';
+import Cookies from 'js-cookie';
 
-var cookieStorage = new CookieStorage();
 const KEY = 'foo';
 const VALUE = 'bar';
 
 describe('storage.cookies', function () {
+  let cookieStorage;
+
   beforeEach(function () {
     sinon.stub(CookieLibrary, 'get').callsFake(function (key) {
       expect(key).to.be(KEY);
@@ -22,6 +24,8 @@ describe('storage.cookies', function () {
     sinon.stub(CookieLibrary, 'remove').callsFake(function (key) {
       expect(key).to.be(KEY);
     });
+
+    cookieStorage = new CookieStorage({ legacySameSiteCookie: true });
   });
   afterEach(function () {
     CookieLibrary.get.restore();
@@ -33,10 +37,30 @@ describe('storage.cookies', function () {
       const value = cookieStorage.getItem(KEY);
       expect(value).to.be(VALUE);
     });
+
+    it('should fall back to the compatibility cookie if the main one isnt available', () => {
+      CookieLibrary.get.restore();
+
+      sinon.stub(CookieLibrary, 'get').callsFake(key => {
+        switch (key) {
+          case `_${KEY}_compat`:
+            return VALUE;
+          default:
+            return undefined;
+        }
+      });
+
+      const value = cookieStorage.getItem(KEY);
+      expect(value).to.be(VALUE);
+    });
   });
   describe('removeItem', function () {
     it('calls Cookie.remove', function (done) {
+      CookieLibrary.remove.restore();
+      sinon.stub(CookieLibrary, 'remove').callsFake();
       cookieStorage.removeItem(KEY);
+      expect(Cookies.remove.firstCall.args).to.be.eql(['foo']);
+      expect(Cookies.remove.secondCall.args).to.be.eql(['_foo_compat']);
       done();
     });
   });
@@ -75,8 +99,39 @@ describe('storage.cookies', function () {
       ]);
     });
 
-    it('sets the secure flag on cookies when using the https protocol', function () {
+    it('sets the secure flag on cookies when using the https protocol, and saves legacy compatibility cookie', function () {
+      windowHandler.getWindow.restore();
+      sinon.stub(windowHandler, 'getWindow').callsFake(function () {
+        return {
+          location: {
+            protocol: 'https:'
+          }
+        };
+      });
+
+      CookieLibrary.set.restore();
+      sinon.stub(CookieLibrary, 'set').callsFake(() => {});
+
+      cookieStorage.setItem(KEY, VALUE, { expires: 2, test: true });
+
+      expect(CookieLibrary.set.firstCall.args).to.be.eql([
+        '_foo_compat',
+        'bar',
+        { expires: 2, test: true, secure: true }
+      ]);
+
+      expect(CookieLibrary.set.secondCall.args).to.be.eql([
+        'foo',
+        'bar',
+        { expires: 2, test: true, secure: true, sameSite: 'none' }
+      ]);
+    });
+
+    it('does not save a legacy compat cookie when legacySameSiteCookie: false', function () {
+      cookieStorage = new CookieStorage();
+
       windowHandler.getWindow.restore();
+
       sinon.stub(windowHandler, 'getWindow').callsFake(function () {
         return {
           location: {
@@ -85,6 +140,9 @@ describe('storage.cookies', function () {
         };
       });
 
+      CookieLibrary.set.restore();
+      sinon.stub(CookieLibrary, 'set').callsFake(() => {});
+
       cookieStorage.setItem(KEY, VALUE, { expires: 2, test: true });
 
       expect(CookieLibrary.set.firstCall.args).to.be.eql([
diff --git a/test/web-auth/redirect.test.js b/test/web-auth/redirect.test.js
index 4e2342f..3663d47 100644
--- a/test/web-auth/redirect.test.js
+++ b/test/web-auth/redirect.test.js
@@ -9,21 +9,20 @@ import RequestBuilder from '../../src/helper/request-builder';
 import CrossOriginAuthentication from '../../src/web-auth/cross-origin-authentication';
 var telemetryInfo = new RequestBuilder({}).getTelemetryData();
 import TransactionManager from '../../src/web-auth/transaction-manager';
-import objectHelper from '../../src/helper/object';
 
-describe('auth0.WebAuth.redirect', function() {
-  before(function() {
+describe('auth0.WebAuth.redirect', function () {
+  before(function () {
     sinon
       .stub(TransactionManager.prototype, 'generateTransaction')
-      .callsFake(function(appState, state, nonce) {
+      .callsFake(function (appState, state, nonce) {
         return { state: state || 'randomState', nonce: nonce || 'randomNonce' };
       });
   });
-  after(function() {
+  after(function () {
     TransactionManager.prototype.generateTransaction.restore();
   });
-  context('signup', function() {
-    before(function() {
+  context('signup', function () {
+    before(function () {
       this.auth0 = new WebAuth({
         domain: 'me.auth0.com',
         clientID: '...',
@@ -33,12 +32,12 @@ describe('auth0.WebAuth.redirect', function() {
       });
     });
 
-    afterEach(function() {
+    afterEach(function () {
       request.post.restore();
     });
 
-    it('should call db-connection signup with all the options', function(done) {
-      sinon.stub(request, 'post').callsFake(function(url) {
+    it('should call db-connection signup with all the options', function (done) {
+      sinon.stub(request, 'post').callsFake(function (url) {
         expect(url).to.be('https://me.auth0.com/dbconnections/signup');
         return new RequestMock({
           body: {
@@ -50,7 +49,7 @@ describe('auth0.WebAuth.redirect', function() {
           headers: {
             'Content-Type': 'application/json'
           },
-          cb: function(cb) {
+          cb: function (cb) {
             cb(null, {
               body: {
                 email_verified: false,
@@ -67,7 +66,7 @@ describe('auth0.WebAuth.redirect', function() {
           email: 'me@example.com',
           password: '123456'
         },
-        function(err, data) {
+        function (err, data) {
           expect(err).to.be(null);
           expect(data).to.eql({
             emailVerified: false,
@@ -79,8 +78,8 @@ describe('auth0.WebAuth.redirect', function() {
     });
   });
 
-  context('login', function() {
-    before(function() {
+  context('login', function () {
+    before(function () {
       this.auth0 = new WebAuth({
         domain: 'me.auth0.com',
         clientID: '...',
@@ -89,7 +88,7 @@ describe('auth0.WebAuth.redirect', function() {
         _sendTelemetry: false
       });
     });
-    afterEach(function() {
+    afterEach(function () {
       if (CrossOriginAuthentication.prototype.login.restore) {
         CrossOriginAuthentication.prototype.login.restore();
       }
@@ -97,24 +96,24 @@ describe('auth0.WebAuth.redirect', function() {
         CrossOriginAuthentication.prototype.callback.restore();
       }
     });
-    it('should call CrossOriginAuthentication.login', function(done) {
+    it('should call CrossOriginAuthentication.login', function (done) {
       var inputOptions = { foo: 'bar', connection: 'realm' };
       var expectedOptions = { foo: 'bar', realm: 'realm' };
       sinon
         .stub(CrossOriginAuthentication.prototype, 'login')
-        .callsFake(function(options, cb) {
+        .callsFake(function (options, cb) {
           expect(options).to.be.eql(expectedOptions);
           expect(cb()).to.be('cb');
           done();
         });
-      this.auth0.redirect.loginWithCredentials(inputOptions, function() {
+      this.auth0.redirect.loginWithCredentials(inputOptions, function () {
         return 'cb';
       });
     });
   });
 
-  context('signup and login', function() {
-    before(function() {
+  context('signup and login', function () {
+    before(function () {
       this.auth0 = new WebAuth({
         domain: 'me.auth0.com',
         clientID: '...',
@@ -122,10 +121,10 @@ describe('auth0.WebAuth.redirect', function() {
         responseType: 'token',
         _sendTelemetry: false
       });
-      sinon.stub(windowHelper, 'getWindow').callsFake(function() {
+      sinon.stub(windowHelper, 'getWindow').callsFake(function () {
         return {
           crypto: {
-            getRandomValues: function() {
+            getRandomValues: function () {
               return [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
             }
           }
@@ -133,16 +132,16 @@ describe('auth0.WebAuth.redirect', function() {
       });
     });
 
-    afterEach(function() {
+    afterEach(function () {
       request.post.restore();
     });
 
-    after(function() {
+    after(function () {
       windowHelper.getWindow.restore();
     });
 
-    it('should call db-connection signup with all the options', function(done) {
-      sinon.stub(request, 'post').callsFake(function(url) {
+    it('should call db-connection signup with all the options', function (done) {
+      sinon.stub(request, 'post').callsFake(function (url) {
         if (url !== 'https://me.auth0.com/dbconnections/signup') {
           throw new Error('Invalid URL');
         }
@@ -157,7 +156,7 @@ describe('auth0.WebAuth.redirect', function() {
           headers: {
             'Content-Type': 'application/json'
           },
-          cb: function(cb) {
+          cb: function (cb) {
             cb(null, {
               body: {
                 _id: '...',
@@ -168,7 +167,7 @@ describe('auth0.WebAuth.redirect', function() {
           }
         });
       });
-      sinon.stub(this.auth0, 'login').callsFake(function(options, cb) {
+      sinon.stub(this.auth0, 'login').callsFake(function (options, cb) {
         expect(options).to.be.eql({
           email: 'me@example.com',
           password: '123456',
@@ -186,8 +185,8 @@ describe('auth0.WebAuth.redirect', function() {
       });
     });
 
-    it('should propagate signup errors', function(done) {
-      sinon.stub(request, 'post').callsFake(function(url) {
+    it('should propagate signup errors', function (done) {
+      sinon.stub(request, 'post').callsFake(function (url) {
         expect(url).to.be('https://me.auth0.com/dbconnections/signup');
 
         return new RequestMock({
@@ -200,7 +199,7 @@ describe('auth0.WebAuth.redirect', function() {
           headers: {
             'Content-Type': 'application/json'
           },
-          cb: function(cb) {
+          cb: function (cb) {
             cb({
               response: {
                 statusCode: 400,
@@ -221,7 +220,7 @@ describe('auth0.WebAuth.redirect', function() {
           password: '123456',
           scope: 'openid'
         },
-        function(err, data) {
+        function (err, data) {
           expect(data).to.be(undefined);
           expect(err).to.eql({
             original: {
@@ -243,8 +242,8 @@ describe('auth0.WebAuth.redirect', function() {
     });
   });
 
-  context('passwordlessVerify', function() {
-    before(function() {
+  context('passwordlessVerify', function () {
+    before(function () {
       this.auth0 = new WebAuth({
         domain: 'me.auth0.com',
         clientID: '...',
@@ -253,13 +252,13 @@ describe('auth0.WebAuth.redirect', function() {
       });
     });
 
-    afterEach(function() {
+    afterEach(function () {
       request.post.restore();
       windowHelper.redirect.restore();
     });
 
-    it('should verify the code and redirect to the passwordless verify page', function(done) {
-      sinon.stub(windowHelper, 'redirect').callsFake(function(url) {
+    it('should verify the code and redirect to the passwordless verify page', function (done) {
+      sinon.stub(windowHelper, 'redirect').callsFake(function (url) {
         expect(url).to.be(
           'https://me.auth0.com/passwordless/verify_redirect?client_id=test-client-id&response_type=code&redirect_uri=http%3A%2F%2Fpage.com%2Fcallback&connection=the_connection&phone_number=123456&verification_code=abc&state=randomState&auth0Client=' +
             encodeURIComponent(telemetryInfo)
@@ -267,7 +266,7 @@ describe('auth0.WebAuth.redirect', function() {
         done();
       });
 
-      sinon.stub(request, 'post').callsFake(function(url) {
+      sinon.stub(request, 'post').callsFake(function (url) {
         expect(url).to.be('https://me.auth0.com/passwordless/verify');
         return new RequestMock({
           body: {
@@ -280,7 +279,7 @@ describe('auth0.WebAuth.redirect', function() {
             'Content-Type': 'application/json',
             'Auth0-Client': telemetryInfo
           },
-          cb: function(cb) {
+          cb: function (cb) {
             cb(null, {
               body: {}
             });
@@ -295,12 +294,12 @@ describe('auth0.WebAuth.redirect', function() {
           phoneNumber: '123456',
           verificationCode: 'abc'
         },
-        function(err) {}
+        function (err) {}
       );
     });
 
-    it('should verify the code and redirect to the passwordless verify page (using global client ID)', function(done) {
-      sinon.stub(windowHelper, 'redirect').callsFake(function(url) {
+    it('should verify the code and redirect to the passwordless verify page (using global client ID)', function (done) {
+      sinon.stub(windowHelper, 'redirect').callsFake(function (url) {
         expect(url).to.be(
           'https://me.auth0.com/passwordless/verify_redirect?client_id=...&response_type=code&redirect_uri=http%3A%2F%2Fpage.com%2Fcallback&connection=the_connection&phone_number=123456&verification_code=abc&state=randomState&auth0Client=' +
             encodeURIComponent(telemetryInfo)
@@ -308,7 +307,7 @@ describe('auth0.WebAuth.redirect', function() {
         done();
       });
 
-      sinon.stub(request, 'post').callsFake(function(url) {
+      sinon.stub(request, 'post').callsFake(function (url) {
         expect(url).to.be('https://me.auth0.com/passwordless/verify');
         return new RequestMock({
           body: {
@@ -321,7 +320,7 @@ describe('auth0.WebAuth.redirect', function() {
             'Content-Type': 'application/json',
             'Auth0-Client': telemetryInfo
           },
-          cb: function(cb) {
+          cb: function (cb) {
             cb(null, {
               body: {}
             });
@@ -335,13 +334,13 @@ describe('auth0.WebAuth.redirect', function() {
           phoneNumber: '123456',
           verificationCode: 'abc'
         },
-        function(err) {}
+        function (err) {}
       );
     });
   });
 
-  context('passwordlessVerify without telemetry', function() {
-    before(function() {
+  context('passwordlessVerify without telemetry', function () {
+    before(function () {
       this.auth0 = new WebAuth({
         domain: 'me.auth0.com',
         clientID: '...',
@@ -351,20 +350,20 @@ describe('auth0.WebAuth.redirect', function() {
       });
     });
 
-    afterEach(function() {
+    afterEach(function () {
       request.post.restore();
       windowHelper.redirect.restore();
     });
 
-    it('should verify the code and redirect to the passwordless verify page', function(done) {
-      sinon.stub(windowHelper, 'redirect').callsFake(function(url) {
+    it('should verify the code and redirect to the passwordless verify page', function (done) {
+      sinon.stub(windowHelper, 'redirect').callsFake(function (url) {
         expect(url).to.be(
           'https://me.auth0.com/passwordless/verify_redirect?client_id=...&response_type=code&redirect_uri=http%3A%2F%2Fpage.com%2Fcallback&connection=the_connection&phone_number=123456&verification_code=abc&state=randomState'
         );
         done();
       });
 
-      sinon.stub(request, 'post').callsFake(function(url) {
+      sinon.stub(request, 'post').callsFake(function (url) {
         expect(url).to.be('https://me.auth0.com/passwordless/verify');
         return new RequestMock({
           body: {
@@ -376,7 +375,7 @@ describe('auth0.WebAuth.redirect', function() {
           headers: {
             'Content-Type': 'application/json'
           },
-          cb: function(cb) {
+          cb: function (cb) {
             cb(null, {
               body: {}
             });
@@ -390,12 +389,12 @@ describe('auth0.WebAuth.redirect', function() {
           phoneNumber: '123456',
           verificationCode: 'abc'
         },
-        function(err) {}
+        function (err) {}
       );
     });
   });
-  context('passwordlessVerify with error', function() {
-    before(function() {
+  context('passwordlessVerify with error', function () {
+    before(function () {
       this.auth0 = new WebAuth({
         domain: 'me.auth0.com',
         clientID: '...',
@@ -405,12 +404,12 @@ describe('auth0.WebAuth.redirect', function() {
       });
     });
 
-    afterEach(function() {
+    afterEach(function () {
       request.post.restore();
     });
 
-    it('should verify the code and redirect to the passwordless verify page', function(done) {
-      sinon.stub(request, 'post').callsFake(function(url) {
+    it('should verify the code and redirect to the passwordless verify page', function (done) {
+      sinon.stub(request, 'post').callsFake(function (url) {
         expect(url).to.be('https://me.auth0.com/passwordless/verify');
         return new RequestMock({
           body: {
@@ -422,7 +421,7 @@ describe('auth0.WebAuth.redirect', function() {
           headers: {
             'Content-Type': 'application/json'
           },
-          cb: function(cb) {
+          cb: function (cb) {
             cb({
               error: 'some_error_code',
               error_description: 'Some error description'
@@ -437,7 +436,7 @@ describe('auth0.WebAuth.redirect', function() {
           phoneNumber: '123456',
           verificationCode: 'abc'
         },
-        function(err) {
+        function (err) {
           expect(err).to.eql({
             original: {
               error: 'some_error_code',
@@ -452,8 +451,8 @@ describe('auth0.WebAuth.redirect', function() {
     });
   });
 
-  describe('authenticate', function() {
-    beforeEach(function() {
+  describe('authenticate', function () {
+    beforeEach(function () {
       global.window = { location: '' };
       this.auth0 = new WebAuth({
         domain: 'me.auth0.com',
@@ -463,16 +462,16 @@ describe('auth0.WebAuth.redirect', function() {
       });
     });
 
-    it('should check that responseType is present', function() {
+    it('should check that responseType is present', function () {
       var _this = this;
-      expect(function() {
+      expect(function () {
         _this.auth0.authorize({ connection: 'facebook' });
-      }).to.throwException(function(e) {
+      }).to.throwException(function (e) {
         expect(e.message).to.be('responseType option is required');
       });
     });
 
-    it('should redirect to authorize', function() {
+    it('should redirect to authorize', function () {
       this.auth0.authorize({
         responseType: 'code',
         connection: 'facebook',
@@ -484,7 +483,7 @@ describe('auth0.WebAuth.redirect', function() {
       );
     });
 
-    it('should redirect to logout', function() {
+    it('should redirect to logout', function () {
       this.auth0.logout({ redirect_to: 'http://example.com/logout' });
       expect(global.window.location).to.be(
         'https://me.auth0.com/v2/logout?client_id=...&redirect_to=http%3A%2F%2Fexample.com%2Flogout'