Repository: auth0/node-auth0
Pull request #290 (2018-07-27 21:24:00)

Added authorizationCodeGrant method to OAuthAuthenticator
This PR adds a method to exchange an authorization code for a Bearer token containing the access_token & id_token.

closes: https://github.com/auth0/node-auth0/issues/284


Patch:
diff --git a/src/auth/OAuthAuthenticator.js b/src/auth/OAuthAuthenticator.js
index ce4768c69..0af66bdf1 100644
--- a/src/auth/OAuthAuthenticator.js
+++ b/src/auth/OAuthAuthenticator.js
@@ -243,4 +243,73 @@ OAuthAuthenticator.prototype.clientCredentialsGrant = function(options, cb) {
   return this.oauth.create(params, data);
 };
 
+/**
+ * Sign in using an authorization code
+ *
+ * @method    authorizationCodeGrant
+ * @memberOf  module:auth.OAuthAuthenticator.prototype
+ *
+ * @example <caption>
+ *   Given the code returned in the URL params after the redirect
+ *   from successful authentication, exchange the code for auth0
+ *   credentials. It will return JSON with the access_token and id_token.
+ *   More information in the
+ *   <a href="https://auth0.com/docs/api/authentication#authorization-code-grant">
+ *     API Docs
+ *   </a>.
+ * </caption>
+ *
+ * var data = {
+ *   code: '{CODE}',
+ *   redirect_uri: '{REDIRECT_URI}',
+ *   client_id: '{CLIENT_ID}',  // Optional field.
+ *   client_secret: '{CLIENT_SECRET}',  // Optional field.
+ * };
+ *
+ * auth0.oauth.authorizationCodeGrant(data, function (err, userData) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(userData);
+ * });
+ *
+ * @param   {Object}    data                  Authorization code payload
+ * @param   {String}    userData.code         Code in URL returned after authentication
+ * @param   {String}    userData.redirect_uri The URL to which Auth0 will redirect the browser after authorization has been granted by the user.
+ *
+ * @return  {Promise|undefined}
+ */
+OAuthAuthenticator.prototype.authorizationCodeGrant = function(options, cb) {
+  var params = {
+    type: 'token'
+  };
+
+  var defaultFields = {
+    grant_type: 'authorization_code',
+    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.code || data.code.trim().length === 0) {
+    throw new ArgumentError('code field is required');
+  }
+
+  if (!data.redirect_uri || data.redirect_uri.trim().length === 0) {
+    throw new ArgumentError('redirect_uri field is required');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.oauth.create(params, data, cb);
+  }
+
+  return this.oauth.create(params, data);
+};
+
 module.exports = OAuthAuthenticator;


Test patch:
diff --git a/test/auth/oauth.tests.js b/test/auth/oauth.tests.js
index fdd5f3e83..03e652f1d 100644
--- a/test/auth/oauth.tests.js
+++ b/test/auth/oauth.tests.js
@@ -38,7 +38,7 @@ describe('OAuthAuthenticator', function() {
   });
 
   describe('instance', function() {
-    var methods = ['signIn', 'socialSignIn', 'passwordGrant'];
+    var methods = ['signIn', 'socialSignIn', 'passwordGrant', 'authorizationCodeGrant'];
     var authenticator = new Authenticator(validOptions);
 
     methods.forEach(function(method) {
@@ -674,4 +674,146 @@ describe('OAuthAuthenticator', function() {
       });
     });
   });
+
+  describe('#authorizationCodeGrant', function() {
+    var path = '/oauth/token';
+    var data = {
+      code: 'auth_code',
+      redirect_uri: API_URL
+    };
+
+    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.authorizationCodeGrant).to.throw(
+        ArgumentError,
+        'Missing options object'
+      );
+    });
+
+    it('should require a code', function() {
+      var auth = this.authenticator;
+      var signIn = auth.authorizationCodeGrant.bind(auth, { redirect: API_URL });
+
+      expect(signIn).to.throw(ArgumentError, 'code field is required');
+    });
+
+    it('should require a redirect_uri', function() {
+      var auth = this.authenticator;
+      var signIn = auth.authorizationCodeGrant.bind(auth, { code: 'auth_code' });
+
+      expect(signIn).to.throw(ArgumentError, 'redirect_uri field is required');
+    });
+
+    it('should accept a callback', function(done) {
+      this.authenticator.authorizationCodeGrant(data, done.bind(null, null));
+    });
+
+    it('should return a promise when no callback is provided', function(done) {
+      this.authenticator
+        .authorizationCodeGrant(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+    it('should perform a POST request to ' + path, function(done) {
+      var request = this.request;
+
+      this.authenticator
+        .authorizationCodeGrant(data)
+        .then(function() {
+          expect(request.isDone()).to.be.true;
+
+          done();
+        })
+        .catch(done);
+    });
+
+    it('should include the data in the request', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function(body) {
+          for (var property in data) {
+            if (data[property] !== body[property]) {
+              return false;
+            }
+          }
+
+          return true;
+        })
+        .reply(200);
+
+      this.authenticator
+        .authorizationCodeGrant(data)
+        .then(function() {
+          expect(request.isDone()).to.be.true;
+
+          done();
+        })
+        .catch(done);
+    });
+
+    it('should include the Auth0 client ID in the request', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function(body) {
+          return body.client_id === CLIENT_ID;
+        })
+        .reply(200);
+
+      this.authenticator
+        .authorizationCodeGrant(data)
+        .then(function() {
+          expect(request.isDone()).to.be.true;
+
+          done();
+        })
+        .catch(done);
+    });
+
+    it('should include the Auth0 client secret in the request', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function(body) {
+          return body.client_secret === CLIENT_SECRET;
+        })
+        .reply(200);
+
+      this.authenticator
+        .authorizationCodeGrant(data)
+        .then(function() {
+          expect(request.isDone()).to.be.true;
+
+          done();
+        })
+        .catch(done);
+    });
+
+    it('should use authorization_code as default grant type', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post(path, function(body) {
+          return body.grant_type === 'authorization_code';
+        })
+        .reply(200);
+
+      this.authenticator
+        .authorizationCodeGrant(data)
+        .then(function() {
+          expect(request.isDone()).to.be.true;
+
+          done();
+        })
+        .catch(done);
+    });
+  });
 });