Repository: auth0/node-auth0 (ts)

Issue:
Missing call to invalidate remember browser
**Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues.

**Thank you in advance for helping us to improve this library!** Your attention to detail here is greatly appreciated and will help us respond as quickly as possible. For general support or usage questions, use the [Auth0 Community](https://community.auth0.com/) or [Auth0 Support](https://support.auth0.com/). Finally, to avoid duplicates, please search existing Issues before submitting one here.

By submitting an Issue to this repository, you agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).

### Describe the problem you'd like to have solved

I'm missing the call to https://auth0.com/docs/api/management/v2#!/Users/post_invalidate_remember_browser

### Describe the ideal solution

An API call to invalidate remember browser

## Alternatives and current work-arounds

Not use node-auth0 to do the call.

### Additional context

N/A

Pull request description:
Adding call to invalidate all remembered browsers.
### Changes

Adding an API call to invalidate remember browser using https://auth0.com/docs/api/management/v2#!/Users/post_invalidate_remember_browser

### References

Fixes #505 

### Testing

Used the recovery code call as inspiration

### 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 existing and new tests complete without errors


Patch:
diff --git a/src/management/UsersManager.js b/src/management/UsersManager.js
index f5469204d..674a37670 100644
--- a/src/management/UsersManager.js
+++ b/src/management/UsersManager.js
@@ -122,6 +122,21 @@ var UsersManager = function(options) {
     options.retry
   );
 
