diff --git a/src/auth/OAuthAuthenticator.js b/src/auth/OAuthAuthenticator.js index 02f813eb7..22a58a475 100644 --- a/src/auth/OAuthAuthenticator.js +++ b/src/auth/OAuthAuthenticator.js @@ -17,6 +17,9 @@ function getParamsFromOptions(options) { req.set('auth0-forwarded-for', options.forwardedFor); }; } + if (options.type) { + params.type = options.type; + } return params; } @@ -117,7 +120,10 @@ OAuthAuthenticator.prototype.signIn = function(userData, options, cb) { throw new ArgumentError('Missing user data object'); } - if (typeof data.connection !== 'string' || data.connection.split().length === 0) { + if ( + params.type === 'ro' && + (typeof data.connection !== 'string' || data.connection.split().length === 0) + ) { throw new ArgumentError('connection field is required'); } diff --git a/src/auth/PasswordlessAuthenticator.js b/src/auth/PasswordlessAuthenticator.js index 794649604..38e1122ad 100644 --- a/src/auth/PasswordlessAuthenticator.js +++ b/src/auth/PasswordlessAuthenticator.js @@ -48,7 +48,7 @@ var PasswordlessAuthenticator = function(options, oauth) { * @example * Given the user credentials (`phone_number` and `code`), it will do the * authentication on the provider and return a JSON with the `access_token` - * and `id_token`. + * and `id_token` using `/oauth/ro` endpoint. * * * var data = { @@ -63,6 +63,21 @@ var PasswordlessAuthenticator = function(options, oauth) { * }); * * @example + * To use `/oauth/token` endpoint, use `otp` and `realm` instead + * + * + * var data = { + * username: '{PHONE_NUMBER}', + * otp: '{VERIFICATION_CODE}' + * }; + * + * auth0.passwordless.signIn(data, function (err) { + * if (err) { + * // Handle error. + * } + * }); + * + * @example * The user data object has the following structure. * * @@ -73,9 +88,11 @@ var PasswordlessAuthenticator = function(options, oauth) { * } * * @param {Object} userData User credentials object. - * @param {String} userData.username Username. - * @param {String} userData.password Password. - * @param {String} [userData.connection=sms] Connection string: "sms" or "email". + * @param {String} userData.otp The user's verification code. + * @param {String} [userData.realm=sms] Realm string: "sms" or "email". + * @param {String} userData.username The user's phone number if realm=sms, or the user's email if realm=email + * @param {String} userData.password [DEPRECATED] Password. + * @param {String} [userData.connection=sms] [DEPRECATED] Connection string: "sms" or "email". * @param {Function} [cb] Method callback. * * @return {Promise|undefined} @@ -87,12 +104,6 @@ PasswordlessAuthenticator.prototype.signIn = function(userData, cb) { }; var data = extend(defaultFields, userData); - // Don't let the user override the connection nor the grant type. - if (!data.connection || (data.connection !== 'email' && data.connection !== 'sms')) { - data.connection = 'sms'; - } - data.grant_type = 'password'; - if (!userData || typeof userData !== 'object') { throw new ArgumentError('Missing user data object'); } @@ -101,10 +112,29 @@ PasswordlessAuthenticator.prototype.signIn = function(userData, cb) { throw new ArgumentError('username field (phone number) is required'); } + // If otp is provided, attempt to sign in using otp grant + if (typeof data.otp === 'string' && data.otp.trim().length > 0) { + if (!data.realm || (data.realm !== 'email' && data.realm !== 'sms')) { + data.realm = 'sms'; + } + data.grant_type = 'http://auth0.com/oauth/grant-type/passwordless/otp'; + return this.oauth.signIn(data, { type: 'token' }, cb); + } + + // Don't let the user override the connection nor the grant type. + if (!data.connection || (data.connection !== 'email' && data.connection !== 'sms')) { + data.connection = 'sms'; + } + data.grant_type = 'password'; + if (typeof data.password !== 'string' || data.password.trim().length === 0) { throw new ArgumentError('password field (verification code) is required'); } + console.warn( + 'The oauth/ro endpoint has been deprecated. Please use the realm and otp parameters in this function.' + ); + return this.oauth.signIn(data, cb); }; diff --git a/test/auth/passwordless.tests.js b/test/auth/passwordless.tests.js index 67271376d..c694ad545 100644 --- a/test/auth/passwordless.tests.js +++ b/test/auth/passwordless.tests.js @@ -48,202 +48,404 @@ describe('PasswordlessAuthenticator', function() { }); describe('#signIn', function() { - var path = '/oauth/ro'; - var userData = { - username: 'username', - password: 'pwd' - }; - - beforeEach(function() { - var oauth = new OAuth(validOptions); - this.authenticator = new Authenticator(validOptions, oauth); - this.request = nock(API_URL) - .post(path) - .reply(200); - }); + describe('/oauth/ro', function() { + var path = '/oauth/ro'; + var userData = { + username: 'username', + password: 'pwd' + }; + + beforeEach(function() { + var oauth = new OAuth(validOptions); + this.authenticator = new Authenticator(validOptions, oauth); + this.request = nock(API_URL) + .post(path) + .reply(200); + }); - it('should require an object as first argument', function() { - expect(this.authenticator.signIn).to.throw(ArgumentError, 'Missing user data object'); - }); + it('should require an object as first argument', function() { + expect(this.authenticator.signIn).to.throw(ArgumentError, 'Missing user data object'); + }); - it('should require a phone number', function() { - var auth = this.authenticator; - var userData = { password: 'password' }; - var signIn = auth.signIn.bind(auth, userData); + it('should require a phone number', function() { + var auth = this.authenticator; + var userData = { password: 'password' }; + var signIn = auth.signIn.bind(auth, userData); - expect(signIn).to.throw(ArgumentError, 'username field (phone number) is required'); - }); + expect(signIn).to.throw(ArgumentError, 'username field (phone number) is required'); + }); - it('should require a verification code', function() { - var auth = this.authenticator; - var userData = { username: 'username' }; - var signIn = auth.signIn.bind(auth, userData); + it('should require a verification code', function() { + var auth = this.authenticator; + var userData = { username: 'username' }; + var signIn = auth.signIn.bind(auth, userData); - expect(signIn).to.throw(ArgumentError, 'password field (verification code) is required'); - }); + expect(signIn).to.throw(ArgumentError, 'password field (verification code) is required'); + }); - it('should accept a callback', function(done) { - this.authenticator.signIn(userData, done.bind(null, null)); - }); + it('should accept a callback', function(done) { + this.authenticator.signIn(userData, done.bind(null, null)); + }); - it('should return a promise when no callback is provided', function(done) { - this.authenticator - .signIn(userData) - .then(done.bind(null, null)) - .catch(done.bind(null, null)); - }); + it('should return a promise when no callback is provided', function(done) { + this.authenticator + .signIn(userData) + .then(done.bind(null, null)) + .catch(done.bind(null, null)); + }); - it('should perform a POST request to ' + path, function(done) { - var request = this.request; + it('should perform a POST request to ' + path, function(done) { + var request = this.request; - this.authenticator - .signIn(userData) - .then(function() { - expect(request.isDone()).to.be.true; + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; - done(); - }) - .catch(done); - }); + done(); + }) + .catch(done); + }); - it('should include the user data in the request', function(done) { - nock.cleanAll(); + it('should include the user data in the request', function(done) { + nock.cleanAll(); - var request = nock(API_URL) - .post(path, function(body) { - for (var property in userData) { - if (userData[property] !== body[property]) { - return false; + var request = nock(API_URL) + .post(path, function(body) { + for (var property in userData) { + if (userData[property] !== body[property]) { + return false; + } } - } - return true; - }) - .reply(200); + return true; + }) + .reply(200); - this.authenticator - .signIn(userData) - .then(function() { - expect(request.isDone()).to.be.true; + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; - done(); - }) - .catch(done); - }); + done(); + }) + .catch(done); + }); - it('should include the Auth0 client ID in the request', function(done) { - nock.cleanAll(); + 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); + var request = nock(API_URL) + .post(path, function(body) { + return body.client_id === CLIENT_ID; + }) + .reply(200); - this.authenticator - .signIn(userData) - .then(function() { - expect(request.isDone()).to.be.true; + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; - done(); - }) - .catch(done); - }); + done(); + }) + .catch(done); + }); - it('should use SMS connection', function(done) { - nock.cleanAll(); + it('should use SMS connection', function(done) { + nock.cleanAll(); - var request = nock(API_URL) - .post(path, function(body) { - return body.connection === 'sms'; - }) - .reply(200); + var request = nock(API_URL) + .post(path, function(body) { + return body.connection === 'sms'; + }) + .reply(200); - this.authenticator - .signIn(userData) - .then(function() { - expect(request.isDone()).to.be.true; + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; - done(); - }) - .catch(done); - }); + done(); + }) + .catch(done); + }); - it('should use email connection', function(done) { - nock.cleanAll(); - var data = extend({ connection: 'email' }, userData); - var request = nock(API_URL) - .post(path, function(body) { - return body.connection === 'email'; - }) - .reply(200); + it('should use email connection', function(done) { + nock.cleanAll(); + var data = extend({ connection: 'email' }, userData); + var request = nock(API_URL) + .post(path, function(body) { + return body.connection === 'email'; + }) + .reply(200); + + this.authenticator + .signIn(data) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); - this.authenticator - .signIn(data) - .then(function() { - expect(request.isDone()).to.be.true; + it('should allow the user to specify the connection as sms or email', function(done) { + nock.cleanAll(); - done(); - }) - .catch(done); - }); + var data = extend({ connection: 'TEST_CONNECTION' }, userData); + var request = nock(API_URL) + .post(path, function(body) { + return body.connection === 'sms' || body.connection === 'email'; + }) + .reply(200); - it('should allow the user to specify the connection as sms or email', function(done) { - nock.cleanAll(); + this.authenticator + .signIn(data) + .then(function() { + expect(request.isDone()).to.be.true; - var data = extend({ connection: 'TEST_CONNECTION' }, userData); - var request = nock(API_URL) - .post(path, function(body) { - return body.connection === 'sms' || body.connection === 'email'; - }) - .reply(200); + done(); + }) + .catch(done); + }); - this.authenticator - .signIn(data) - .then(function() { - expect(request.isDone()).to.be.true; + it('should use password as grant type', function(done) { + nock.cleanAll(); - done(); - }) - .catch(done); - }); + var request = nock(API_URL) + .post(path, function(body) { + return body.grant_type === 'password'; + }) + .reply(200); - it('should use password as grant type', function(done) { - nock.cleanAll(); + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; - var request = nock(API_URL) - .post(path, function(body) { - return body.grant_type === 'password'; - }) - .reply(200); + done(); + }) + .catch(done); + }); - this.authenticator - .signIn(userData) - .then(function() { - expect(request.isDone()).to.be.true; + it('should use the openid scope', function(done) { + nock.cleanAll(); - done(); - }) - .catch(done); + var request = nock(API_URL) + .post(path, function(body) { + return body.scope === 'openid'; + }) + .reply(200); + + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); }); - it('should use the openid scope', function(done) { - nock.cleanAll(); + describe('/oauth/token', function() { + var path = '/oauth/token'; + var userData = { + username: 'username', + otp: '000000' + }; - var request = nock(API_URL) - .post(path, function(body) { - return body.scope === 'openid'; - }) - .reply(200); + beforeEach(function() { + var oauth = new OAuth(validOptions); + this.authenticator = new Authenticator(validOptions, oauth); + this.request = nock(API_URL) + .post(path) + .reply(200); + }); - this.authenticator - .signIn(userData) - .then(function() { - expect(request.isDone()).to.be.true; + it('should require an object as first argument', function() { + expect(this.authenticator.signIn).to.throw(ArgumentError, 'Missing user data object'); + }); - done(); - }) - .catch(done); + it('should require a phone number', function() { + var auth = this.authenticator; + var userData = { otp: '000000' }; + var signIn = auth.signIn.bind(auth, userData); + + expect(signIn).to.throw(ArgumentError, 'username field (phone number) is required'); + }); + + it('should require a verification code', function() { + var auth = this.authenticator; + var userData = { username: 'username' }; + var signIn = auth.signIn.bind(auth, userData); + + expect(signIn).to.throw(ArgumentError, 'password field (verification code) is required'); + }); + + it('should accept a callback', function(done) { + this.authenticator.signIn(userData, done.bind(null, null)); + }); + + it('should return a promise when no callback is provided', function(done) { + this.authenticator + .signIn(userData) + .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 + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); + + it('should include the user data in the request', function(done) { + nock.cleanAll(); + + var request = nock(API_URL) + .post(path, function(body) { + for (var property in userData) { + if (userData[property] !== body[property]) { + return false; + } + } + + return true; + }) + .reply(200); + + this.authenticator + .signIn(userData) + .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 + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); + + it('should use SMS realm', function(done) { + nock.cleanAll(); + + var request = nock(API_URL) + .post(path, function(body) { + return body.realm === 'sms'; + }) + .reply(200); + + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); + + it('should use email realm', function(done) { + nock.cleanAll(); + var data = extend({ realm: 'email' }, userData); + var request = nock(API_URL) + .post(path, function(body) { + return body.realm === 'email'; + }) + .reply(200); + + this.authenticator + .signIn(data) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); + + it('should allow the user to specify the realm as sms or email', function(done) { + nock.cleanAll(); + + var data = extend({ realm: 'TEST_CONNECTION' }, userData); + var request = nock(API_URL) + .post(path, function(body) { + return body.realm === 'sms' || body.realm === 'email'; + }) + .reply(200); + + this.authenticator + .signIn(data) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); + + it('should use otp as grant type', function(done) { + nock.cleanAll(); + + var request = nock(API_URL) + .post(path, function(body) { + return body.grant_type === 'http://auth0.com/oauth/grant-type/passwordless/otp'; + }) + .reply(200); + + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); + + it('should use the openid scope', function(done) { + nock.cleanAll(); + + var request = nock(API_URL) + .post(path, function(body) { + return body.scope === 'openid'; + }) + .reply(200); + + this.authenticator + .signIn(userData) + .then(function() { + expect(request.isDone()).to.be.true; + + done(); + }) + .catch(done); + }); }); });