Repository: auth0/node-auth0
Pull request #401 (2019-07-26 12:49:39)

Implement auth0-forwarded-for header passing in Authentication Client
### Changes

This PR implements [auth0-forwarded-for](https://auth0.com/docs/api-auth/tutorials/using-resource-owner-password-from-server-side#sending-the-end-user-ip-from-your-server) header passing in the Authentication Client.
User-facing methods now accept an additional argument `options`, right before callback, for example:
```js
client.passwordGrant(userData, {forwardFor: '1.2.3.4'}, callback);
```
This change is non-breaking. If there is no options object, the callback as a second argument will still work.

### References

#191 

### Testing

- [x] This change adds unit test coverage

I have also npm linked the module to my existing project and it seemed to do it's job.

### 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


luisrudge: Does it make sense to use this as an option per method? Maybe it's better to use as a global config instead?
kjarmicki: It would be neat to use as a global config, but I don't think it's possible. The IP value will change for each request.
luisrudge: Why would it change on every request?
kjarmicki: Because as I understand it, it's about forwarding individual client's IP to Auth0 backend in order to tie brute force attempts with particular IPs.
Reference: https://auth0.com/docs/api-auth/tutorials/using-resource-owner-password-from-server-side#configuring-the-auth0-application-to-receive-and-trust-the-ip-sent-by-your-server
luisrudge: @kjarmicki ahh! Of course 😝 
damieng: @luisrudge Let's make sure we have a cross-platform strategy/design for the other SDKs before we progress too far on this. Auth0.Net already has an issue open about providing similar functionality auth0/auth0.net#274

Whether you need to change IP per request really depends on whether you imagine the lifecycle of the client object being per user request or if you want to pool/hold it around to use it across multiple user requests. I don't think there is anything in it today that forces it to be used one way or another.
luisrudge: @damieng I agree, that's a good point. But, considering node-auth0 doesn't hold any state, there's no hard requirement to create one instance per request, right?

@kjarmicki how do you use this in your app? do you create one Management instance per request or one per application?
kjarmicki: @luisrudge we're initializing the client object once at the start of the application and reusing it throughout the life of the app. I think having a global initialization option would force the module users to re-initialize the client for each request while having it as a method option enables both use cases @damieng mentioned.
luisrudge: @kjarmicki perfect. Thanks for your feedback. I'll discuss this internally and get back to you.
kjarmicki: @luisrudge howdy :) any news? If I can help in any way let me know.
luisrudge: @kjarmicki we're having some internal discussions on how to tackle this across all of our SDKs. Sorry about the wait. I'll get back to you when I have more information.
luisrudge: @kjarmicki did you test this with all the methods? I just contacted our protocols team and only two grant types do this: `password` and `password-realm`. Can you modify the PR to apply this header only in those methods? Thanks!
kjarmicki: @luisrudge I did not, I assumed that all HTTP-based login methods support this header and checked the one we use (`passwordGrant`). I'll adjust my PR to affect only `password` and `password-realm` grant type methods.

Patch:
diff --git a/src/auth/OAuthAuthenticator.js b/src/auth/OAuthAuthenticator.js
index 6586b81ba..31a841bd5 100644
--- a/src/auth/OAuthAuthenticator.js
+++ b/src/auth/OAuthAuthenticator.js
@@ -1,10 +1,24 @@
 var extend = require('util')._extend;
+var sanitizeArguments = require('../utils').sanitizeArguments;
 
 var ArgumentError = require('rest-facade').ArgumentError;
 var RestClient = require('rest-facade').Client;
 
 var OAUthWithIDTokenValidation = require('./OAUthWithIDTokenValidation');
 
+function getParamsFromOptions(options) {
+  const params = {};
+  if (!options || typeof options !== 'object') {
+    return params;
+  }
+  if (options.forwardedFor) {
+    params._requestCustomizer = function(req) {
+      req.set('auth0-forwarded-for', options.forwardedFor);
+    };
+  }
+  return params;
+}
+
 /**
  * @class
  * Abstracts the sign-in, sign-up and change-password processes for Database &
@@ -75,17 +89,21 @@ var OAuthAuthenticator = function(options) {
  *   console.log(userData);
  * });
  *
- * @param   {Object}    userData              User credentials object.
- * @param   {String}    userData.username     Username.
- * @param   {String}    userData.password     User password.
- * @param   {String}    userData.connection   The identity provider in use.
+ * @param   {Object}    userData               User credentials object.
+ * @param   {String}    userData.username      Username.
+ * @param   {String}    userData.password      User password.
+ * @param   {String}    userData.connection    The identity provider in use.
+ * @param   {Object}    [options]              Additional options.
+ * @param   {String}    [options.forwardedFor] Value to be used for auth0-forwarded-for header
  *
  * @return  {Promise|undefined}
  */
