diff --git a/bin/load_gen b/bin/load_gen index aa858490e194c7a1af8834a882d31289f617d530..a5b33cf11b66c6a0917c9578c29e2bfbefe1a633 100755 --- a/bin/load_gen +++ b/bin/load_gen @@ -61,7 +61,7 @@ var argv = require('optimist') .string('s') .describe('s', 'base URL to browserid server') .check(function(argv) { - return (typeof argv.s === 'string' || argv.l) != undefined; + return (argv.h || typeof argv.s === 'string' || argv.l) != undefined; }) .alias('v', 'verifier') .describe('v', 'base URL to verifier service (default is browserid server + \'/verify\')') diff --git a/lib/load_gen/common.js b/lib/load_gen/common.js index 0a7cf61789c473e022578ff4195436de7309cd06..cd94b5ade023e161b63719957271f56ef26bdde2 100644 --- a/lib/load_gen/common.js +++ b/lib/load_gen/common.js @@ -67,7 +67,7 @@ exports.genAssertionAndVerify = function(cfg, user, ctx, email, audience, cb) { try { if (!typeof JSON.parse(r.body) === 'object') throw 'bogus response'; } catch(e) { - return cb(e.toString() + " - " + r.body); + return cb(e.toString() + (r ? (" - " + r.body) : "")); } var assertion = crypto.getAssertion({ diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 4b20930d2604b35332b7cb58574f7934f6829b23..3447756e277df0647906734557dec594441950bc 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -419,6 +419,11 @@ button.delete:active { #edit_password { margin-bottom: 10px; + display: none; +} + +.canSetPassword #edit_password { + display: block; } #edit_password label { diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index b6b935f0558ab574609aedf6098e6624bd398b1b..01d972fd16cd59ff49853265762e43b06059f427 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -157,12 +157,6 @@ BrowserID.Modules.Actions = (function() { user.logoutUser(self.publish.bind(self, "logged_out"), self.getErrorDialog(errors.logoutUser)); }, - doSyncThenPickEmail: function(options) { - var self = this; - user.syncEmails(self.doPickEmail.bind(self, options), - self.getErrorDialog(errors.syncEmails)); - }, - doCheckAuth: function() { var self=this; user.checkAuthenticationAndSync(function(authenticated) { diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js index 1ab9faf3204fa989df54ecaa1cb89eb442fd4ecf..59b4f351dba29400c97abc605b0a96f9a1873695 100644 --- a/resources/static/dialog/controllers/required_email.js +++ b/resources/static/dialog/controllers/required_email.js @@ -63,39 +63,34 @@ BrowserID.Modules.RequiredEmail = (function() { function signIn(callback) { var self = this; - // If the user is already authenticated and they own this address, sign - // them in. - if (authenticated) { - if(primaryInfo) { - closePrimaryUser.call(self, callback); - } - else { - dialogHelpers.getAssertion.call(self, email, callback); - } + function getAssertion() { + dialogHelpers.getAssertion.call(self, email, callback); + } + + if(primaryInfo) { + // With a primary, just go to the primary flow, it'll be taken care of. + closePrimaryUser.call(self, callback); + } + else if (authenticated) { + // If the user is already authenticated and they own this address, sign + // them in. + getAssertion(); } else { - if(primaryInfo) { - closePrimaryUser.call(self, callback); - } - else { - // If the user is not already authenticated, but they potentially own - // this address, try and sign them in and generate an assertion if they - // get the password right. - var password = helpers.getAndValidatePassword("#password"); - if (password) { - dialogHelpers.authenticateUser.call(self, email, password, function(authenticated) { - if (authenticated) { - // Now that the user has authenticated, sync their emails and get an - // assertion for the email we care about. - user.syncEmailKeypair(email, function() { - dialogHelpers.getAssertion.call(self, email, callback); - }, self.getErrorDialog(errors.syncEmailKeypair, callback)); - } - else { - callback && callback(); - } - }); - } + // If the user is not already authenticated, but they potentially own + // this address, try and sign them in and generate an assertion if they + // get the password right. + var password = helpers.getAndValidatePassword("#password"); + if (password) { + dialogHelpers.authenticateUser.call(self, email, password, function(authenticated) { + if (authenticated) { + // Now that the user has authenticated, we can get an assertion. + getAssertion(); + } + else { + callback && callback(); + } + }); } } } diff --git a/resources/static/dialog/resources/state_machine.js b/resources/static/dialog/resources/state_machine.js index e4b86b772e3eb3e3718f23f708731ba393dfe30f..488c57638fdab71539e9ba80252d018d63aa563b 100644 --- a/resources/static/dialog/resources/state_machine.js +++ b/resources/static/dialog/resources/state_machine.js @@ -202,10 +202,6 @@ startState("doEmailChosen", info); }); - subscribe("authenticate_with_required_email", function(msg, info) { - startState("doAuthenticateWithRequiredEmail", info); - }); - subscribe("pick_email", function() { startState("doPickEmail", { origin: self.hostname, @@ -216,13 +212,18 @@ subscribe("email_chosen", function(msg, info) { var idInfo = storage.getEmail(info.email); if(idInfo) { - if(idInfo.type === "primary" && !idInfo.cert) { - // If the email is a primary, and their cert is not available, - // throw the user down the primary flow. - // Doing so will catch cases where the primary certificate is expired - // and the user must re-verify with their IdP. This flow will - // generate its own assertion when ready. - publish("primary_user", info); + if(idInfo.type === "primary") { + if(idInfo.cert) { + startState("doEmailChosen", info); + } + else { + // If the email is a primary, and their cert is not available, + // throw the user down the primary flow. + // Doing so will catch cases where the primary certificate is expired + // and the user must re-verify with their IdP. This flow will + // generate its own assertion when ready. + publish("primary_user", info); + } } else { startState("doEmailChosen", info); @@ -242,10 +243,7 @@ }); subscribe("authenticated", function(msg, info) { - startState("doSyncThenPickEmail", { - origin: self.hostname, - allow_persistent: self.allowPersistent - }); + mediator.publish("pick_email"); }); subscribe("forgot_password", function(msg, info) { diff --git a/resources/static/pages/manage_account.js b/resources/static/pages/manage_account.js index 896099c622d8dbe95e66712f6e64ad8089522125..44e38a9e7a56dafed6153877c8ffdde499c8f60c 100644 --- a/resources/static/pages/manage_account.js +++ b/resources/static/pages/manage_account.js @@ -131,7 +131,6 @@ BrowserID.manageAccount = (function() { } function syncAndDisplayEmails(oncomplete) { - user.syncEmails(function() { displayStoredEmails(oncomplete); }, pageHelpers.getFailure(errors.syncEmails, oncomplete)); @@ -265,6 +264,13 @@ BrowserID.manageAccount = (function() { storage.manage_page.set("has_visited_manage_page", true); } + function displayChangePassword(oncomplete) { + user.canSetPassword(function(canSetPassword) { + dom[canSetPassword ? "addClass" : "removeClass"]("body", "canSetPassword"); + oncomplete && oncomplete(); + }, pageHelpers.getFailure(errors.hasSecondary)); + } + function init(options, oncomplete) { options = options || {}; @@ -277,9 +283,10 @@ BrowserID.manageAccount = (function() { dom.bindEvent("button.done", "click", cancelEdit); dom.bindEvent("#edit_password_form", "submit", cancelEvent(changePassword)); - syncAndDisplayEmails(oncomplete); - - displayHelpTextToNewUser(); + syncAndDisplayEmails(function() { + displayHelpTextToNewUser(); + displayChangePassword(oncomplete); + }); } // BEGIN TESTING API diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index b40308c96e66bbfd3fa637b8a6a1b253be6669f8..e8d2c148fd5b933a2fe7cbf2680dbad0a7df07d2 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -521,6 +521,18 @@ BrowserID.User = (function() { }, onFailure); }, + /** + * Check if the user can set their password. Only returns true for users + * with secondary accounts + * @method canSetPassword + * @param {function} [onComplete] - Called on with boolean flag on + * successful completion. + * @param {function} [onFailure] - Called on error. + */ + canSetPassword: function(onComplete, onFailure) { + User.hasSecondary(onComplete, onFailure); + }, + /** * Set the initial password of the current user. * @method setPassword @@ -691,7 +703,8 @@ BrowserID.User = (function() { }, /** - * Authenticate the user with the given email and password. + * Authenticate the user with the given email and password. This will sync + * the user's addresses. * @method authenticate * @param {string} email - Email address to authenticate. * @param {string} password - Password. @@ -702,8 +715,14 @@ BrowserID.User = (function() { authenticate: function(email, password, onComplete, onFailure) { network.authenticate(email, password, function(authenticated) { setAuthenticationStatus(authenticated); - if (onComplete) + + if(authenticated) { + User.syncEmails(function() { + onComplete && onComplete(authenticated); + }, onFailure); + } else if (onComplete) { onComplete(authenticated); + } }, onFailure); }, @@ -951,7 +970,7 @@ BrowserID.User = (function() { else { // we have no key for this identity, go generate the key, // sync it and then get the assertion recursively. - User.syncEmailKeypair(email, function() { + User.syncEmailKeypair(email, function(status) { User.getAssertion(email, audience, onComplete, onFailure); }, onFailure); } @@ -1033,7 +1052,30 @@ BrowserID.User = (function() { onComplete(false); } }, onFailure); + }, + + /** + * Check if the user has any secondary addresses. + * @method hasSecondary + * @param {function} onComplete - called with true if user has at least one + * email address, false otw. + * @param {function} onFailure - called on XHR failure. + */ + hasSecondary: function(onComplete, onFailure) { + var hasSecondary = false, + emails = storage.getEmails(); + + for(var key in emails) { + if(emails[key].type === "secondary") { + hasSecondary = true; + break; + } + } + + onComplete(hasSecondary); } + + }; User.setOrigin(document.location.host); diff --git a/resources/static/test/qunit/controllers/required_email_unit_test.js b/resources/static/test/qunit/controllers/required_email_unit_test.js index e806045deacf239d1d77316c79e478c89642f972..ead470e41ce7548b7afa2f198efc722fd6281572 100644 --- a/resources/static/test/qunit/controllers/required_email_unit_test.js +++ b/resources/static/test/qunit/controllers/required_email_unit_test.js @@ -357,7 +357,7 @@ authenticated: false }); - var email = "registered@testuser.com"; + var email = "testuser@testuser.com"; xhr.useResult("known_secondary"); createController({ diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js index f77340dc6e2867596341b02682b400a84cb9543b..7438c4dba2c3029f4c991fbd4aeb5068344765c9 100644 --- a/resources/static/test/qunit/mocks/xhr.js +++ b/resources/static/test/qunit/mocks/xhr.js @@ -1,5 +1,5 @@ /*jshint browsers:true, forin: true, laxbreak: true */ -/*global wrappedAsyncTest: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */ +/*global start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * @@ -113,9 +113,11 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/email_addition_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" }, "get /wsapi/email_addition_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" }, "get /wsapi/email_addition_status?email=registered%40testuser.com ajaxError": undefined, - "get /wsapi/list_emails valid": {"testuser@testuser.com":{}}, + "get /wsapi/list_emails valid": {"testuser@testuser.com":{ type: "secondary" }}, + //"get /wsapi/list_emails known_secondary": {"registered@testuser.com":{ type: "secondary" }}, + "get /wsapi/list_emails primary": {"testuser@testuser.com": { type: "primary" }}, "get /wsapi/list_emails multiple": {"testuser@testuser.com":{}, "testuser2@testuser.com":{}}, - "get /wsapi/list_emails no_identities": [], + "get /wsapi/list_emails no_identities": {}, "get /wsapi/list_emails ajaxError": undefined, // Used in conjunction with registration to do a complete userflow "get /wsapi/list_emails complete": {"registered@testuser.com":{}}, diff --git a/resources/static/test/qunit/pages/manage_account_unit_test.js b/resources/static/test/qunit/pages/manage_account_unit_test.js index 50c679253ce5452f2a9778ea726c946c79f54f01..0d12a81f807edcb3c905325caa372cd7063dbc8e 100644 --- a/resources/static/test/qunit/pages/manage_account_unit_test.js +++ b/resources/static/test/qunit/pages/manage_account_unit_test.js @@ -38,12 +38,10 @@ "use strict"; var bid = BrowserID, - user = bid.User, xhr = bid.Mocks.xhr, errorScreen = bid.Screens.error, + storage = bid.Storage, testHelpers = bid.TestHelpers, - validToken = true, - TEST_ORIGIN = "http://browserid.org", tooltip = bid.Tooltip, mocks = { confirm: function() { return true; }, @@ -53,7 +51,6 @@ module("pages/manage_account", { setup: function() { testHelpers.setup(); - user.setOrigin(TEST_ORIGIN); bid.Renderer.render("#page_head", "site/index", {}); mocks.document.location = ""; }, @@ -166,6 +163,24 @@ }); }); + asyncTest("user with only primary emails should not have 'canSetPassword' class", function() { + xhr.useResult("primary"); + + bid.manageAccount(mocks, function() { + equal($("body").hasClass("canSetPassword"), false, "canSetPassword class not added to body"); + start(); + }); + }); + + asyncTest("user with >= 1 secondary email should see have 'canSetPassword' class", function() { + storage.addEmail("primary_user@primaryuser.com", { type: "secondary" }); + + bid.manageAccount(mocks, function() { + equal($("body").hasClass("canSetPassword"), true, "canSetPassword class added to body"); + start(); + }); + }); + asyncTest("changePassword with missing old password, expect tooltip", function() { bid.manageAccount(mocks, function() { $("#old_password").val(""); diff --git a/resources/static/test/qunit/resources/state_machine_unit_test.js b/resources/static/test/qunit/resources/state_machine_unit_test.js index 0a413215b3c44b8ecb223ae78c36ad619071c74e..d0df3c9e6837e4b893768495ce4267030d499a27 100644 --- a/resources/static/test/qunit/resources/state_machine_unit_test.js +++ b/resources/static/test/qunit/resources/state_machine_unit_test.js @@ -151,7 +151,7 @@ test("authenticated", function() { mediator.publish("authenticated"); - ok(actions.called.doSyncThenPickEmail, "doSyncThenPickEmail has been called"); + ok(actions.called.doPickEmail, "doPickEmail has been called"); }); test("forgot_password", function() { @@ -261,13 +261,16 @@ }); - test("email_chosen with secondary email - call doEmailChosen", function() { + test("email_chosen with secondary email, user must authenticate - call doAuthenticateWithRequiredEmail", function() { + + }); + + test("email_chosen with secondary email, user authenticated to secondary - call doEmailChosen", function() { var email = "testuser@testuser.com"; storage.addEmail(email, { type: "secondary" }); mediator.publish("email_chosen", { email: email }); equal(actions.called.doEmailChosen, true, "doEmailChosen called"); - }); test("email_chosen with primary email - call doProvisionPrimaryUser", function() { diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js index 8ba403ec6bc1638e6359a66c018d3e28371f5cde..50564754d25a5725eeeb25d6cd69b1f740bc8ad8 100644 --- a/resources/static/test/qunit/shared/user_unit_test.js +++ b/resources/static/test/qunit/shared/user_unit_test.js @@ -469,15 +469,45 @@ var vep = require("./vep"); ); }); - /* - asyncTest("setPassword", function() { - lib.setPassword("password", function() { - // XXX fill this in. - ok(true); + asyncTest("canSetPassword with only primary addresses - expect false", function() { + storage.addEmail("testuser@testuser.com", { type: "primary" }); + + lib.canSetPassword(function(status) { + equal(false, status, "status is false with user with only primaries"); start(); - }); + }, testHelpers.unexpectedFailure); }); - */ + + asyncTest("canSetPassword with secondary addresses - expect true", function() { + storage.addEmail("testuser@testuser.com", { type: "secondary" }); + + lib.canSetPassword(function(status) { + equal(true, status, "status is true with user with secondaries"); + start(); + }, testHelpers.unexpectedFailure); + }); + + asyncTest("setPassword with XHR failure", function() { + xhr.useResult("ajaxError"); + + lib.setPassword( + "password", + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure + ); + }); + + asyncTest("setPassword success", function() { + lib.setPassword( + "password", + function(status) { + ok(status, true, "status is true for success"); + start(); + }, + testHelpers.expectedXHRFailure + ); + }); + asyncTest("requestPasswordReset with known email", function() { lib.requestPasswordReset("registered@testuser.com", function(status) { equal(status.success, true, "password reset for known user"); @@ -507,9 +537,11 @@ var vep = require("./vep"); }); - asyncTest("authenticate with valid credentials", function() { + asyncTest("authenticate with valid credentials, also syncs email with server", function() { lib.authenticate("testuser@testuser.com", "testuser", function(authenticated) { equal(true, authenticated, "we are authenticated!"); + var emails = lib.getStoredEmailKeypairs(); + equal(_.size(emails) > 0, true, "emails have been synced to server"); start(); }, testHelpers.unexpectedXHRFailure); }); @@ -1158,4 +1190,19 @@ var vep = require("./vep"); ); }); + asyncTest("hasSecondary returns false if the user has 0 secondary email address", function() { + lib.hasSecondary(function(hasSecondary) { + equal(hasSecondary, false, "hasSecondary is false"); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("hasSecondary returns true if the user has at least one secondary email address", function() { + storage.addEmail("testuser@testuser.com", { type: "secondary" }); + lib.hasSecondary(function(hasSecondary) { + equal(hasSecondary, true, "hasSecondary is true"); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + }()); diff --git a/tests/verifier-test.js b/tests/verifier-test.js index d0db8a31a77e1f9b6809e3766e137d6d7be2fb09..c960717b24fb5046e97daca1f18e789e55a9be5f 100755 --- a/tests/verifier-test.js +++ b/tests/verifier-test.js @@ -604,9 +604,12 @@ function make_crazy_assertion_tests(new_style) { tok.sign(g_keypair.secretKey), new_style); }, - "and removing the last char from it": { + "and removing the last two chars from it": { topic: function(assertion) { - assertion = assertion.substr(0, assertion.length - 1); + // we used to chop off one char, but because of + // robustness in base64-decoding, that still worked 25% + // of the time. No need to build this knowledge in here. + assertion = assertion.substr(0, assertion.length - 2); wsapi.post('/verify', { audience: TEST_ORIGIN, assertion: assertion