+  /**
+   * Provides an abstraction layer for invalidating all remembered browsers for MFA.
+   *
+   * @type {external:RestClient}
+   */
+  var invalidateRememberBrowserAuth0RestClients = new Auth0RestClient(
+    options.baseUrl + '/users/:id/multifactor/actions/invalidate-remember-browser',
+    clientOptions,
+    options.tokenProvider
+  );
+  this.invalidateRememberBrowsers = new RetryRestClient(
+    invalidateRememberBrowserAuth0RestClients,
+    options.retry
+  );
+
   /**
    * Provides an abstraction layer for CRD on roles for a user
    *
@@ -647,6 +662,39 @@ UsersManager.prototype.regenerateRecoveryCode = function(params, cb) {
   return this.recoveryCodeRegenerations.create(params, {});
 };
 
+/**
+ * Invalidate all remembered browsers for MFA.
+ *
+ * @method    invalidateRememberBrowser
+ * @memberOf  module:management.UsersManager.prototype
+ *
+ * @example
+ * management.users.invalidateRememberBrowser({ id: USER_ID }, function (err, result) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Invalidated all remembered browsers.
+ * });
+ *
+ * @param   {Object}    params                The user data object.
+ * @param   {String}    params.id             The user id.
+ * @param   {Function}  [cb]                  Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+UsersManager.prototype.invalidateRememberBrowser = function(params, cb) {
+  if (!params || !params.id) {
+    throw new ArgumentError('The userId cannot be null or undefined');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.invalidateRememberBrowsers.create(params, {}, cb);
+  }
+
+  return this.invalidateRememberBrowsers.create(params, {});
+};
+
 /**
  * Get a list of roles for a user.
  *
diff --git a/src/management/index.js b/src/management/index.js
index f06da9331..9d53813c5 100644
--- a/src/management/index.js
+++ b/src/management/index.js
@@ -1560,6 +1560,33 @@ utils.wrapPropertyMethod(
   'users.regenerateRecoveryCode'
 );
 
+/**
+ * Invalidate all remembered browsers for MFA.
+ *
+ * @method    invalidateRememberBrowser
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * management.invalidateRememberBrowser({ id: USER_ID }, function (err) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Invalidated all remembered browsers.
+ * });
+ *
+ * @param   {Object}    data      The user data object.
+ * @param   {String}    data.id   The user id.
+ * @param   {Function}  [cb]      Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(
+  ManagementClient,
+  'invalidateRememberBrowser',
+  'users.invalidateRememberBrowser'
+);
+
 /**
  * Get user blocks by its id.
  *


Test patch:
diff --git a/test/management/management-client.tests.js b/test/management/management-client.tests.js
index 34a108e79..35e03b741 100644
--- a/test/management/management-client.tests.js
+++ b/test/management/management-client.tests.js
@@ -279,6 +279,9 @@ describe('ManagementClient', function() {
         expect(
           client.users.recoveryCodeRegenerations.restClient.restClient.options.headers
         ).to.contain(requestHeaders);
+        expect(
+          client.users.invalidateRememberBrowsers.restClient.restClient.options.headers
+        ).to.contain(requestHeaders);
         expect(client.users.roles.restClient.restClient.options.headers).to.contain(requestHeaders);
         expect(client.users.permissions.restClient.restClient.options.headers).to.contain(
           requestHeaders
@@ -424,6 +427,9 @@ describe('ManagementClient', function() {
         expect(
           client.users.recoveryCodeRegenerations.restClient.restClient.options.headers
         ).to.contain(requestHeaders);
+        expect(
+          client.users.invalidateRememberBrowsers.restClient.restClient.options.headers
+        ).to.contain(requestHeaders);
         expect(client.users.roles.restClient.restClient.options.headers).to.contain(requestHeaders);
         expect(client.users.permissions.restClient.restClient.options.headers).to.contain(
           requestHeaders
@@ -565,6 +571,9 @@ describe('ManagementClient', function() {
         expect(
           client.users.recoveryCodeRegenerations.restClient.restClient.options.headers
         ).to.not.have.property('Auth0-Client');
+        expect(
+          client.users.invalidateRememberBrowsers.restClient.restClient.options.headers
+        ).to.not.have.property('Auth0-Client');
         expect(client.users.roles.restClient.restClient.options.headers).to.not.have.property(
           'Auth0-Client'
         );
@@ -711,6 +720,9 @@ describe('ManagementClient', function() {
         expect(
           client.users.recoveryCodeRegenerations.restClient.restClient.options.headers
         ).to.not.have.property('Auth0-Client');
+        expect(
+          client.users.invalidateRememberBrowsers.restClient.restClient.options.headers
+        ).to.not.have.property('Auth0-Client');
         expect(client.users.roles.restClient.restClient.options.headers).to.not.have.property(
           'Auth0-Client'
         );
diff --git a/test/management/users.tests.js b/test/management/users.tests.js
index 72b8156c5..16714a6ae 100644
--- a/test/management/users.tests.js
+++ b/test/management/users.tests.js
@@ -32,6 +32,7 @@ describe('UsersManager', function() {
       'updateAppMetadata',
       'getGuardianEnrollments',
       'regenerateRecoveryCode',
+      'invalidateRememberBrowser',
       'getRoles',
       'assignRoles',
       'removeRoles',
@@ -1042,6 +1043,77 @@ describe('UsersManager', function() {
     });
   });
 
+  describe('#invalidateRememberBrowser', function() {
+    var data = {
+      id: 'USER_ID'
+    };
+
+    beforeEach(function() {
+      this.request = nock(API_URL)
+        .post('/users/' + data.id + '/multifactor/actions/invalidate-remember-browser')
+        .reply(204);
+    });
+
+    it('should validate empty userId', function() {
+      var _this = this;
+      expect(function() {
+        _this.users.invalidateRememberBrowser(null, function() {});
+      }).to.throw('The userId cannot be null or undefined');
+    });
+
+    it('should accept a callback', function(done) {
+      this.users.invalidateRememberBrowser(data, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.users
+        .invalidateRememberBrowser(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+    it('should pass any errors to the promise catch handler', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users/' + data.id + '/multifactor/actions/invalidate-remember-browser')
+        .reply(500);
+
+      this.users.invalidateRememberBrowser(data).catch(function(err) {
+        expect(err).to.exist;
+
+        done();
+      });
+    });
+
+    it('should perform a POST request to /api/v2/users/:id/multifactor/actions/invalidate-remember-browser', function(done) {
+      var request = this.request;
+
+      this.users.invalidateRememberBrowser(data).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+
+    it('should include the token in the Authorization header', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users/' + data.id + '/multifactor/actions/invalidate-remember-browser')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.users.invalidateRememberBrowser(data).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+
   describe('#getRoles', function() {
     var data = {
       id: 'user_id'