-OAuthAuthenticator.prototype.signIn = function(userData, cb) {
-  var params = {
+OAuthAuthenticator.prototype.signIn = function(userData, options, cb) {
+  var { options, cb } = sanitizeArguments(options, cb);
+  var defaultParams = {
     type: 'ro'
   };
+  var params = extend(defaultParams, getParamsFromOptions(options));
   var defaultFields = {
     client_id: this.clientId,
     grant_type: 'password',
@@ -140,17 +158,21 @@ OAuthAuthenticator.prototype.signIn = function(userData, cb) {
  *   console.log(userData);
  * });
  *
- * @param   {Object}    userData              User credentials object.
- * @param   {String}    userData.username     Username.
- * @param   {String}    userData.password     User password.
- * @param   {String}    [userData.realm]      Name of the realm to use to authenticate or the connection name
+ * @param   {Object}    userData               User credentials object.
+ * @param   {String}    userData.username      Username.
+ * @param   {String}    userData.password      User password.
+ * @param   {String}    [userData.realm]       Name of the realm to use to authenticate or the connection name
+ * @param   {Object}    [options]              Additional options.
+ * @param   {String}    [options.forwardedFor] Value to be used for auth0-forwarded-for header
  *
  * @return  {Promise|undefined}
  */
-OAuthAuthenticator.prototype.passwordGrant = function(userData, cb) {
-  var params = {
+OAuthAuthenticator.prototype.passwordGrant = function(userData, options, cb) {
+  var { options, cb } = sanitizeArguments(options, cb);
+  var defaultParams = {
     type: 'token'
   };
+  var params = extend(defaultParams, getParamsFromOptions(options));
   var defaultFields = {
     client_id: this.clientId,
     client_secret: this.clientSecret,
diff --git a/src/utils.js b/src/utils.js
index 8d9f2caa8..ab562a2ab 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -102,3 +102,16 @@ utils.maybeDecode = url => {
   }
   return url;
 };
+
+utils.sanitizeArguments = function(optionsCandidate, cbCandidate) {
+  if (optionsCandidate instanceof Function) {
+    return {
+      cb: optionsCandidate,
+      options: undefined
+    };
+  }
+  return {
+    cb: cbCandidate,
+    options: optionsCandidate
+  };
+};


Test patch:
diff --git a/test/auth/oauth.tests.js b/test/auth/oauth.tests.js
index c3433d121..3cfe3419d 100644
--- a/test/auth/oauth.tests.js
+++ b/test/auth/oauth.tests.js
@@ -67,6 +67,9 @@ describe('OAuthAuthenticator', function() {
       password: 'pwd',
       connection: 'Username-Password-Authentication'
     };
+    var options = {
+      forwardedFor: '0.0.0.0'
+    };
 
     beforeEach(function() {
       this.authenticator = new Authenticator(validOptions);
@@ -230,6 +233,26 @@ describe('OAuthAuthenticator', function() {
         })
         .catch(done);
     });
+
+    it('should make it possible to pass auth0-forwarded-for header', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function() {
+          return this.getHeader('auth0-forwarded-for') === options.forwardedFor;
+        })
+        .reply(200);
+
+      this.authenticator
+        .signIn(userData, options)
+        .then(function() {
+          expect(request.isDone()).to.be.true;
+
+          done();
+        })
+        .catch(done);
+    });
+
     it('should use OAUthWithIDTokenValidation', function(done) {
       this.authenticator
         .signIn(userData)
@@ -247,6 +270,9 @@ describe('OAuthAuthenticator', function() {
       username: 'username',
       password: 'pwd'
     };
+    var options = {
+      forwardedFor: '0.0.0.0'
+    };
 
     beforeEach(function() {
       this.authenticator = new Authenticator(validOptions);
@@ -400,6 +426,26 @@ describe('OAuthAuthenticator', function() {
         })
         .catch(done);
     });
+
+    it('should make it possible to pass auth0-forwarded-for header', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function() {
+          return this.getHeader('auth0-forwarded-for') === options.forwardedFor;
+        })
+        .reply(200);
+
+      this.authenticator
+        .passwordGrant(userData, options)
+        .then(function() {
+          expect(request.isDone()).to.be.true;
+
+          done();
+        })
+        .catch(done);
+    });
+
     it('should use OAUthWithIDTokenValidation', function(done) {
       this.authenticator
         .passwordGrant(userData)
@@ -416,6 +462,7 @@ describe('OAuthAuthenticator', function() {
     var userData = {
       refresh_token: 'refresh_token'
     };
+
     beforeEach(function() {
       this.authenticator = new Authenticator(validOptions);
       this.request = nock(API_URL)
@@ -902,6 +949,7 @@ describe('OAuthAuthenticator', function() {
         })
         .catch(done);
     });
+
     it('should use OAUthWithIDTokenValidation', function(done) {
       this.authenticator
         .authorizationCodeGrant(data)