Repository: auth0/node-auth0
Pull request #460 (2020-02-18 15:06:40)

[DXEX-455] Allow custom headers to be set in Management/Auth Clients.
### Changes

In order to better determine what version of the Deploy CLI tool is being used
by our customers, we would like to include the Deploy CLI's version in the
User-agent header as opposed to the current behavior (the version of node under
which the CLI is running).

The Deploy CLI uses ManagementClient to do its work, but the User-agent header
was not configurable before this change. This commit provides a new option
called `userAgent` that will override the User-agent header if it is provided.
If it is not provided, the original User-agent will be used (e.g. `node.js/13.1.2`).

### References

- https://auth0team.atlassian.net/browse/DXEX-455

### Testing

Unit tests were added for the new behavior as well as ones to ensure the original behavior works when `userAgent` is not provided.

- [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 existing and new tests complete without errors


seejee: One note/question about this PR: we only *need* the user agent header to be exposed, but should we just expose the entire headers object instead so that the consumer can add whatever additional headers they like?
seejee: Closing this PR in favor of upgrading the aws-sdk version used in our dependencies. I'll open a new PR once new versions exist for them.
seejee: Closed this PR by mistake! :derp:
seejee: Thanks for updating this branch @damieng! I just realized that my original commit is not properly signed. Let me know if you want me to amend it and force push a new commit that is signed (and has my auth0 email instead of my personal one).

Patch:
diff --git a/src/auth/index.js b/src/auth/index.js
index bcbceff2c..191dcd726 100644
--- a/src/auth/index.js
+++ b/src/auth/index.js
@@ -4,6 +4,7 @@ var util = require('util');
 var utils = require('../utils');
 var jsonToBase64 = utils.jsonToBase64;
 var ArgumentError = require('rest-facade').ArgumentError;
+var assign = Object.assign || require('object.assign');
 
 // Authenticators.
 var OAuthAuthenticator = require('./OAuthAuthenticator');
@@ -43,6 +44,7 @@ var BASE_URL_FORMAT = 'https://%s';
  * @param   {String}  [options.clientSecret]            Default client Secret.
  * @param   {String}  [options.supportedAlgorithms]     Algorithms that your application expects to receive
  * @param  {Boolean}  [options.__bypassIdTokenValidation] Whether the id_token should be validated or not
+ * @param   {Object}  [options.headers]                 Additional headers that will be added to the outgoing requests.
  */
 var AuthenticationClient = function(options) {
   if (!options || typeof options !== 'object') {
@@ -53,14 +55,16 @@ var AuthenticationClient = function(options) {
     throw new ArgumentError('Must provide a domain');
   }
 
+  var defaultHeaders = {
+    'User-agent': 'node.js/' + process.version.replace('v', ''),
+    'Content-Type': 'application/json'
+  };
+
   var managerOptions = {
     clientId: options.clientId,
     domain: options.domain,
     clientSecret: options.clientSecret,
-    headers: {
-      'User-agent': 'node.js/' + process.version.replace('v', ''),
-      'Content-Type': 'application/json'
-    },
+    headers: assign(defaultHeaders, options.headers || {}),
     baseUrl: util.format(BASE_URL_FORMAT, options.domain),
     supportedAlgorithms: options.supportedAlgorithms,
     __bypassIdTokenValidation: options.__bypassIdTokenValidation
diff --git a/src/management/index.js b/src/management/index.js
index b2c8d11ca..88deab422 100644
--- a/src/management/index.js
+++ b/src/management/index.js
@@ -90,6 +90,7 @@ var MANAGEMENT_API_AUD_FORMAT = 'https://%s/api/v2/';
  * @param   {Number}  [options.tokenProvider.cacheTTLInSeconds]   By default the `expires_in` value will be used to determine the cached time of the token, this can be overridden.
  * @param   {Boolean} [options.retry.enabled=true]                Enabled or Disable Retry Policy functionality.
  * @param   {Number}  [options.retry.maxRetries=10]               Retry failed requests X times.
+ * @param   {Object}  [options.headers]                           Additional headers that will be added to the outgoing requests.
  *
  */
 var ManagementClient = function(options) {
@@ -102,11 +103,15 @@ var ManagementClient = function(options) {
   }
 
   var baseUrl = util.format(BASE_URL_FORMAT, options.domain);
+  var userAgent = options.userAgent || 'node.js/' + process.version.replace('v', '');
+
+  var defaultHeaders = {
+    'User-agent': 'node.js/' + process.version.replace('v', ''),
+    'Content-Type': 'application/json'
+  };
+
   var managerOptions = {
-    headers: {
-      'User-agent': 'node.js/' + process.version.replace('v', ''),
-      'Content-Type': 'application/json'
-    },
+    headers: assign(defaultHeaders, options.headers || {}),
     baseUrl: baseUrl
   };
 


Test patch:
diff --git a/test/auth/authentication-client.tests.js b/test/auth/authentication-client.tests.js
index 7cb62e23f..d7f7c07e2 100644
--- a/test/auth/authentication-client.tests.js
+++ b/test/auth/authentication-client.tests.js
@@ -143,6 +143,42 @@ describe('AuthenticationClient', function() {
     });
   });
 
+  describe('user agent', function() {
+    it('should use the node version when the user agent option is not provided', function() {
+      var client = new AuthenticationClient({
+        token: 'token',
+        domain: 'auth0.com'
+      });
+
+      var expected = { 'User-agent': 'node.js/' + process.version.replace('v', '') };
+
+      expect(client.oauth.oauth.options.headers).to.contain(expected);
+      expect(client.database.dbConnections.options.headers).to.contain(expected);
+      expect(client.passwordless.passwordless.options.headers).to.contain(expected);
+      expect(client.users.headers).to.contain(expected);
+      expect(client.tokens.headers).to.contain(expected);
+    });
+
+    it('should include additional headers when provided', function() {
+      var customHeaders = {
+        'User-agent': 'my-user-agent',
+        'Another-header': 'test-header'
+      };
+
+      var client = new AuthenticationClient({
+        token: 'token',
+        domain: 'auth0.com',
+        headers: customHeaders
+      });
+
+      expect(client.oauth.oauth.options.headers).to.contain(customHeaders);
+      expect(client.database.dbConnections.options.headers).to.contain(customHeaders);
+      expect(client.passwordless.passwordless.options.headers).to.contain(customHeaders);
+      expect(client.users.headers).to.contain(customHeaders);
+      expect(client.tokens.headers).to.contain(customHeaders);
+    });
+  });
+
   describe('instance methods', function() {
     var methods = [];
     var client = new AuthenticationClient({ token: 'token', domain: 'auth0.com' });
diff --git a/test/management/management-client.tests.js b/test/management/management-client.tests.js
index e50d53304..742c74d8d 100644
--- a/test/management/management-client.tests.js
+++ b/test/management/management-client.tests.js
@@ -198,6 +198,39 @@ describe('ManagementClient', function() {
       }
     };
 
+    describe('user agent', function() {
+      for (var name in managers) {
+        manager = managers[name];
+
+        it(manager + ' should use the node version by default', function() {
+          var client = new ManagementClient(withTokenConfig);
+
+          expect(
+            client[manager.property].resource.restClient.restClient.options.headers
+          ).to.contain({
+            'User-agent': 'node.js/' + process.version.replace('v', '')
+          });
+        });
+
+        it(manager + ' should include additional headers when provided', function() {
+          var customHeaders = {
+            'User-agent': 'my-user-agent',
+            'Another-header': 'test-header'
+          };
+
+          var options = assign({ headers: customHeaders }, withTokenConfig);
+          var client = new ManagementClient(options);
+
+          expect(
+            client[manager.property].resource.restClient.restClient.options.headers
+          ).to.contain({
+            'User-agent': 'my-user-agent',
+            'Another-header': 'test-header'
+          });
+        });
+      }
+    });
+
     describe('client info', function() {
       it('should configure instances with default telemetry header', function() {
         var utilsStub = {