Repository: auth0/node-auth0
Pull request #133 (2016-10-07 14:57:14)

Added support for client credentials flow

coveralls: 
[![Coverage Status](https://coveralls.io/builds/8234995/badge)](https://coveralls.io/builds/8234995)

Coverage decreased (-1.5%) to 93.342% when pulling **d41c903827f8c2fc401d295e3914b069e84aec01 on add-client-credentials-support** into **69e26e550e234aab1b02dbc7228d047532ea4124 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/8235550/badge)](https://coveralls.io/builds/8235550)

Coverage increased (+0.08%) to 94.925% when pulling **0e8ca358dcbdc3b0403d4df50e8b725489e2e700 on add-client-credentials-support** into **69e26e550e234aab1b02dbc7228d047532ea4124 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/8235600/badge)](https://coveralls.io/builds/8235600)

Coverage increased (+0.08%) to 94.925% when pulling **0e8ca358dcbdc3b0403d4df50e8b725489e2e700 on add-client-credentials-support** into **69e26e550e234aab1b02dbc7228d047532ea4124 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/8235667/badge)](https://coveralls.io/builds/8235667)

Coverage increased (+0.08%) to 94.925% when pulling **0e8ca358dcbdc3b0403d4df50e8b725489e2e700 on add-client-credentials-support** into **69e26e550e234aab1b02dbc7228d047532ea4124 on master**.


Patch:
diff --git a/src/auth/OAuthAuthenticator.js b/src/auth/OAuthAuthenticator.js
index 4600ec77e..6002da3a9 100644
--- a/src/auth/OAuthAuthenticator.js
+++ b/src/auth/OAuthAuthenticator.js
@@ -11,9 +11,10 @@ var RestClient = require('rest-facade').Client;
  * @constructor
  * @memberOf module:auth
  *
- * @param  {Object}              options            Authenticator options.
- * @param  {String}              options.baseUrl    The auth0 account URL.
- * @param  {String}              [options.clientId] Default client ID.
+ * @param  {Object}              options                Authenticator options.
+ * @param  {String}              options.baseUrl        The auth0 account URL.
+ * @param  {String}              [options.clientId]     Default client ID.
+ * @param  {String}              [options.clientSecret] Default client Secret.
  */
 var OAuthAuthenticator = function (options) {
   if (!options) {
@@ -28,6 +29,7 @@ var OAuthAuthenticator = function (options) {
 
   this.oauth = new RestClient(oauthUrl);
   this.clientId = options.clientId;
+  this.clientSecret = options.clientSecret;
 };
 
 
@@ -136,5 +138,37 @@ OAuthAuthenticator.prototype.socialSignIn = function (data, cb) {
   return this.oauth.create(params, data);
 };
 
+OAuthAuthenticator.prototype.clientCredentialsGrant = function(options, cb) {
+
+  var params = {
+    type: 'token'
+  };
+
+  var defaultFields = {
+    grant_type:     "client_credentials",
+    client_id:      this.clientId,
+    client_secret:  this.clientSecret
+  };
+
+  var data = extend(defaultFields, options);
+
+  if (!options || typeof options !== 'object') {
+    throw new ArgumentError('Missing options object');
+  }
+
+  if (!data.client_id || data.client_id.trim().length === 0) {
+    throw new ArgumentError('client_id field is required');
+  }
+
+  if (!data.client_secret || data.client_secret.trim().length === 0) {
+    throw new ArgumentError('client_secret field is required');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.oauth.create(params, data, cb);
+  }
+
+  return this.oauth.create(params, data);
+};
 
 module.exports = OAuthAuthenticator;
diff --git a/src/auth/index.js b/src/auth/index.js
index e53642b5c..90f1d8b99 100644
--- a/src/auth/index.js
+++ b/src/auth/index.js
@@ -40,10 +40,11 @@ var BASE_URL_FORMAT = 'https://%s';
  *   clientId: '{OPTIONAL_CLIENT_ID}'
  * });
  *
- * @param   {Object}  options             Options for the Authentication Client
- *                                        SDK.
- * @param   {String}  options.domain      AuthenticationClient server domain.
- * @param   {String}  [options.clientId]  Default client ID.
+ * @param   {Object}  options                 Options for the Authentication Client
+ *                                            SDK.
+ * @param   {String}  options.domain          AuthenticationClient server domain.
+ * @param   {String}  [options.clientId]      Default client ID.
+ * @param   {String}  [options.clientSecret]  Default client Secret.
  */
 var AuthenticationClient = function (options) {
   if (!options || typeof options !== 'object') {
@@ -58,6 +59,8 @@ var AuthenticationClient = function (options) {
 
   var managerOptions = {
     clientId: options.clientId,
+    domain: options.domain,
+    clientSecret: options.clientSecret,
     headers: {
       'User-agent': 'node.js/' + process.version.replace('v', ''),
       'Content-Type': 'application/json'
@@ -476,5 +479,30 @@ AuthenticationClient.prototype.requestChangePasswordEmail = function (data, cb)
  */
 utils.wrapPropertyMethod(AuthenticationClient, 'getProfile', 'users.getInfo');
 
+/**
+ * Gets an access token using the client credentials grant flow.
+ *
+ * @method    clientCredentialsGrant
+ * @memberOf  module:auth.AuthenticationClient.prototype
+ *
+ * @example <caption>
+ *   Gets an access token using the client credentials grant flow. Find more information in the
+ *   <a href="https://auth0.com/docs/api-auth/config/asking-for-access-tokens">API Docs</a>.
+ * </caption>
+ *
+ * auth0.clientCredentialsGrant(data, function (err, response) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(response);
+ * });
+ *
+ * @param     {String}  scope   
+ *
+ * @return    {Promise|undefined}
+ */
+utils.wrapPropertyMethod(AuthenticationClient, 'clientCredentialsGrant', 'oauth.clientCredentialsGrant');
+
 
 module.exports = AuthenticationClient;
diff --git a/src/utils.js b/src/utils.js
index fcb475ab8..dbc9b273a 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -72,5 +72,4 @@ utils.getRequestPromise = function (settings) {
     });
     
   });
-  
 }


Test patch:
diff --git a/test/auth/oauth.tests.js b/test/auth/oauth.tests.js
index 962e17b21..ecc8ee33c 100644
--- a/test/auth/oauth.tests.js
+++ b/test/auth/oauth.tests.js
@@ -8,13 +8,15 @@ var SRC_DIR = '../../src';
 var DOMAIN = 'tenant.auth0.com';
 var API_URL = 'https://' + DOMAIN;
 var CLIENT_ID = 'TEST_CLIENT_ID';
+var CLIENT_SECRET = 'TEST_CLIENT_SECRET';
 
 var ArgumentError = require(SRC_DIR + '/exceptions').ArgumentError;
 var Authenticator = require(SRC_DIR + '/auth/OAuthAuthenticator');
 
 var validOptions = {
   baseUrl: API_URL,
-  clientId: CLIENT_ID
+  clientId: CLIENT_ID,
+  clientSecret: CLIENT_SECRET,
 };
 
 
@@ -465,4 +467,171 @@ describe('OAuthAuthenticator', function () {
     });
   });
 
