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); + }); + }); });