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
+ * 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
+ *
+ * API Docs
+ * .
+ *
+ *
+ * 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;
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);
+ });
+ });
});