From c043c17587d760d8a250b65fcf91575a7a8d1002 Mon Sep 17 00:00:00 2001 From: Luis Deschamps Rudge Date: Wed, 31 Jan 2018 23:01:16 -0200 Subject: [PATCH] Adding usernamepassword/login support for hosted login page --- example/index.html | 16 ++ src/web-auth/hosted-pages.js | 98 ++++++++ src/web-auth/index.js | 2 + src/web-auth/username-password.js | 55 ++++ test/web-auth/hosted-pages.test.js | 389 +++++++++++++++++++++++++++++ 5 files changed, 560 insertions(+) create mode 100644 src/web-auth/hosted-pages.js create mode 100644 src/web-auth/username-password.js create mode 100644 test/web-auth/hosted-pages.test.js diff --git a/example/index.html b/example/index.html index 9288a44f..3b045e95 100644 --- a/example/index.html +++ b/example/index.html @@ -123,6 +123,13 @@

Login with database connection:

+
+

Login with database connection (from universal login):

+ + + +
+

Login with passwordless connection:

@@ -261,6 +268,15 @@

Console:

}, htmlConsole.dumpCallback.bind(htmlConsole)); }); + $('.universal-login').click(function (e) { + e.preventDefault(); + webAuth._hostedPages.login({ + connection: 'acme', + username: $('.login-username').val(), + password: $('.login-password').val() + }, htmlConsole.dumpCallback.bind(htmlConsole)); + }); + $('.passwordless-login-verify').click(function (e) { e.preventDefault(); webAuthPasswordless.passwordlessLogin({ diff --git a/src/web-auth/hosted-pages.js b/src/web-auth/hosted-pages.js new file mode 100644 index 00000000..262f7979 --- /dev/null +++ b/src/web-auth/hosted-pages.js @@ -0,0 +1,98 @@ +var UsernamePassword = require('./username-password'); +var objectHelper = require('../helper/object'); +var windowHelper = require('../helper/window'); +var Warn = require('../helper/warn'); +var assert = require('../helper/assert'); + +function HostedPages(client, options) { + this.baseOptions = options; + this.client = client; + + this.warn = new Warn({ + disableWarnings: !!options._disableDeprecationWarnings + }); +} + +/** + * @callback credentialsCallback + * @param {Error} [err] error returned by Auth0 with the reason of the Auth failure + * @param {Object} [result] result of the AuthN request + * @param {String} result.accessToken token that can be used with {@link userinfo} + * @param {String} [result.idToken] token that identifies the user + * @param {String} [result.refreshToken] token that can be used to get new access tokens from Auth0. Note that not all clients can request them or the resource server might not allow them. + */ + +/** + * Performs authentication with username/email and password with a database connection + * + * This method is not compatible with API Auth so if you need to fetch API tokens with audience + * you should use {@link authorize} or {@link login}. + * + * @method loginWithCredentials + * @param {Object} options + * @param {String} [options.redirectUri] url that the Auth0 will redirect after Auth with the Authorization Response + * @param {String} [options.responseType] type of the response used. It can be any of the values `code` and `token` + * @param {String} [options.responseMode] how the AuthN response is encoded and redirected back to the client. Supported values are `query` and `fragment` + * @param {String} [options.scope] scopes to be requested during AuthN. e.g. `openid email` + * @param {credentialsCallback} cb + */ +HostedPages.prototype.login = function(options, cb) { + if (windowHelper.getWindow().location.host !== this.baseOptions.domain) { + throw new Error('This method is meant to be used only inside the Universal Login Page.'); + } + var usernamePassword; + + var params = objectHelper + .merge(this.baseOptions, [ + 'clientID', + 'redirectUri', + 'tenant', + 'responseType', + 'responseMode', + 'scope', + 'audience', + '_csrf', + 'state', + '_intstate', + 'nonce' + ]) + .with(options); + + assert.check( + params, + { type: 'object', message: 'options parameter is not valid' }, + { + responseType: { type: 'string', message: 'responseType option is required' } + } + ); + + usernamePassword = new UsernamePassword(this.baseOptions); + return usernamePassword.login(params, function(err, data) { + if (err) { + return cb(err); + } + return usernamePassword.callback(data); + }); +}; + +/** + * Signs up a new user and automatically logs the user in after the signup. + * + * @method signupAndLogin + * @param {Object} options + * @param {String} options.email user email address + * @param {String} options.password user password + * @param {String} options.connection name of the connection where the user will be created + * @param {credentialsCallback} cb + */ +HostedPages.prototype.signupAndLogin = function(options, cb) { + var _this = this; + return _this.client.client.dbConnection.signup(options, function(err) { + if (err) { + return cb(err); + } + return _this.login(options, cb); + }); +}; + +module.exports = HostedPages; diff --git a/src/web-auth/index.js b/src/web-auth/index.js index 8d6b0f0d..1da3f6ca 100644 --- a/src/web-auth/index.js +++ b/src/web-auth/index.js @@ -14,6 +14,7 @@ var Popup = require('./popup'); var SilentAuthenticationHandler = require('./silent-authentication-handler'); var CrossOriginAuthentication = require('./cross-origin-authentication'); var WebMessageHandler = require('./web-message-handler'); +var HostedPages = require('./hosted-pages'); /** * Handles all the browser's AuthN/AuthZ flows @@ -107,6 +108,7 @@ function WebAuth(options) { this.popup = new Popup(this, this.baseOptions); this.crossOriginAuthentication = new CrossOriginAuthentication(this, this.baseOptions); this.webMessageHandler = new WebMessageHandler(this); + this._hostedPages = new HostedPages(this, this.baseOptions); } /** diff --git a/src/web-auth/username-password.js b/src/web-auth/username-password.js new file mode 100644 index 00000000..81b78cd2 --- /dev/null +++ b/src/web-auth/username-password.js @@ -0,0 +1,55 @@ +var urljoin = require('url-join'); + +var objectHelper = require('../helper/object'); +var RequestBuilder = require('../helper/request-builder'); +var responseHandler = require('../helper/response-handler'); +var windowHelper = require('../helper/window'); +var TransactionManager = require('./transaction-manager'); + +function UsernamePassword(options) { + this.baseOptions = options; + this.request = new RequestBuilder(options); + this.transactionManager = new TransactionManager(this.baseOptions.transaction); +} + +UsernamePassword.prototype.login = function(options, cb) { + var url; + var body; + + url = urljoin(this.baseOptions.rootUrl, 'usernamepassword', 'login'); + + options.username = options.username || options.email; // eslint-disable-line + + options = objectHelper.blacklist(options, ['email']); // eslint-disable-line + + body = objectHelper + .merge(this.baseOptions, [ + 'clientID', + 'redirectUri', + 'tenant', + 'responseType', + 'responseMode', + 'scope', + 'audience' + ]) + .with(options); + body = this.transactionManager.process(body); + + body = objectHelper.toSnakeCase(body, ['auth0Client']); + + return this.request.post(url).send(body).end(responseHandler(cb)); +}; + +UsernamePassword.prototype.callback = function(formHtml) { + var div; + var form; + var _document = windowHelper.getDocument(); + + div = _document.createElement('div'); + div.innerHTML = formHtml; + form = _document.body.appendChild(div).children[0]; + + form.submit(); +}; + +module.exports = UsernamePassword; diff --git a/test/web-auth/hosted-pages.test.js b/test/web-auth/hosted-pages.test.js new file mode 100644 index 00000000..20394535 --- /dev/null +++ b/test/web-auth/hosted-pages.test.js @@ -0,0 +1,389 @@ +var expect = require('expect.js'); +var stub = require('sinon').stub; +var request = require('superagent'); + +var RequestMock = require('../mock/request-mock'); +var UsernamePassword = require('../../src/web-auth/username-password'); +var windowHelper = require('../../src/helper/window'); +var WebAuth = require('../../src/web-auth'); +var TransactionManager = require('../../src/web-auth/transaction-manager'); +var RequestBuilder = require('../../src/helper/request-builder'); + +var telemetryInfo = new RequestBuilder({}).getTelemetryData(); + +describe('auth0.WebAuth._hostedPages', function() { + beforeEach(function() { + stub(TransactionManager.prototype, 'process', function(params) { + return params; + }); + }); + afterEach(function() { + TransactionManager.prototype.process.restore(); + }); + context('login', function() { + beforeEach(function() { + stub(windowHelper, 'getWindow', function() { + return { + location: { + host: 'me.auth0.com' + }, + crypto: { + getRandomValues: function() { + return [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + } + } + }; + }); + }); + afterEach(function() { + windowHelper.getWindow.restore(); + if (request.post.restore) { + request.post.restore(); + } + if (windowHelper.getDocument.restore) { + windowHelper.getDocument.restore(); + } + }); + it('should throw an error if window.location.host !== domain', function() { + var configuration = { + domain: 'other-domain.auth0.com', + redirectUri: 'https://localhost:3000/example/', + clientID: '0HP71GSd6PuoRY', + responseType: 'token' + }; + + var auth0 = new WebAuth(configuration); + + expect(function() { + auth0._hostedPages.login({ + connection: 'tests', + email: 'me@example.com', + password: '1234', + scope: 'openid' + }); + }).throwError('This method is meant to be used only inside the Universal Login Page.'); + }); + + it('should authenticate the user, render the callback form and submit it', function(done) { + stub(request, 'post', function(url) { + expect(url).to.be('https://me.auth0.com/usernamepassword/login'); + return new RequestMock({ + body: { + client_id: '0HP71GSd6PuoRY', + connection: 'tests', + password: '1234', + redirect_uri: 'https://localhost:3000/example/', + response_type: 'id_token', + scope: 'openid', + tenant: 'me', + username: 'me@example.com' + }, + headers: { + 'Content-Type': 'application/json', + 'Auth0-Client': telemetryInfo + }, + cb: function(cb) { + cb(null, { + text: 'the_form_html', + type: 'text/html' + }); + } + }); + }); + + stub(windowHelper, 'getDocument', function() { + return { + createElement: function() { + return {}; + }, + body: { + appendChild: function(element) { + expect(element.innerHTML).to.eql('the_form_html'); + return { + children: [ + { + submit: done + } + ] + }; + } + } + }; + }); + + var configuration = { + domain: 'me.auth0.com', + redirectUri: 'https://localhost:3000/example/', + clientID: '0HP71GSd6PuoRY', + responseType: 'id_token' + }; + + var auth0 = new WebAuth(configuration); + + auth0._hostedPages.login( + { + connection: 'tests', + username: 'me@example.com', + password: '1234', + scope: 'openid' + }, + function(err) { + console.log(err); + } + ); + }); + it('should use transactionManager.process', function(done) { + stub(request, 'post', function() { + expect(TransactionManager.prototype.process.calledOnce).to.be(true); + done(); + }); + + var auth0 = new WebAuth({ + domain: 'me.auth0.com', + redirectUri: 'https://localhost:3000/example/', + clientID: '0HP71GSd6PuoRY', + responseType: 'id_token' + }); + + auth0._hostedPages.login({ + connection: 'tests', + username: 'me@example.com', + password: '1234', + scope: 'openid' + }); + }); + it('should propagate the error', function(done) { + stub(request, 'post', function(url) { + expect(url).to.be('https://me.auth0.com/usernamepassword/login'); + return new RequestMock({ + body: { + client_id: '0HP71GSd6PuoRY', + connection: 'tests', + password: '1234', + redirect_uri: 'https://localhost:3000/example/', + response_type: 'token', + scope: 'openid', + tenant: 'me', + username: 'me@example.com' + }, + headers: { + 'Content-Type': 'application/json', + 'Auth0-Client': telemetryInfo + }, + cb: function(cb) { + cb({ + name: 'ValidationError', + code: 'invalid_user_password', + description: 'Wrong email or password.' + }); + } + }); + }); + + var configuration = { + domain: 'me.auth0.com', + redirectUri: 'https://localhost:3000/example/', + clientID: '0HP71GSd6PuoRY', + responseType: 'token' + }; + + var auth0 = new WebAuth(configuration); + + auth0._hostedPages.login( + { + connection: 'tests', + email: 'me@example.com', + password: '1234', + scope: 'openid' + }, + function(err) { + expect(err).to.eql({ + original: { + name: 'ValidationError', + code: 'invalid_user_password', + description: 'Wrong email or password.' + }, + name: 'ValidationError', + code: 'invalid_user_password', + description: 'Wrong email or password.' + }); + done(); + } + ); + }); + }); + + context('signup and login', function() { + before(function() { + this.auth0 = new WebAuth({ + domain: 'me.auth0.com', + clientID: '...', + redirectUri: 'http://page.com/callback', + responseType: 'token', + _sendTelemetry: false + }); + stub(windowHelper, 'getWindow', function() { + return { + location: { + host: 'me.auth0.com' + }, + crypto: { + getRandomValues: function() { + return [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + } + } + }; + }); + }); + + afterEach(function() { + request.post.restore(); + }); + + after(function() { + windowHelper.getWindow.restore(); + }); + + it('should call db-connection signup with all the options', function(done) { + stub(request, 'post', function(url) { + if (url === 'https://me.auth0.com/usernamepassword/login') { + return new RequestMock({ + body: { + client_id: '...', + connection: 'the_connection', + password: '123456', + redirect_uri: 'http://page.com/callback', + response_type: 'token', + scope: 'openid', + tenant: 'me', + username: 'me@example.com' + }, + headers: { + 'Content-Type': 'application/json' + }, + cb: function(cb) { + cb({ + response: { + body: { + name: 'ValidationError', + code: 'invalid_user_password', + description: 'Wrong email or password.' + }, + statusCode: 400 + } + }); + } + }); + } else if (url === 'https://me.auth0.com/dbconnections/signup') { + return new RequestMock({ + body: { + client_id: '...', + connection: 'the_connection', + email: 'me@example.com', + password: '123456' + }, + headers: { + 'Content-Type': 'application/json' + }, + cb: function(cb) { + cb(null, { + body: { + _id: '...', + email_verified: false, + email: 'me@example.com' + } + }); + } + }); + } + + throw new Error('Invalid URL'); + }); + + this.auth0._hostedPages.signupAndLogin( + { + connection: 'the_connection', + email: 'me@example.com', + password: '123456', + scope: 'openid' + }, + function(err, data) { + expect(data).to.be(undefined); + expect(err).to.eql({ + original: { + response: { + body: { + name: 'ValidationError', + code: 'invalid_user_password', + description: 'Wrong email or password.' + }, + statusCode: 400 + } + }, + name: 'ValidationError', + code: 'invalid_user_password', + description: 'Wrong email or password.', + statusCode: 400 + }); + done(); + } + ); + }); + + it('should propagate signup errors', function(done) { + stub(request, 'post', function(url) { + expect(url).to.be('https://me.auth0.com/dbconnections/signup'); + + return new RequestMock({ + body: { + client_id: '...', + connection: 'the_connection', + email: 'me@example.com', + password: '123456' + }, + headers: { + 'Content-Type': 'application/json' + }, + cb: function(cb) { + cb({ + response: { + statusCode: 400, + body: { + code: 'user_exists', + description: 'The user already exists.' + } + } + }); + } + }); + }); + + this.auth0._hostedPages.signupAndLogin( + { + connection: 'the_connection', + email: 'me@example.com', + password: '123456', + scope: 'openid' + }, + function(err, data) { + expect(data).to.be(undefined); + expect(err).to.eql({ + original: { + response: { + statusCode: 400, + body: { + code: 'user_exists', + description: 'The user already exists.' + } + } + }, + code: 'user_exists', + description: 'The user already exists.', + statusCode: 400 + }); + done(); + } + ); + }); + }); +});