+  describe('#clientCredentials', function () {
+    var path = '/oauth/token';
+    var options = {
+      audience: 'audience',
+      scope: 'scope'
+    };
+
+    beforeEach(function () {
+      this.authenticator = new Authenticator(validOptions);
+      this.request = nock(API_URL)
+        .post(path)
+        .reply(200);
+    });
+
+
+    it('should require an object as first argument', function () {
+      expect(this.authenticator.clientCredentialsGrant)
+        .to.throw(ArgumentError, 'Missing options object');
+    });
+
+    it('should require the client_id', function () {
+      var authenticator = new Authenticator({});
+      expect(function(){
+        authenticator.clientCredentialsGrant({})
+      }).to.throw(ArgumentError, 'client_id field is required');
+    });
+
+    it('should require the client_secret', function () {
+      var authenticator = new Authenticator({
+        clientId: CLIENT_ID
+      });
+      expect(function(){
+        authenticator.clientCredentialsGrant({})
+      }).to.throw(ArgumentError, 'client_secret field is required');
+    });
+
+    it('should accept a callback', function (done) {
+      this
+        .authenticator
+        .clientCredentialsGrant(options, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is provided', function () {
+      return this
+        .authenticator
+        .clientCredentialsGrant(options);
+    });
+
+
+    it('should perform a POST request to ' + path, function () {
+      var request = this.request;
+
+      return this
+        .authenticator
+        .clientCredentialsGrant(options)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+        });
+    });
+
+
+    it('should include the options in the request', function () {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function (body) {
+          for (var property in options) {
+            if (options[property] !== body[property]) {
+              return false;
+            }
+          }
+
+          return true;
+        })
+        .reply(200);
+
+      return this
+        .authenticator
+        .clientCredentialsGrant(options)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+        });
+    });
+
+
+    it('should include the Auth0 client ID and secret in the request', function () {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function (body) {
+          return body.client_id === CLIENT_ID;
+          return body.client_secret === CLIENT_SECRET;
+        })
+        .reply(200);
+
+      return this
+        .authenticator
+        .clientCredentialsGrant(options)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+        });
+    });
+
+
+    it('should allow the user to specify the audience and scope', function () {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function (body) {
+          return body.audience === 'audience' && body.scope === 'scope';
+        })
+        .reply(200);
+
+      return this
+        .authenticator
+        .clientCredentialsGrant(options)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+        });
+    });
+
+
+    it('should use client_credentials as default grant type', function () {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function (body) {
+          return body.grant_type === 'client_credentials';
+        })
+        .reply(200);
+
+      return this
+        .authenticator
+        .clientCredentialsGrant(options)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+        });
+    });
+
+
+    it('should allow the user to specify the grant type', function () {
+      nock.cleanAll();
+
+      var data = extend({ grant_type: 'TEST_GRANT' }, options);
+      var request = nock(API_URL)
+        .post(path, function (body) {
+          return body.grant_type === 'TEST_GRANT';
+        })
+        .reply(200);
+
+      return this
+        .authenticator
+        .clientCredentialsGrant(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+        });
+    });
+
+  });
+
 });