diff --git a/bin/browserid b/bin/browserid index 6f23273cbcb5e5c86adc231d3831e548b7175c6b..03fa955e44d9c29e754dee7b3bc5721ec32c2155 100755 --- a/bin/browserid +++ b/bin/browserid @@ -24,7 +24,7 @@ config = require('../lib/configuration.js'), heartbeat = require('../lib/heartbeat.js'), metrics = require('../lib/metrics.js'), logger = require('../lib/logging.js').logger, -forward = require('../lib/http_forward'), +forward = require('../lib/http_forward').forward, shutdown = require('../lib/shutdown'), views = require('../lib/browserid/views.js'); diff --git a/bin/proxy b/bin/proxy index 02b2da157cdbf29799957d5d764aec5cdf2e45ce..ce26d11277480ae7ea312db34bcf5c17da061a24 100755 --- a/bin/proxy +++ b/bin/proxy @@ -14,6 +14,9 @@ config = require('../lib/configuration.js'); var port = config.has('bind_to.port') ? config.get('bind_to.port') : 0; var addy = config.has('bind_to.host') ? config.get('bind_to.host') : "127.0.0.1"; +// set a maximum allowed time on responses to declaration of support requests +forward.setTimeout(config.get('declaration_of_support_timeout_ms')); + const allowed = /^https:\/\/[a-zA-Z0-9\.\-_]+\/\.well-known\/browserid$/; var server = http.createServer(function (req, res) { @@ -24,7 +27,7 @@ var server = http.createServer(function (req, res) { return; } - forward(url, req, res, function(err) { + forward.forward(url, req, res, function(err) { if (err) { res.writeHead(400); res.end('Oops: ' + err.toString()); diff --git a/lib/configuration.js b/lib/configuration.js index 7e6c71935737d60d2e9d3e293ee1d4a2c5f2b71c..4e576d079f346b009b903fb9b3d4ae66ea0d4959 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -193,7 +193,11 @@ var conf = module.exports = convict({ env: 'DBWRITER_URL' }, process_type: 'string', - email_to_console: 'boolean = false' + email_to_console: 'boolean = false', + declaration_of_support_timeout_ms: { + doc: "The amount of time we wait for a server to respond with a declaration of support, before concluding that they are not a primary. Only relevant when our local proxy is in use, not in production or staging", + format: 'integer = 15000' + } }); // At the time this file is required, we'll determine the "process name" for this proc diff --git a/lib/db.js b/lib/db.js index 6765c28fbc71ad132d2c3e5dd69352954c2e7b50..1f08f6e99c5d421bb53c4c4a35b5a116e452cf37 100644 --- a/lib/db.js +++ b/lib/db.js @@ -73,20 +73,21 @@ exports.onReady = function(f) { // these are read only database calls [ + 'authForVerificationSecret', + 'checkAuth', + 'emailForVerificationSecret', 'emailKnown', - 'userKnown', - 'isStaged', + 'emailToUID', + 'emailType', 'emailsBelongToSameAccount', - 'emailForVerificationSecret', 'haveVerificationSecret', - 'verificationSecretForEmail', - 'checkAuth', - 'listEmails', + 'isStaged', 'lastStaged', + 'listEmails', 'ping', - 'emailType', + 'userKnown', 'userOwnsEmail', - 'emailToUID' + 'verificationSecretForEmail' ].forEach(function(fn) { exports[fn] = function() { checkReady(); diff --git a/lib/db/json.js b/lib/db/json.js index b9a4c64088258d22a75452e88521af511239dc28..e49615c1fb4f5c2f495c9246cb8bcb4dbb695684 100644 --- a/lib/db/json.js +++ b/lib/db/json.js @@ -175,13 +175,14 @@ function addEmailToAccount(userID, email, type, cb) { }); } -exports.stageUser = function(email, cb) { +exports.stageUser = function(email, hash, cb) { secrets.generate(48, function(secret) { // overwrite previously staged users sync(); db.staged[secret] = { type: "add_account", email: email, + passwd: hash, when: (new Date()).getTime() }; db.stagedEmails[email] = secret; @@ -190,7 +191,7 @@ exports.stageUser = function(email, cb) { }); }; -exports.stageEmail = function(existing_user, new_email, cb) { +exports.stageEmail = function(existing_user, new_email, hash, cb) { secrets.generate(48, function(secret) { // overwrite previously staged users sync(); @@ -198,6 +199,7 @@ exports.stageEmail = function(existing_user, new_email, cb) { type: "add_email", existing_user: existing_user, email: new_email, + passwd: hash, when: (new Date()).getTime() }; db.stagedEmails[new_email] = secret; @@ -234,15 +236,26 @@ exports.emailForVerificationSecret = function(secret, cb) { process.nextTick(function() { sync(); if (!db.staged[secret]) return cb("no such secret"); + cb(null, db.staged[secret].email, db.staged[secret].existing_user); + }); +}; + +exports.authForVerificationSecret = function(secret, cb) { + process.nextTick(function() { + sync(); + if (!db.staged[secret]) return cb("no such secret"); + + if (db.staged[secret].passwd) { + return cb(null, db.staged[secret].passwd, db.staged[secret].existing_user); + } + exports.checkAuth(db.staged[secret].existing_user, function (err, hash) { - cb(err, { - email: db.staged[secret].email, - needs_password: !hash - }); + cb(err, hash, db.staged[secret].existing_user); }); }); }; + exports.verificationSecretForEmail = function(email, cb) { setTimeout(function() { sync(); @@ -250,7 +263,7 @@ exports.verificationSecretForEmail = function(email, cb) { }, 0); }; -exports.gotVerificationSecret = function(secret, hash, cb) { +exports.gotVerificationSecret = function(secret, cb) { sync(); if (!db.staged.hasOwnProperty(secret)) return cb("unknown secret"); @@ -265,6 +278,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { var emailVal = {}; emailVal[o.email] = { type: 'secondary' }; var uid = getNextUserID(); + var hash = o.passwd; db.users.push({ id: uid, password: hash, @@ -293,7 +307,17 @@ exports.gotVerificationSecret = function(secret, hash, cb) { exports.emailKnown(o.email, function(err, known) { function addIt() { addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) { - cb(e, o.email, o.existing_user); + var hash = o.passwd; + if(e || hash === null) return cb(e, o.email, o.existing_user); + + // a hash was specified, update the password for the user + exports.emailToUID(o.email, function(err, uid) { + if(err) return cb(err, o.email, o.existing_user); + + exports.updatePassword(uid, hash, function(err) { + cb(err || null, o.email, o.existing_user); + }); + }); }); } if (known) { diff --git a/lib/db/mysql.js b/lib/db/mysql.js index b984dcbf438fd4a6bcaf3cb2c26f0666b66b9228..5e5dcf98eb4b624678322afe74ab29a0a645bcba 100644 --- a/lib/db/mysql.js +++ b/lib/db/mysql.js @@ -24,6 +24,7 @@ * | bool new_acct | * | int existing_user | * |*string email | + * |*string passwd | * | timestamp ts | * +------------------------+ */ @@ -78,6 +79,7 @@ const schemas = [ "new_acct BOOL NOT NULL," + "existing_user BIGINT," + "email VARCHAR(255) UNIQUE NOT NULL," + + "passwd CHAR(64)," + "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL," + "FOREIGN KEY existing_user_fkey (existing_user) REFERENCES user(id)" + ") ENGINE=InnoDB;", @@ -240,12 +242,12 @@ exports.lastStaged = function(email, cb) { ); }; -exports.stageUser = function(email, cb) { +exports.stageUser = function(email, hash, cb) { secrets.generate(48, function(secret) { // overwrite previously staged users - client.query('INSERT INTO staged (secret, new_acct, email) VALUES(?,TRUE,?) ' + + client.query('INSERT INTO staged (secret, new_acct, email, passwd) VALUES(?,TRUE,?,?) ' + 'ON DUPLICATE KEY UPDATE secret=VALUES(secret), existing_user=NULL, new_acct=TRUE, ts=NOW()', - [ secret, email ], + [ secret, email, hash ], function(err) { cb(err, err ? undefined : secret); }); @@ -263,7 +265,20 @@ exports.haveVerificationSecret = function(secret, cb) { exports.emailForVerificationSecret = function(secret, cb) { client.query( - "SELECT * FROM staged WHERE secret = ?", [ secret ], + "SELECT email, existing_user FROM staged WHERE secret = ?", [ secret ], + function(err, rows) { + if (err) return cb("database unavailable"); + + // if the record was not found, fail out + if (!rows || rows.length != 1) return cb("no such secret"); + + cb(null, rows[0].email, rows[0].existing_user); + }); +}; + +exports.authForVerificationSecret = function(secret, cb) { + client.query( + "SELECT existing_user, passwd FROM staged WHERE secret = ?", [ secret ], function(err, rows) { if (err) return cb("database unavailable"); @@ -272,34 +287,15 @@ exports.emailForVerificationSecret = function(secret, cb) { var o = rows[0]; - // if the record was found and this is for a new_acct, return the email - if (o.new_acct) return cb(null, { email: o.email, needs_password: false }); - - // we need a userid. the old schema had an 'existing' field which was an email - // address. the new schema has an 'existing_user' field which is a userid. - // this is transitional code so outstanding verification links continue working - // and can be removed in feb 2012 some time. maybe for valentines day? - if (typeof o.existing_user === 'number') doCheckAuth(o.existing_user); - else if (typeof o.existing === 'string') { - exports.emailToUID(o.existing, function(err, uid) { - if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist'); - doCheckAuth(uid); - }); - } + // if there is a hashed passwd in the result, we're done + if (o.passwd) return cb(null, o.passwd, o.existing_user); - function doCheckAuth(uid) { - // if the account is being added to an existing account, let's find - // out if the account has a password set (if only primary email addresses - // are associated with the acct at the moment, then there will not be a - // password set and the user will need to set one with the addition of - // this addresss) - exports.checkAuth(uid, function(err, hash) { - cb(err, { - email: o.email, - needs_password: !hash - }); - }); - } + // otherwise, let's get the passwd from the user record + if (!o.existing_user) cb("no password for user"); + + exports.checkAuth(o.existing_user, function(err, hash) { + cb(err, hash, o.existing_user); + }); }); }; @@ -334,7 +330,7 @@ function addEmailToUser(userID, email, type, cb) { } -exports.gotVerificationSecret = function(secret, hash, cb) { +exports.gotVerificationSecret = function(secret, cb) { client.query( "SELECT * FROM staged WHERE secret = ?", [ secret ], function(err, rows) { @@ -349,6 +345,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]); if (o.new_acct) { + var hash = o.passwd; // we're creating a new account, add appropriate entries into user and email tables. client.query( "INSERT INTO user(passwd) VALUES(?)", @@ -362,17 +359,25 @@ exports.gotVerificationSecret = function(secret, hash, cb) { // address. the new schema has an 'existing_user' field which is a userid. // this is transitional code so outstanding verification links continue working // and can be removed in feb 2012 some time. maybe for valentines day? - if (typeof o.existing_user === 'number') doAddEmail(o.existing_user); + if (typeof o.existing_user === 'number') doAddEmailSetPassword(o.existing_user); else if (typeof o.existing === 'string') { exports.emailToUID(o.existing, function(uid) { if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist'); - doAddEmail(uid); + doAddEmailSetPassword(uid); }); } - function doAddEmail(uid) { + function doAddEmailSetPassword(uid) { // we're adding an email address to an existing user account. add appropriate entries into // email table - addEmailToUser(uid, o.email, 'secondary', cb); + var hash = o.passwd; + if (hash) { + exports.updatePassword(uid, hash, function(err) { + if (err) return cb('could not set user\'s password'); + addEmailToUser(uid, o.email, 'secondary', cb); + }); + } else { + addEmailToUser(uid, o.email, 'secondary', cb); + } } }; } @@ -420,12 +425,12 @@ exports.userOwnsEmail = function(uid, email, cb) { }); } -exports.stageEmail = function(existing_user, new_email, cb) { +exports.stageEmail = function(existing_user, new_email, hash, cb) { secrets.generate(48, function(secret) { // overwrite previously staged users - client.query('INSERT INTO staged (secret, new_acct, existing_user, email) VALUES(?,FALSE,?,?) ' + + client.query('INSERT INTO staged (secret, new_acct, existing_user, email, passwd) VALUES(?,FALSE,?,?,?) ' + 'ON DUPLICATE KEY UPDATE secret=VALUES(secret), existing_user=VALUES(existing_user), new_acct=FALSE, ts=NOW()', - [ secret, existing_user, new_email ], + [ secret, existing_user, new_email, hash ], function(err) { cb(err, err ? undefined : secret); }); diff --git a/lib/http_forward.js b/lib/http_forward.js index 5277aa95643a31659a9a45085902e5b31cc37065..d88cbd85de18c4f97f85fbb563fc0929dc05c30d 100644 --- a/lib/http_forward.js +++ b/lib/http_forward.js @@ -9,7 +9,21 @@ https = require('https'), logger = require('./logging.js').logger, querystring = require('querystring'); -module.exports = function(dest, req, res, cb) { +var global_forward_timeout = undefined; + +exports.setTimeout = function(to) { + if (typeof to != 'number') throw "setTimeout expects a numeric argument"; + global_forward_timeout = to; +}; + +exports.forward = function(dest, req, res, cb) { + var _cb = cb; + var requestTimeout = undefined; + cb = function() { + if (requestTimeout) clearTimeout(requestTimeout); + if (_cb) _cb.apply(null, arguments); + } + function cleanupReq() { if (preq) { preq.removeAllListeners(); @@ -56,6 +70,10 @@ module.exports = function(dest, req, res, cb) { cb(e); }); + if (global_forward_timeout) { + requestTimeout = setTimeout(function() { preq.destroy(); }, global_forward_timeout); + } + if (req.headers['content-type']) { preq.setHeader('Content-Type', req.headers['content-type']); } diff --git a/lib/httputils.js b/lib/httputils.js index 81e68334d52bcf82484bef7e0d086f8ab471e17f..cbc7a8c655f5fce4a18abd4a1bf0be9f1bee903f 100644 --- a/lib/httputils.js +++ b/lib/httputils.js @@ -28,6 +28,10 @@ exports.serviceUnavailable = function(resp, reason) { sendResponse(resp, "Service Unavailable", reason, 503); }; +exports.authRequired = function(resp, reason) { + sendResponse(resp, "Authentication Required", reason, 401); +}; + exports.badRequest = function(resp, reason) { sendResponse(resp, "Bad Request", reason, 400); }; diff --git a/lib/primary.js b/lib/primary.js index 55861ece63a6614d02107bf4b4927705d3031e83..5e7f481d43acbc617408affb594cadad418190d4 100644 --- a/lib/primary.js +++ b/lib/primary.js @@ -186,7 +186,7 @@ exports.checkSupport = function(domain, cb, delegates) { if (typeof domain !== 'string' || !domain.length) { return process.nextTick(function() { cb("invalid domain"); }); } - getWellKnown(domain, delegates, function (err, body, domain, delegates) { + getWellKnown(domain, delegates, function (err, body, domain, cbdelegates) { if (err) { logger.debug(err); return cb(err); @@ -196,7 +196,7 @@ exports.checkSupport = function(domain, cb, delegates) { } try { - var r = parseWellKnownBody(body, domain, delegates, function (err, r) { + var r = parseWellKnownBody(body, domain, cbdelegates, function (err, r) { if (err) { logger.debug(err); cb(err); @@ -226,6 +226,18 @@ exports.getPublicKey = function(domain, cb) { }); }; +// Does emailDomain actual delegate to the issuingDomain? +exports.delegatesAuthority = function (emailDomain, issuingDomain, cb) { + exports.checkSupport(emailDomain, function(err, urls, publicKey) { + // Check http or https://{issuingDomain}/some/sign_in_path + if (! err && urls && urls.auth && + urls.auth.indexOf('://' + issuingDomain + '/') !== -1) { + cb(true); + } + cb(false); + }); +} + // verify an assertion generated to authenticate to browserid exports.verifyAssertion = function(assertion, cb) { if (config.get('disable_primary_support')) { diff --git a/lib/static_resources.js b/lib/static_resources.js index 8f4d85c9947b850a366f82f39d8dc1e0a64cd5ac..7aade4d0e34c4c160c09985bd7d2470ba55a4ce7 100644 --- a/lib/static_resources.js +++ b/lib/static_resources.js @@ -62,8 +62,7 @@ var browserid_js = und.flatten([ '/pages/page_helpers.js', '/pages/index.js', '/pages/start.js', - '/pages/add_email_address.js', - '/pages/verify_email_address.js', + '/pages/verify_secondary_address.js', '/pages/forgot.js', '/pages/manage_account.js', '/pages/signin.js', @@ -91,7 +90,6 @@ var dialog_js = und.flatten([ '/dialog/controllers/actions.js', '/dialog/controllers/dialog.js', '/dialog/controllers/authenticate.js', - '/dialog/controllers/forgot_password.js', '/dialog/controllers/check_registration.js', '/dialog/controllers/pick_email.js', '/dialog/controllers/add_email.js', @@ -101,6 +99,7 @@ var dialog_js = und.flatten([ '/dialog/controllers/primary_user_provisioned.js', '/dialog/controllers/generate_assertion.js', '/dialog/controllers/is_this_your_computer.js', + '/dialog/controllers/set_password.js', '/dialog/start.js' ]]); diff --git a/lib/verifier/certassertion.js b/lib/verifier/certassertion.js index b437fe4f9abbc5ecbcb38b66ae71e21557ceda50..babd1eae4e55096bad33b0016b243c876ff2a521 100644 --- a/lib/verifier/certassertion.js +++ b/lib/verifier/certassertion.js @@ -134,20 +134,30 @@ function verify(assertion, audience, successCB, errorCB) { return errorCB("audience mismatch: " + err); } - // verify that the issuer is the same as the email domain - // NOTE: for "delegation of authority" support we'll need to make this check - // more sophisticated + var token_verify = function (tok, pk, principal, ultimateIssuer) { + if (tok.verify(pk)) { + return successCB(principal.email, tok.audience, tok.expires, ultimateIssuer); + } else { + return errorCB("verification failure"); + } + } + + // verify that the issuer is the same as the email domain or + // that the email's domain delegated authority to the issuer var domainFromEmail = principal.email.replace(/^.*@/, ''); + if (ultimateIssuer != HOSTNAME && ultimateIssuer !== domainFromEmail) { - return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '" - + domainFromEmail + "'"); - } - - if (tok.verify(pk)) { - successCB(principal.email, tok.audience, tok.expires, ultimateIssuer); + primary.delegatesAuthority(domainFromEmail, ultimateIssuer, function (delegated) { + if (delegated) { + return token_verify(tok, pk, principal, ultimateIssuer); + } else { + return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '" + + domainFromEmail + "'"); + } + }); } else { - errorCB("verification failure"); + return token_verify(tok, pk, principal, ultimateIssuer); } }, errorCB); }; diff --git a/lib/wsapi.js b/lib/wsapi.js index c76c7e3e23bf501492316c40eb4dcf1b3a4c4c0a..7b736425feb5113c86a8f8d9ca91fddfe3204e87 100644 --- a/lib/wsapi.js +++ b/lib/wsapi.js @@ -22,7 +22,7 @@ secrets = require('./secrets'), config = require('./configuration'), logger = require('./logging.js').logger, httputils = require('./httputils.js'), -forward = require('./http_forward.js'), +forward = require('./http_forward.js').forward, url = require('url'), fs = require('fs'), path = require('path'), diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js index 68284f964fd8f325adabbf53c91d3a75e4201b4b..778bd4291953da0cc9af22988a039bd80e67cab3 100644 --- a/lib/wsapi/address_info.js +++ b/lib/wsapi/address_info.js @@ -23,29 +23,29 @@ exports.i18n = false; const emailRegex = /\@(.*)$/; -exports.process = function(req, resp) { +exports.process = function(req, res) { // parse out the domain from the email var email = url.parse(req.url, true).query['email']; var m = emailRegex.exec(email); if (!m) { - return httputils.badRequest(resp, "invalid email address"); + return httputils.badRequest(res, "invalid email address"); } - primary.checkSupport(m[1], function(err, urls, publicKey) { + primary.checkSupport(m[1], function(err, urls, publicKey, delegates) { if (err) { logger.warn('error checking "' + m[1] + '" for primary support: ' + err); - return httputils.serverError(resp, "can't check email address"); + return httputils.serverError(res, "can't check email address"); } if (urls) { urls.type = 'primary'; - resp.json(urls); + res.json(urls); } else { db.emailKnown(email, function(err, known) { if (err) { - return wsapi.databaseDown(resp, err); + return wsapi.databaseDown(res, err); } else { - resp.json({ type: 'secondary', known: known }); + res.json({ type: 'secondary', known: known }); } }); } diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js index 777d61223b6732ab620c66325baa9975eac891f5..0f0c81ef3f8d7336db02f0ad90a6719fcc086957 100644 --- a/lib/wsapi/cert_key.js +++ b/lib/wsapi/cert_key.js @@ -6,7 +6,7 @@ const db = require('../db.js'), httputils = require('../httputils'), logger = require('../logging.js').logger, -forward = require('../http_forward.js'), +forward = require('../http_forward.js').forward, config = require('../configuration.js'), urlparse = require('urlparse'), wsapi = require('../wsapi.js'); diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js index 6e7dd2a4df4ce2116142fe5603d848ae43f9cccf..fcff7281387b49d78fb53a05ad4b74187dd36db2 100644 --- a/lib/wsapi/complete_email_addition.js +++ b/lib/wsapi/complete_email_addition.js @@ -5,11 +5,11 @@ const db = require('../db.js'), logger = require('../logging.js').logger, -wsapi = require('../wsapi.js'); +wsapi = require('../wsapi.js'), +brycpt = require('../bcrypt.js'); exports.method = 'post'; exports.writes_db = true; -// XXX: see issue #290 - we want to require authentication here and update frontend code exports.authed = false; // NOTE: this API also takes a 'pass' parameter which is required // when a user has a null password (only primaries on their acct) @@ -17,63 +17,44 @@ exports.args = ['token']; exports.i18n = false; exports.process = function(req, res) { - // a password *must* be supplied to this call iff the user's password - // is currently NULL - this would occur in the case where this is the - // first secondary address to be added to an account - db.emailForVerificationSecret(req.body.token, function(err, r) { - if (err === 'database unavailable') { + // in order to complete an email addition, one of the following must be true: + // + // 1. you must already be authenticated as the user who initiated the verification + // 2. you must provide the password of the initiator. + // + db.authForVerificationSecret(req.body.token, function(err, initiator_hash, initiator_uid) { + if (err) { + logger.info("unknown verification secret: " + err); return wsapi.databaseDown(res, err); } - if (!err && r.needs_password && !req.body.pass) { - err = "user must choose a password"; - } - if (!err && !r.needs_password && req.body.pass) { - err = "a password may not be set at this time"; - } - if (!err && r.needs_password) err = wsapi.checkPassword(req.body.pass); - - if (err) { - logger.info("addition of email fails: " + err); - return res.json({ - success: false, - reason: err + if (req.session.userid === initiator_uid) { + postAuthentication(); + } else if (typeof req.body.pass === 'string') { + bcrypt.compare(req.body.pass, initiator_hash, function (err, success) { + if (err) { + logger.warn("max load hit, failing on auth request with 503: " + err); + return httputils.serviceUnavailable(res, "server is too busy"); + } else if (!success) { + return httputils.authRequired(res, "password mismatch"); + } else { + postAuthentication(); + } }); + } else { + return httputils.authRequired(res, "password required"); } - // got verification secret's second paramter is a password. That password - // will only be used on new account creation. Because we know this is not - // a new account, we don't provide it. - db.gotVerificationSecret(req.body.token, "", function(e, email, uid) { - if (e) { - logger.warn("couldn't complete email verification: " + e); - wsapi.databaseDown(res, e); - } else { - // now do we need to set the password? - if (r.needs_password && req.body.pass) { - // requiring the client to wait until the bcrypt process is complete here - // exacerbates race conditions in front-end code. We'll return success early, - // here, then update the password after the fact. The worst thing that could - // happen is that password update could fail (due to extreme load), and the - // user will have to reset their password. - wsapi.authenticateSession(req.session, uid, 'password'); - res.json({ success: true }); - - wsapi.bcryptPassword(req.body.pass, function(err, hash) { - if (err) { - logger.warn("couldn't bcrypt password during email verification: " + err); - return; - } - db.updatePassword(uid, hash, function(err) { - if (err) { - logger.warn("couldn't update password during email verification: " + err); - } - }); - }); + function postAuthentication() { + db.gotVerificationSecret(req.body.token, function(e, email, uid) { + if (e) { + logger.warn("couldn't complete email verification: " + e); + wsapi.databaseDown(res, e); } else { + wsapi.authenticateSession(req.session, uid, 'password'); res.json({ success: true }); } - } - }); + }); + }; }); }; diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js index dca109d14da9a856d1ab7fc6792ec100a932a37c..e507e0f95e1396268a4f180100f00191ee469656 100644 --- a/lib/wsapi/complete_user_creation.js +++ b/lib/wsapi/complete_user_creation.js @@ -6,43 +6,66 @@ const db = require('../db.js'), wsapi = require('../wsapi.js'), httputils = require('../httputils'), -logger = require('../logging.js').logger; +logger = require('../logging.js').logger, +bcrypt = require('../bcrypt'); exports.method = 'post'; exports.writes_db = true; exports.authed = false; -exports.args = ['token','pass']; +exports.args = ['token']; exports.i18n = false; exports.process = function(req, res) { - // issue #155, valid password length is between 8 and 80 chars. - var err = wsapi.checkPassword(req.body.pass); - if (err) return httputils.badRequest(res, err); + // in order to complete a user creation, one of the following must be true: + // + // 1. you are using the same browser to complete the email verification as you + // used to start it + // 2. you have provided the password chosen by the initiator of the verification + // request + // + // These protections guard against the case where an attacker can send out a bunch + // of verification emails, wait until a distracted internet user clicks on one, + // and then control a browserid account that they can use to prove they own + // the email address of the attacked. - // at the time the email verification is performed, we'll clear the pendingCreation - // data on the session. - delete req.session.pendingCreation; - - // We should check to see if the verification secret is valid *before* - // bcrypting the password (which is expensive), to prevent a possible - // DoS attack. - db.haveVerificationSecret(req.body.token, function(err, known) { - if (err) return wsapi.databaseDown(res, err); - - if (!known) return res.json({ success: false} ); - - // now bcrypt the password - wsapi.bcryptPassword(req.body.pass, function (err, hash) { + // is this the same browser? + if (typeof req.session.pendingCreation === 'string' && + req.body.token === req.session.pendingCreation) { + postAuthentication(); + } + // is a password provided? + else if (typeof req.body.pass === 'string') { + return db.authForVerificationSecret(req.body.token, function(err, hash) { if (err) { - if (err.indexOf('exceeded') != -1) { + logger.warn("couldn't get password for verification secret: " + err); + return wsapi.databaseDown(res, err); + } + bcrypt.compare(req.body.pass, hash, function (err, success) { + if (err) { logger.warn("max load hit, failing on auth request with 503: " + err); - return httputils.serviceUnavailable("server is too busy"); + return httputils.serviceUnavailable(res, "server is too busy"); + } else if (!success) { + return httputils.authRequired(res, "password mismatch"); + } else { + postAuthentication(); } - logger.error("can't bcrypt: " + err); - return res.json({ success: false }); - } + }); + }); + } else { + return httputils.authRequired(res, 'Provide your password'); + } + + function postAuthentication() { + // the time the email verification is performed, we'll clear the pendingCreation + // data on the session. + delete req.session.pendingCreation; + + db.haveVerificationSecret(req.body.token, function(err, known) { + if (err) return wsapi.databaseDown(res, err); + + if (!known) return res.json({ success: false} ); - db.gotVerificationSecret(req.body.token, hash, function(err, email, uid) { + db.gotVerificationSecret(req.body.token, function(err, email, uid) { if (err) { logger.warn("couldn't complete email verification: " + err); wsapi.databaseDown(res, err); @@ -56,5 +79,5 @@ exports.process = function(req, res) { } }); }); - }); + } }; diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js index bfb122a747e8a514d1aca045fb3f14d7567fc6de..f492bcff595e0978c7f96e24f31b287fdceb8851 100644 --- a/lib/wsapi/email_for_token.js +++ b/lib/wsapi/email_for_token.js @@ -19,7 +19,7 @@ exports.args = ['token']; exports.i18n = false; exports.process = function(req, res) { - db.emailForVerificationSecret(req.query.token, function(err, r) { + db.emailForVerificationSecret(req.query.token, function(err, email, uid) { if (err) { if (err === 'database unavailable') { httputils.serviceUnavailable(res, err); @@ -30,10 +30,23 @@ exports.process = function(req, res) { }); } } else { + // must the user authenticate? This is true if they are not authenticated + // as the uid who initiated the verification, and they are not on the same + // browser as the initiator + var must_auth = true; + + if (uid && req.session.userid === uid) { + must_auth = false; + } + else if (!uid && typeof req.session.pendingCreation === 'string' && + req.query.token === req.session.pendingCreation) { + must_auth = false; + } + res.json({ success: true, - email: r.email, - needs_password: r.needs_password + email: email, + must_auth: must_auth }); } }); diff --git a/lib/wsapi/have_email.js b/lib/wsapi/have_email.js index ec832546bc261fa1970197a5eda4c0e7312afcd0..5caf1feffbafb6ba86ea17f40af4bd1d80435d89 100644 --- a/lib/wsapi/have_email.js +++ b/lib/wsapi/have_email.js @@ -14,10 +14,10 @@ exports.authed = false; exports.args = ['email']; exports.i18n = false; -exports.process = function(req, resp) { +exports.process = function(req, res) { var email = url.parse(req.url, true).query['email']; db.emailKnown(email, function(err, known) { - if (err) return wsapi.databaseDown(resp, err); - resp.json({ email_known: known }); + if (err) return wsapi.databaseDown(res, err); + res.json({ email_known: known }); }); }; diff --git a/lib/wsapi/list_emails.js b/lib/wsapi/list_emails.js index 6da607007c3b967b53ccc4b6d77834c705db4387..dc615dff7c06fc0af6ccc4e233c7153896f0d003 100644 --- a/lib/wsapi/list_emails.js +++ b/lib/wsapi/list_emails.js @@ -19,10 +19,10 @@ exports.writes_db = false; exports.authed = 'assertion'; exports.i18n = false; -exports.process = function(req, resp) { +exports.process = function(req, res) { logger.debug('listing emails for user ' + req.session.userid); db.listEmails(req.session.userid, function(err, emails) { - if (err) wsapi.databaseDown(resp, err); - else resp.json(emails); + if (err) wsapi.databaseDown(res, err); + else res.json(emails); }); }; diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js index 6ffe560eefd98102908f86946076a0643f99d083..60129ddcc72117f1c9801b262d29c5dde5dc7449 100644 --- a/lib/wsapi/stage_email.js +++ b/lib/wsapi/stage_email.js @@ -23,6 +23,10 @@ exports.args = ['email','site']; exports.i18n = true; exports.process = function(req, res) { + // a password *must* be supplied to this call iff the user's password + // is currently NULL - this would occur in the case where this is the + // first secondary address to be added to an account + // validate try { sanitize(req.body.email).isEmail(); @@ -30,7 +34,7 @@ exports.process = function(req, res) { } catch(e) { var msg = "invalid arguments: " + e; logger.warn("bad request received: " + msg); - return httputils.badRequest(resp, msg); + return httputils.badRequest(res, msg); } db.lastStaged(req.body.email, function (err, last) { @@ -42,23 +46,58 @@ exports.process = function(req, res) { return httputils.throttled(res, "Too many emails sent to that address, try again later."); } - try { - // on failure stageEmail may throw - db.stageEmail(req.session.userid, req.body.email, function(err, secret) { - if (err) return wsapi.databaseDown(res, err); + db.checkAuth(req.session.userid, function(err, hash) { + var needs_password = !hash; - var langContext = wsapi.langContext(req); + if (!err && needs_password && !req.body.pass) { + err = "user must choose a password"; + } + if (!err && !needs_password && req.body.pass) { + err = "a password may not be set at this time"; + } + if (!err && needs_password) err = wsapi.checkPassword(req.body.pass); - // store the email being added in session data - req.session.pendingAddition = secret; + if (err) { + logger.info("stage of email fails: " + err); + return res.json({ + success: false, + reason: err + }); + } - res.json({ success: true }); - // let's now kick out a verification email! - email.sendAddAddressEmail(req.body.email, req.body.site, secret, langContext); - }); - } catch(e) { - // we should differentiate tween' 400 and 500 here. - httputils.badRequest(res, e.toString()); - } + if (needs_password) { + wsapi.bcryptPassword(req.body.pass, function(err, hash) { + if (err) { + logger.warn("couldn't bcrypt password during email verification: " + err); + return res.json({ success: false }); + } + completeStage(hash); + }); + } + else { + completeStage(null); + } + + function completeStage(hash) { + try { + // on failure stageEmail may throw + db.stageEmail(req.session.userid, req.body.email, hash, function(err, secret) { + if (err) return wsapi.databaseDown(res, err); + + var langContext = wsapi.langContext(req); + + // store the email being added in session data + req.session.pendingAddition = secret; + + res.json({ success: true }); + // let's now kick out a verification email! + email.sendAddAddressEmail(req.body.email, req.body.site, secret, langContext); + }); + } catch(e) { + // we should differentiate tween' 400 and 500 here. + httputils.badRequest(res, e.toString()); + } + } + }); }); }; diff --git a/lib/wsapi/stage_user.js b/lib/wsapi/stage_user.js index 0408b7e76a53de41a380254feed56c9ae8282773..ff1dd24bf03c10cee69671ba8ef0b80786c56db3 100644 --- a/lib/wsapi/stage_user.js +++ b/lib/wsapi/stage_user.js @@ -19,56 +19,75 @@ sanitize = require('../sanitize'); exports.method = 'post'; exports.writes_db = true; exports.authed = false; -exports.args = ['email','site']; +exports.args = ['email','pass','site']; exports.i18n = true; -exports.process = function(req, resp) { +exports.process = function(req, res) { var langContext = wsapi.langContext(req); - // staging a user logs you out. - wsapi.clearAuthenticatedUser(req.session); - // validate try { sanitize(req.body.email).isEmail(); sanitize(req.body.site).isOrigin(); + if(!req.body.pass) throw "missing pass"; } catch(e) { var msg = "invalid arguments: " + e; logger.warn("bad request received: " + msg); - return httputils.badRequest(resp, msg); + return httputils.badRequest(res, msg); + } + + var err = wsapi.checkPassword(req.body.pass); + if (err) { + logger.warn("invalid password received: " + err); + return httputils.badRequest(res, err); } db.lastStaged(req.body.email, function (err, last) { - if (err) return wsapi.databaseDown(resp, err); + if (err) return wsapi.databaseDown(res, err); if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) { logger.warn('throttling request to stage email address ' + req.body.email + ', only ' + ((new Date() - last) / 1000.0) + "s elapsed"); - return httputils.throttled(resp, "Too many emails sent to that address, try again later."); + return httputils.throttled(res, "Too many emails sent to that address, try again later."); } - try { - // upon success, stage_user returns a secret (that'll get baked into a url - // and given to the user), on failure it throws - db.stageUser(req.body.email, function(err, secret) { - if (err) return wsapi.databaseDown(resp, err); + // staging a user logs you out. + wsapi.clearAuthenticatedUser(req.session); - // store the email being registered in the session data - if (!req.session) req.session = {}; + // now bcrypt the password + wsapi.bcryptPassword(req.body.pass, function (err, hash) { + if (err) { + if (err.indexOf('exceeded') != -1) { + logger.warn("max load hit, failing on auth request with 503: " + err); + return httputils.serviceUnavailable("server is too busy"); + } + logger.error("can't bcrypt: " + err); + return res.json({ success: false }); + } - // store the secret we're sending via email in the users session, as checking - // that it still exists in the database is the surest way to determine the - // status of the email verification. - req.session.pendingCreation = secret; + try { + // upon success, stage_user returns a secret (that'll get baked into a url + // and given to the user), on failure it throws + db.stageUser(req.body.email, hash, function(err, secret) { + if (err) return wsapi.databaseDown(res, err); - resp.json({ success: true }); + // store the email being registered in the session data + if (!req.session) req.session = {}; - // let's now kick out a verification email! - email.sendNewUserEmail(req.body.email, req.body.site, secret, langContext); - }); - } catch(e) { - // we should differentiate tween' 400 and 500 here. - httputils.badRequest(resp, e.toString()); - } + // store the secret we're sending via email in the users session, as checking + // that it still exists in the database is the surest way to determine the + // status of the email verification. + req.session.pendingCreation = secret; + + res.json({ success: true }); + + // let's now kick out a verification email! + email.sendNewUserEmail(req.body.email, req.body.site, secret, langContext); + }); + } catch(e) { + // we should differentiate tween' 400 and 500 here. + httputils.badRequest(res, e.toString()); + } + }); }); }; diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 00b561a12fae1d6333e302dbd391c089012d09ed..b41a984313fe3241ae9b4deff28fc96219ea7932 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -667,7 +667,7 @@ h1 { margin-bottom: 10px; } -.siteinfo, #congrats, #signUpForm > .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit { +.siteinfo, #congrats, .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit { display: none; } @@ -675,7 +675,7 @@ h1 { float: left; } -.enter_password #signUpForm > .password_entry, .known_secondary #signUpForm > .password_entry, +.enter_password .password_entry, .known_secondary .password_entry, .unknown_secondary #unknown_secondary, .verify_primary #verify_primary { display: block; } @@ -820,3 +820,4 @@ footer a:hover { .newsbanner a:hover { color: #000; } + diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index 08239b6befc50e5b56547823020dd07a5b7ef5a1..322749710534ba216c7ef8fd509c647e84260256 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -31,12 +31,13 @@ BrowserID.Modules.Actions = (function() { return module; } - function startRegCheckService(options, verifier, message) { + function startRegCheckService(options, verifier, message, password) { var controller = startService("check_registration", { email: options.email, required: options.required, verifier: verifier, - verificationMessage: message + verificationMessage: message, + password: password }); controller.startCheck(); } @@ -73,8 +74,16 @@ BrowserID.Modules.Actions = (function() { if(onsuccess) onsuccess(null); }, + doSetPassword: function(info) { + startService("set_password", info); + }, + + doStageUser: function(info) { + dialogHelpers.createUser.call(this, info.email, info.password, info.ready); + }, + doConfirmUser: function(info) { - startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed"); + startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed", info.password || undefined); }, doPickEmail: function(info) { @@ -85,6 +94,10 @@ BrowserID.Modules.Actions = (function() { startService("add_email", info); }, + doStageEmail: function(info) { + dialogHelpers.addSecondaryEmailWithPassword.call(this, info.email, info.password, info.ready); + }, + doAuthenticate: function(info) { startService("authenticate", info); }, @@ -94,11 +107,11 @@ BrowserID.Modules.Actions = (function() { }, doForgotPassword: function(info) { - startService("forgot_password", info); + startService("set_password", _.extend(info, { password_reset: true })); }, doResetPassword: function(info) { - this.doConfirmUser(info); + dialogHelpers.resetPassword.call(this, info.email, info.password, info.ready); }, doConfirmEmail: function(info) { diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js index a842fecc0bc196695f8f690c90bc65903c387676..d81fc9db32d86699b65ac10569baa17a3d070321 100644 --- a/resources/static/dialog/controllers/authenticate.js +++ b/resources/static/dialog/controllers/authenticate.js @@ -18,7 +18,7 @@ BrowserID.Modules.Authenticate = (function() { dom = bid.DOM, lastEmail = "", addressInfo, - hints = ["newuser","returning","start","addressInfo"]; + hints = ["returning","start","addressInfo"]; function getEmail() { return helpers.getAndValidateEmail("#email"); @@ -61,7 +61,7 @@ BrowserID.Modules.Authenticate = (function() { else if(info.known) { enterPasswordState.call(self); } else { - createSecondaryUserState.call(self); + createSecondaryUser.call(self); } } } @@ -71,9 +71,9 @@ BrowserID.Modules.Authenticate = (function() { email = getEmail(); if (email) { - dialogHelpers.createUser.call(self, email, callback); + self.close("new_user", { email: email }, { email: email }); } else { - callback && callback(); + complete(callback); } } @@ -129,15 +129,6 @@ BrowserID.Modules.Authenticate = (function() { } } - function createSecondaryUserState() { - var self=this; - - self.publish("create_user"); - self.submit = createSecondaryUser; - showHint("newuser"); - } - - function emailKeyUp() { var newEmail = dom.getInner("#email"); if (newEmail !== lastEmail) { @@ -161,7 +152,7 @@ BrowserID.Modules.Authenticate = (function() { tos_url: options.tosURL }); - $(".newuser,.returning,.start").hide(); + $(".returning,.start").hide(); self.bind("#email", "keyup", emailKeyUp); self.click("#forgotPassword", forgotPassword); diff --git a/resources/static/dialog/controllers/check_registration.js b/resources/static/dialog/controllers/check_registration.js index 3389f3f9e0ca7b47bb22e796e3c8135c91af9ce0..efd38e246b6c7ee4c19515f46c5720679949a6c9 100644 --- a/resources/static/dialog/controllers/check_registration.js +++ b/resources/static/dialog/controllers/check_registration.js @@ -23,6 +23,7 @@ BrowserID.Modules.CheckRegistration = (function() { self.verifier = options.verifier; self.verificationMessage = options.verificationMessage; self.required = options.required; + self.password = options.password; self.click("#back", self.back); @@ -40,9 +41,26 @@ BrowserID.Modules.CheckRegistration = (function() { }); } else if (status === "mustAuth") { - user.addressInfo(self.email, function(info) { - self.close("authenticate", info); - }); + // if we have a password (because it was just chosen in dialog), + // then we can authenticate the user and proceed + if (self.password) { + user.authenticate(self.email, self.password, function (authenticated) { + if (authenticated) { + user.syncEmails(function() { + self.close(self.verificationMessage); + oncomplete && oncomplete(); + }); + } else { + user.addressInfo(self.email, function(info) { + self.close("authenticate", info); + }); + } + }); + } else { + user.addressInfo(self.email, function(info) { + self.close("authenticate", info); + }); + } oncomplete && oncomplete(); } diff --git a/resources/static/dialog/controllers/forgot_password.js b/resources/static/dialog/controllers/forgot_password.js deleted file mode 100644 index 268f72417b625a66a07ade614a8a1b8d26ceb8e2..0000000000000000000000000000000000000000 --- a/resources/static/dialog/controllers/forgot_password.js +++ /dev/null @@ -1,49 +0,0 @@ -/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */ -/*global BrowserID:true, PageController: true */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -BrowserID.Modules.ForgotPassword = (function() { - "use strict"; - - var ANIMATION_TIME = 250, - bid = BrowserID, - helpers = bid.Helpers, - dialogHelpers = helpers.Dialog, - dom = bid.DOM; - - function resetPassword() { - var self=this; - dialogHelpers.resetPassword.call(self, self.email); - } - - function cancelResetPassword() { - this.close("cancel_state", { email: this.email }); - } - - var Module = bid.Modules.PageModule.extend({ - start: function(options) { - var self=this; - self.email = options.email; - self.renderDialog("forgot_password", { - email: options.email || "", - requiredEmail: options.requiredEmail - }); - - self.click("#cancel", cancelResetPassword); - - Module.sc.start.call(self, options); - }, - - submit: resetPassword - - // BEGIN TESTING API - , - resetPassword: resetPassword, - cancelResetPassword: cancelResetPassword - // END TESTING API - }); - - return Module; - -}()); diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js index b57933026038d0c3e1c9508d579016fda9d53538..61e65c469480a0f29a03415f979895d3506b5abf 100644 --- a/resources/static/dialog/controllers/pick_email.js +++ b/resources/static/dialog/controllers/pick_email.js @@ -27,14 +27,14 @@ BrowserID.Modules.PickEmail = (function() { } function addEmail() { - this.close("add_email"); + this.publish("add_email"); } function checkEmail(email) { var identity = user.getStoredEmailKeypair(email); if (!identity) { alert(gettext("The selected email is invalid or has been deleted.")); - this.close("assertion_generated", { + this.publish("assertion_generated", { assertion: null }); } @@ -76,6 +76,10 @@ BrowserID.Modules.PickEmail = (function() { } } + function notMe() { + this.publish("notme"); + } + var Module = bid.Modules.PageModule.extend({ start: function(options) { var origin = user.getOrigin(), @@ -104,6 +108,7 @@ BrowserID.Modules.PickEmail = (function() { // is needed for the label handler so that the correct radio button is // selected. self.bind("#selectEmail label", "click", proxyEventToInput); + self.click("#thisIsNotMe", notMe); sc.start.call(self, options); @@ -118,7 +123,8 @@ BrowserID.Modules.PickEmail = (function() { // BEGIN TESTING API , signIn: signIn, - addEmail: addEmail + addEmail: addEmail, + notMe: notMe // END TESTING API }); diff --git a/resources/static/dialog/controllers/set_password.js b/resources/static/dialog/controllers/set_password.js new file mode 100644 index 0000000000000000000000000000000000000000..308cee3288d858b05a047fbe39697d4ddc766049 --- /dev/null +++ b/resources/static/dialog/controllers/set_password.js @@ -0,0 +1,53 @@ +/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */ +/*global _: true, BrowserID: true, PageController: true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +BrowserID.Modules.SetPassword = (function() { + "use strict"; + var bid = BrowserID, + dom = bid.DOM, + helpers = bid.Helpers, + complete = helpers.complete, + dialogHelpers = helpers.Dialog, + sc; + + function submit(callback) { + var pass = dom.getInner("#password"), + vpass = dom.getInner("#vpassword"), + options = this.options; + + var valid = bid.Validation.passwordAndValidationPassword(pass, vpass); + if(valid) { + this.publish("password_set", { password: pass }); + } + + complete(callback, valid); + } + + function cancel() { + this.close("cancel_state"); + } + + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + var self=this; + options = options || {}; + + self.renderDialog("set_password", { + password_reset: !!options.password_reset + }); + + self.click("#cancel", cancel); + + sc.start.call(self, options); + }, + + submit: submit, + cancel: cancel + }); + + sc = Module.sc; + + return Module; +}()); diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js index 12029173c581474065a9f0cf4a895003673930cf..e95f7388625b933ccd340778e6c4fcbcd416d709 100644 --- a/resources/static/dialog/resources/helpers.js +++ b/resources/static/dialog/resources/helpers.js @@ -39,7 +39,7 @@ user.getAssertion(email, user.getOrigin(), function(assert) { assert = assert || null; wait.hide(); - self.close("assertion_generated", { + self.publish("assertion_generated", { assertion: assert }); @@ -58,28 +58,28 @@ }, self.getErrorDialog(errors.authenticate, callback)); } - function createUser(email, callback) { + function createUser(email, password, callback) { var self=this; - user.createSecondaryUser(email, function(status) { + user.createSecondaryUser(email, password, function(status) { if (status) { - var info = { email: email }; - self.close("user_staged", info, info); + var info = { email: email, password: password }; + self.publish("user_staged", info, info); complete(callback, true); } else { + // XXX will this tooltip ever be shown, the authentication screen has + // already been torn down by this point? tooltip.showTooltip("#could_not_add"); complete(callback, false); } }, self.getErrorDialog(errors.createUser, callback)); } - function resetPassword(email, callback) { + function resetPassword(email, password, callback) { var self=this; - user.requestPasswordReset(email, function(status) { + user.requestPasswordReset(email, password, function(status) { if (status.success) { - self.close("reset_password", { - email: email - }); + self.publish("password_reset", { email: email }); } else { tooltip.showTooltip("#could_not_add"); @@ -100,25 +100,32 @@ user.addressInfo(email, function(info) { if (info.type === "primary") { var info = _.extend(info, { email: email, add: true }); - self.close("primary_user", info, info); + self.publish("primary_user", info, info); complete(callback, true); } else { - user.addEmail(email, function(added) { - if (added) { - var info = { email: email }; - self.close("email_staged", info, info ); - } - else { - tooltip.showTooltip("#could_not_add"); - } - complete(callback, added); - }, self.getErrorDialog(errors.addEmail, callback)); + self.publish("add_email_submit_with_secondary", { email: email }); + complete(callback, true); } }, self.getErrorDialog(errors.addressInfo, callback)); } } + function addSecondaryEmailWithPassword(email, password, callback) { + var self=this; + + user.addEmail(email, password, function(added) { + if (added) { + var info = { email: email }; + self.publish("email_staged", info, info ); + } + else { + tooltip.showTooltip("#could_not_add"); + } + complete(callback, added); + }, self.getErrorDialog(errors.addEmail, callback)); + } + helpers.Dialog = helpers.Dialog || {}; helpers.extend(helpers.Dialog, { @@ -126,6 +133,7 @@ authenticateUser: authenticateUser, createUser: createUser, addEmail: addEmail, + addSecondaryEmailWithPassword: addSecondaryEmailWithPassword, resetPassword: resetPassword, cancelEvent: helpers.cancelEvent, animateClose: animateClose diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js index a96d5b8881da56ca8efda4538b355de817b3cf7b..288dd0e0864e3a5668cb994c21ad197b31154542 100644 --- a/resources/static/dialog/resources/state.js +++ b/resources/static/dialog/resources/state.js @@ -20,7 +20,13 @@ BrowserID.State = (function() { function startStateMachine() { var self = this, - handleState = self.subscribe.bind(self), + handleState = function(msg, callback) { + self.subscribe(msg, function(msg, info) { + // This level of indirection is to ensure an info object is + // always present in the handler. + callback(msg, info || {}); + }); + }, redirectToState = mediator.publish.bind(mediator), startAction = function(save, msg, options) { if (typeof save !== "boolean") { @@ -35,8 +41,6 @@ BrowserID.State = (function() { cancelState = self.popState.bind(self); handleState("start", function(msg, info) { - info = info || {}; - self.hostname = info.hostname; self.privacyURL = info.privacyURL; self.tosURL = info.tosURL; @@ -87,12 +91,39 @@ BrowserID.State = (function() { }); handleState("authenticate", function(msg, info) { - info = info || {}; info.privacyURL = self.privacyURL; info.tosURL = self.tosURL; startAction("doAuthenticate", info); }); + handleState("new_user", function(msg, info) { + self.newUserEmail = info.email; + startAction(false, "doSetPassword", info); + }); + + handleState("password_set", function(msg, info) { + /* A password can be set for one of three reasons - 1) This is a new user + * or 2) a user is adding the first secondary address to an account that + * consists only of primary addresses, or 3) an existing user has + * forgotten their password and wants to reset it. #1 is taken care of + * by newUserEmail, #2 by addEmailEmail, #3 by resetPasswordEmail. + */ + info = _.extend({ email: self.newUserEmail || self.addEmailEmail || self.resetPasswordEmail }, info); + + if(self.newUserEmail) { + self.newUserEmail = null; + startAction(false, "doStageUser", info); + } + else if(self.addEmailEmail) { + self.addEmailEmail = null; + startAction(false, "doStageEmail", info); + } + else if(self.resetPasswordEmail) { + self.resetPasswordEmail = null; + startAction(false, "doResetPassword", info); + } + }); + handleState("user_staged", function(msg, info) { self.stagedEmail = info.email; info.required = !!requiredEmail; @@ -121,7 +152,6 @@ BrowserID.State = (function() { }); handleState("primary_user_provisioned", function(msg, info) { - info = info || {}; info.add = !!addPrimaryUser; // The user is is authenticated with their IdP. Two possibilities exist // for the email - 1) create a new account or 2) add address to the @@ -131,7 +161,7 @@ BrowserID.State = (function() { }); handleState("primary_user_unauthenticated", function(msg, info) { - info = helpers.extend(info || {}, { + info = helpers.extend(info, { add: !!addPrimaryUser, email: email, requiredEmail: !!requiredEmail, @@ -179,8 +209,6 @@ BrowserID.State = (function() { }); handleState("email_chosen", function(msg, info) { - info = info || {}; - var email = info.email, idInfo = storage.getEmail(email); @@ -271,6 +299,25 @@ BrowserID.State = (function() { startAction("doGenerateAssertion", info); }); + handleState("forgot_password", function(msg, info) { + // User has forgotten their password, let them reset it. The response + // message from the forgot_password controller will be a set_password. + // the set_password handler needs to know the forgotPassword email so it + // knows how to handle the password being set. When the password is + // finally reset, the password_reset message will be raised where we must + // await email confirmation. + self.resetPasswordEmail = info.email; + startAction(false, "doForgotPassword", info); + }); + + handleState("password_reset", function(msg, info) { + // password_reset says the user has confirmed that they want to + // reset their password. doResetPassword will attempt to invoke + // the create_user wsapi. If the wsapi call is successful, + // the user will be shown the "go verify your account" message. + redirectToState("user_staged", info); + }); + handleState("assertion_generated", function(msg, info) { self.success = true; if (info.assertion !== null) { @@ -295,26 +342,8 @@ BrowserID.State = (function() { redirectToState("email_chosen", info); }); - handleState("forgot_password", function(msg, info) { - // forgot password initiates the forgotten password flow. - startAction(false, "doForgotPassword", info); - }); - - handleState("reset_password", function(msg, info) { - info = info || {}; - // reset_password says the user has confirmed that they want to - // reset their password. doResetPassword will attempt to invoke - // the create_user wsapi. If the wsapi call is successful, - // the user will be shown the "go verify your account" message. - - // We have to save the staged email address here for when the user - // verifies their account and user_confirmed is called. - self.stagedEmail = info.email; - startAction(false, "doResetPassword", info); - }); - handleState("add_email", function(msg, info) { - info = helpers.extend(info || {}, { + info = helpers.extend(info, { privacyURL: self.privacyURL, tosURL: self.tosURL }); @@ -322,6 +351,18 @@ BrowserID.State = (function() { startAction("doAddEmail", info); }); + handleState("add_email_submit_with_secondary", function(msg, info) { + user.passwordNeededToAddSecondaryEmail(function(passwordNeeded) { + if(passwordNeeded) { + self.addEmailEmail = info.email; + startAction(false, "doSetPassword", info); + } + else { + startAction(false, "doStageEmail", info); + } + }); + }); + handleState("email_staged", function(msg, info) { self.stagedEmail = info.email; info.required = !!requiredEmail; @@ -329,7 +370,7 @@ BrowserID.State = (function() { }); handleState("email_confirmed", function() { - redirectToState("email_chosen", { email: self.stagedEmail} ); + redirectToState("email_chosen", { email: self.stagedEmail } ); }); handleState("cancel_state", function(msg, info) { diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js index 1d0655877c632dfd28b18d2fe025877c464dca4f..e0e64b71daab4c1387490ac21dcdab39d9e5690e 100644 --- a/resources/static/dialog/start.js +++ b/resources/static/dialog/start.js @@ -29,7 +29,6 @@ moduleManager.register("add_email", modules.AddEmail); moduleManager.register("authenticate", modules.Authenticate); moduleManager.register("check_registration", modules.CheckRegistration); - moduleManager.register("forgot_password", modules.ForgotPassword); moduleManager.register("is_this_your_computer", modules.IsThisYourComputer); moduleManager.register("pick_email", modules.PickEmail); moduleManager.register("required_email", modules.RequiredEmail); @@ -39,6 +38,7 @@ moduleManager.register("generate_assertion", modules.GenerateAssertion); moduleManager.register("xhr_delay", modules.XHRDelay); moduleManager.register("xhr_disable_form", modules.XHRDisableForm); + moduleManager.register("set_password", modules.SetPassword); moduleManager.start("xhr_delay"); moduleManager.start("xhr_disable_form"); diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs index 4db5074ac61beab2efad4ee8b0e1190efe6ac3b5..2e72d63e08feaf153df142917f6e96c361f43b91 100644 --- a/resources/static/dialog/views/authenticate.ejs +++ b/resources/static/dialog/views/authenticate.ejs @@ -35,11 +35,6 @@ <%= gettext("Please hold on while we get information about your email provider.") %> </li> - <li id="create_text_section" class="newuser"> - <strong><%= gettext('Welcome to BrowserID!') %></strong> - <p><%= gettext("This email looks new, so let's get you set up.") %></p> - </li> - <li class="returning"> <a id="forgotPassword" class="forgot right" href="#" tabindex="4"><%= gettext('forgot your password?') %></a> @@ -71,7 +66,6 @@ <p> <% } %> <button class="start addressInfo" tabindex="3"><%= gettext('next') %></button> - <button class="newuser" tabindex="3"><%= gettext('verify email') %></button> <button class="returning" tabindex="3"><%= gettext('sign in') %></button> <% if (privacy_url && tos_url) { %> </p> diff --git a/resources/static/dialog/views/forgot_password.ejs b/resources/static/dialog/views/forgot_password.ejs deleted file mode 100644 index 3bbd7cd8ef495f38bd4fff59075961c97b59a5db..0000000000000000000000000000000000000000 --- a/resources/static/dialog/views/forgot_password.ejs +++ /dev/null @@ -1,30 +0,0 @@ -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - <strong> - <% if (requiredEmail) { %> - <%= gettext('The site requested you sign in using') %> - <% } else { %> - <%= gettext('Sign in using') %> - <% } %> - </strong> - - <div class="form_section"> - <ul class="inputs"> - <li> - <label for="email" class="serif"><%= gettext('Email') %></label> - <input id="email" class="sans" type="email" value="<%= email %>" disabled /> - - <div id="could_not_add" class="tooltip" for="email"> - <%= gettext('We just sent an email to that address! If you really want to send another, wait a minute or two and try again.') %> - </div> - </li> - - </ul> - - <div class="submit cf"> - <button tabindex="1"><%= gettext('reset password') %></button> - <a href="#" id="cancel" class="action" tabindex="2"><%= gettext('cancel') %></a> - </div> - </div> diff --git a/resources/static/dialog/views/set_password.ejs b/resources/static/dialog/views/set_password.ejs new file mode 100644 index 0000000000000000000000000000000000000000..a07b5d8737d5414e58b677313632c4b77cfc1e40 --- /dev/null +++ b/resources/static/dialog/views/set_password.ejs @@ -0,0 +1,54 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + <strong><%= gettext('Welcome to BrowserID!') %></strong> + + <div class="form_section" id="set_password"> + <ul class="inputs"> + <% if(!password_reset) { %> + <li> + <%= gettext("Next, choose a new password you'll use when you sign in with BrowserID.") %> + </li> + <% } %> + + + <li> + <label for="password" class="serif"><%= gettext('Password') %></label> + <input id="password" class="sans" type="password" maxlength="80" /> + + <div class="tooltip" id="password_required" for="password"> + <%= gettext('Password is required.') %> + </div> + + <div class="tooltip" id="password_length" for="password"> + <%= gettext('Password must be between 8 and 80 characters long.') %> + </div> + + <div id="could_not_add" class="tooltip" for="password"> + <%= gettext('We just sent an email to that address! If you really want to send another, wait a minute or two and try again.') %> + </div> + </li> + + <li> + <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label> + <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength=80 /> + + <div class="tooltip" id="vpassword_required" for="vpassword"> + <%= gettext('Verification password is required.') %> + </div> + + <div class="tooltip" id="passwords_no_match" for="vpassword"> + <%= gettext('Passwords do not match.') %> + </div> + </li> + + </ul> + + <div class="submit cf"> + <button tabindex="1" id="<%= password_reset ? "password_reset" : "verify_user" %>"> + <%= password_reset ? gettext('reset password') : gettext('verify email') %> + </button> + <a id="cancel" class="action" href="#"><%= gettext('cancel') %></a> + </div> + </div> diff --git a/resources/static/lib/dom-jquery.js b/resources/static/lib/dom-jquery.js index 860c033277fa2579044a22216426b7ac88bbd8eb..6438fec068304658384f58d1e86e2c0c1c309dcb 100644 --- a/resources/static/lib/dom-jquery.js +++ b/resources/static/lib/dom-jquery.js @@ -310,6 +310,24 @@ BrowserID.DOM = ( function() { */ is: function( elementToCheck, type ) { return jQuery( elementToCheck ).is( type ); + }, + + /** + * Show an element/elements + * @method show + * @param {selector || element} elementToShow + */ + show: function( elementToShow ) { + return jQuery( elementToShow ).show(); + }, + + /** + * Hide an element/elements + * @method hide + * @param {selector || element} elementToHide + */ + hide: function( elementToHide ) { + return jQuery( elementToHide ).hide(); } diff --git a/resources/static/pages/add_email_address.js b/resources/static/pages/add_email_address.js deleted file mode 100644 index 1a001d048122761bff9bdfdc6e130ff3be76dc8f..0000000000000000000000000000000000000000 --- a/resources/static/pages/add_email_address.js +++ /dev/null @@ -1,125 +0,0 @@ -/*globals BrowserID: true, $:true */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -BrowserID.addEmailAddress = (function() { - "use strict"; - - var ANIMATION_TIME=250, - bid = BrowserID, - user = bid.User, - storage = bid.Storage, - errors = bid.Errors, - pageHelpers = bid.PageHelpers, - dom = bid.DOM, - token, - sc; - - function showError(el, oncomplete) { - $(".hint,#signUpForm").hide(); - $(el).fadeIn(ANIMATION_TIME, oncomplete); - } - - function emailRegistrationComplete(oncomplete, info) { - function complete(status) { - oncomplete && oncomplete(status); - } - - var valid = info.valid; - if (valid) { - emailRegistrationSuccess(info, complete.curry(true)); - } - else { - showError("#cannotconfirm", complete.curry(false)); - } - } - - function showRegistrationInfo(info) { - dom.setInner(".email", info.email); - - if (info.origin) { - dom.setInner(".website", info.origin); - $(".siteinfo").show(); - } - } - - function emailRegistrationSuccess(info, oncomplete) { - dom.addClass("body", "complete"); - - showRegistrationInfo(info); - - setTimeout(function() { - pageHelpers.replaceFormWithNotice("#congrats", oncomplete); - }, 2000); - } - - function userMustEnterPassword(info) { - return !!info.needs_password; - } - - function verifyWithoutPassword(oncomplete) { - user.verifyEmailNoPassword(token, - emailRegistrationComplete.curry(oncomplete), - pageHelpers.getFailure(errors.verifyEmail, oncomplete) - ); - } - - function verifyWithPassword(oncomplete) { - var pass = dom.getInner("#password"), - vpass = dom.getInner("#vpassword"), - valid = bid.Validation.passwordAndValidationPassword(pass, vpass); - - if(valid) { - user.verifyEmailWithPassword(token, pass, - emailRegistrationComplete.curry(oncomplete), - pageHelpers.getFailure(errors.verifyEmail, oncomplete) - ); - } - else { - oncomplete && oncomplete(false); - } - } - - function startVerification(oncomplete) { - user.tokenInfo(token, function(info) { - if(info) { - showRegistrationInfo(info); - - if(userMustEnterPassword(info)) { - dom.addClass("body", "enter_password"); - oncomplete(true); - } - else { - verifyWithoutPassword(oncomplete); - } - } - else { - showError("#cannotconfirm"); - oncomplete(false); - } - }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete)); - } - - var Module = bid.Modules.PageModule.extend({ - start: function(options) { - function oncomplete(status) { - options.ready && options.ready(status); - } - - this.checkRequired(options, "token"); - - token = options.token; - - startVerification(oncomplete); - - sc.start.call(this, options); - }, - - submit: verifyWithPassword - }); - - sc = Module.sc; - - return Module; -}()); diff --git a/resources/static/pages/forgot.js b/resources/static/pages/forgot.js index 529c066fbd866f84e7c3d2f6e4d6e44f476dec58..5d7c9385355f1dbc1eca1ff0bf85b826b867997e 100644 --- a/resources/static/pages/forgot.js +++ b/resources/static/pages/forgot.js @@ -9,6 +9,8 @@ BrowserID.forgot = (function() { var bid = BrowserID, user = bid.User, helpers = bid.Helpers, + complete = helpers.complete, + validation = bid.Validation, pageHelpers = bid.PageHelpers, cancelEvent = pageHelpers.cancelEvent, dom = bid.DOM, @@ -18,20 +20,24 @@ BrowserID.forgot = (function() { // GET RID OF THIS HIDE CRAP AND USE CSS! $(".notifications .notification").hide(); - var email = helpers.getAndValidateEmail("#email"); + var email = helpers.getAndValidateEmail("#email"), + pass = dom.getInner("#password"), + vpass = dom.getInner("#vpassword"), + validPass = email && validation.passwordAndValidationPassword(pass, vpass); - if (email) { - user.requestPasswordReset(email, function onSuccess(info) { + if (email && validPass) { + user.requestPasswordReset(email, pass, function onSuccess(info) { if (info.success) { pageHelpers.emailSent(oncomplete); } else { var tooltipEl = info.reason === "throttle" ? "#could_not_add" : "#not_registered"; - tooltip.showTooltip(tooltipEl, oncomplete); + tooltip.showTooltip(tooltipEl); + complete(oncomplete); } }, pageHelpers.getFailure(bid.Errors.requestPasswordReset, oncomplete)); } else { - oncomplete && oncomplete(); + complete(oncomplete); } }; diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js index 475d53824fef0a7c8366995939d6520720d8468e..3fef51badbfe646ad8f2d9ec523afff178dfd896 100644 --- a/resources/static/pages/signup.js +++ b/resources/static/pages/signup.js @@ -12,63 +12,48 @@ BrowserID.signUp = (function() { helpers = bid.Helpers, pageHelpers = bid.PageHelpers, cancelEvent = pageHelpers.cancelEvent, + validation = bid.Validation, errors = bid.Errors, tooltip = BrowserID.Tooltip, ANIMATION_SPEED = 250, storedEmail = pageHelpers, winchan = window.WinChan, - verifyEmail, - verifyURL; + primaryUserInfo, + sc; function showNotice(selector) { $(selector).fadeIn(ANIMATION_SPEED); } function authWithPrimary(oncomplete) { - pageHelpers.openPrimaryAuth(winchan, verifyEmail, verifyURL, primaryAuthComplete); + pageHelpers.openPrimaryAuth(winchan, primaryUserInfo.email, primaryUserInfo.auth, primaryAuthComplete); oncomplete && oncomplete(); } function primaryAuthComplete(error, result, oncomplete) { - if(error) { + if (error) { pageHelpers.showFailure(errors.primaryAuthentication, error, oncomplete); } else { // hey ho, the user is authenticated, re-try the submit. - createUser(verifyEmail, oncomplete); + createPrimaryUser(primaryUserInfo, oncomplete); } } - function createUser(email, oncomplete) { + function createPrimaryUser(info, oncomplete) { function complete(status) { oncomplete && oncomplete(status); } - user.createUser(email, function onComplete(status, info) { + user.createPrimaryUser(info, function onComplete(status, info) { switch(status) { - case "secondary.already_added": - $('#registeredEmail').html(email); - showNotice(".alreadyRegistered"); - complete(false); - break; - case "secondary.verify": - pageHelpers.emailSent(complete); - break; - case "secondary.could_not_add": - tooltip.showTooltip("#could_not_add"); - complete(false); - break; - case "primary.already_added": - // XXX Is this status possible? - break; case "primary.verified": pageHelpers.replaceFormWithNotice("#congrats", complete.bind(null, true)); break; case "primary.verify": - verifyEmail = email; - verifyURL = info.auth; - dom.setInner("#primary_email", email); + primaryUserInfo = info; + dom.setInner("#primary_email", info.email); pageHelpers.replaceInputsWithNotice("#primary_verify", complete.bind(null, false)); break; case "primary.could_not_add": @@ -80,18 +65,62 @@ BrowserID.signUp = (function() { }, pageHelpers.getFailure(errors.createUser, complete)); } - function submit(oncomplete) { - var email = helpers.getAndValidateEmail("#email"); + function enterPasswordState(info) { + var self=this; + self.emailToStage = info.email; + self.submit = passwordSubmit; - function complete(status) { - oncomplete && oncomplete(status); + dom.addClass("body", "enter_password"); + } + + function passwordSubmit(oncomplete) { + var pass = dom.getInner("#password"), + vpass = dom.getInner("#vpassword"), + valid = validation.passwordAndValidationPassword(pass, vpass); + + if(valid) { + user.createSecondaryUser(this.emailToStage, pass, function(status) { + if(status) { + pageHelpers.emailSent(oncomplete && oncomplete.curry(true)); + } + else { + tooltip.showTooltip("#could_not_add"); + oncomplete && oncomplete(false); + } + }, pageHelpers.getFailure(errors.createUser, oncomplete)); + } + else { + oncomplete && oncomplete(false); } + } + + function emailSubmit(oncomplete) { + var email = helpers.getAndValidateEmail("#email"), + self = this; if (email) { - createUser(email, complete); + + user.isEmailRegistered(email, function(isRegistered) { + if(isRegistered) { + $('#registeredEmail').html(email); + showNotice(".alreadyRegistered"); + oncomplete && oncomplete(false); + } + else { + user.addressInfo(email, function(info) { + if(info.type === "primary") { + createPrimaryUser.call(self, info, oncomplete); + } + else { + enterPasswordState.call(self, info); + oncomplete && oncomplete(!isRegistered); + } + }, pageHelpers.getFailure(errors.addressInfo, oncomplete)); + } + }, pageHelpers.getFailure(errors.isEmailRegistered, oncomplete)); } else { - complete(false); + oncomplete && oncomplete(false); } } @@ -103,39 +132,45 @@ BrowserID.signUp = (function() { if (event.which !== 13) $(".notification").fadeOut(ANIMATION_SPEED); } - function init(config) { - config = config || {}; + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + var self=this; + options = options || {}; - if(config.winchan) { - winchan = config.winchan; - } + if (options.winchan) { + winchan = options.winchan; + } - $("form input[autofocus]").focus(); + dom.focus("form input[autofocus]"); - pageHelpers.setupEmail(); + pageHelpers.setupEmail(); + + self.bind("#email", "keyup", onEmailKeyUp); + self.click("#back", back); + self.click("#authWithPrimary", authWithPrimary); + + sc.start.call(self, options); + }, + + submit: emailSubmit, + // BEGIN TESTING API + emailSubmit: emailSubmit, + passwordSubmit: passwordSubmit, + reset: reset, + back: back, + authWithPrimary: authWithPrimary, + primaryAuthComplete: primaryAuthComplete + // END TESTING API + }); - dom.bindEvent("#email", "keyup", onEmailKeyUp); - dom.bindEvent("form", "submit", cancelEvent(submit)); - dom.bindEvent("#back", "click", cancelEvent(back)); - dom.bindEvent("#authWithPrimary", "click", cancelEvent(authWithPrimary)); - } // BEGIN TESTING API function reset() { - dom.unbindEvent("#email", "keyup"); - dom.unbindEvent("form", "submit"); - dom.unbindEvent("#back", "click"); - dom.unbindEvent("#authWithPrimary", "click"); winchan = window.WinChan; - verifyEmail = verifyURL = null; } - - init.submit = submit; - init.reset = reset; - init.back = back; - init.authWithPrimary = authWithPrimary; - init.primaryAuthComplete = primaryAuthComplete; // END TESTING API - return init; + sc = Module.sc; + + return Module; }()); diff --git a/resources/static/pages/start.js b/resources/static/pages/start.js index 34e4c2dec70f3cfdc4588a875a3ad1f4e09198ce..e43661c6d51a1cbff4ec3f7af807fc2dd271ddf4 100644 --- a/resources/static/pages/start.js +++ b/resources/static/pages/start.js @@ -24,7 +24,8 @@ $(function() { CookieCheck = modules.CookieCheck, XHRDelay = modules.XHRDelay, XHRDisableForm = modules.XHRDisableForm, - ANIMATION_TIME = 500; + ANIMATION_TIME = 500, + checkCookiePaths = [ "/signin", "/signup", "/forgot", "/add_email_address", "/verify_email_address" ]; xhr.init({ time_until_delay: 10 * 1000 }); @@ -41,7 +42,7 @@ $(function() { moduleManager.register("xhr_disable_form", XHRDisableForm); moduleManager.start("xhr_disable_form"); - if(path && path !== "/") { + if(path && (checkCookiePaths.indexOf(path) > -1)) { // do a cookie check on every page except the main page. moduleManager.register("cookie_check", CookieCheck); moduleManager.start("cookie_check", { ready: start }); @@ -66,19 +67,25 @@ $(function() { module.start({}); } else if (path === "/signup") { - bid.signUp(); + var module = bid.signUp.create(); + module.start({}); } else if (path === "/forgot") { bid.forgot(); } else if (path === "/add_email_address") { - var module = bid.addEmailAddress.create(); + var module = bid.verifySecondaryAddress.create(); module.start({ - token: token + token: token, + verifyFunction: "verifyEmail" }); } - else if(token && path === "/verify_email_address") { - bid.verifyEmailAddress(token); + else if(path === "/verify_email_address") { + var module = bid.verifySecondaryAddress.create(); + module.start({ + token: token, + verifyFunction: "verifyUser" + }); } else { // Instead of throwing a hard error here, adding a message to the console diff --git a/resources/static/pages/verify_email_address.js b/resources/static/pages/verify_email_address.js deleted file mode 100644 index b53ab9b8f4f47b8d53de3ebdc99551a0c4151b5e..0000000000000000000000000000000000000000 --- a/resources/static/pages/verify_email_address.js +++ /dev/null @@ -1,66 +0,0 @@ -/*globals BrowserID: true, $:true */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -(function() { - "use strict"; - - var bid = BrowserID, - errors = bid.Errors, - pageHelpers = bid.PageHelpers, - token; - - function submit(oncomplete) { - var pass = $("#password").val(), - vpass = $("#vpassword").val(); - - var valid = bid.Validation.passwordAndValidationPassword(pass, vpass); - - if (valid) { - bid.Network.completeUserRegistration(token, pass, function onSuccess(registered) { - var selector = registered ? "#congrats" : "#cannotcomplete"; - pageHelpers.replaceFormWithNotice(selector, oncomplete); - }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete)); - } - else { - oncomplete && oncomplete(); - } - } - - function init(tok, oncomplete) { - $("#signUpForm").bind("submit", pageHelpers.cancelEvent(submit)); - $(".siteinfo").hide(); - $("#congrats").hide(); - token = tok; - - var staged = bid.Storage.getStagedOnBehalfOf(); - if (staged) { - $('.website').html(staged); - $('.siteinfo').show(); - } - - // go get the email address - bid.Network.emailForVerificationToken(token, function(info) { - if (info) { - $('#email').val(info.email); - oncomplete && oncomplete(); - } - else { - pageHelpers.replaceFormWithNotice("#cannotconfirm", oncomplete); - } - }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete)); - } - - // BEGIN TESTING API - function reset() { - $("#signUpForm").unbind("submit"); - } - - init.submit = submit; - init.reset = reset; - // END TESTING API; - - bid.verifyEmailAddress = init; - -}()); diff --git a/resources/static/pages/verify_secondary_address.js b/resources/static/pages/verify_secondary_address.js new file mode 100644 index 0000000000000000000000000000000000000000..2ccfb0c804ec90c672114c6235fcbefee8b3e0df --- /dev/null +++ b/resources/static/pages/verify_secondary_address.js @@ -0,0 +1,94 @@ +/*globals BrowserID: true, $:true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +BrowserID.verifySecondaryAddress = (function() { + "use strict"; + + var ANIMATION_TIME=250, + bid = BrowserID, + user = bid.User, + errors = bid.Errors, + pageHelpers = bid.PageHelpers, + dom = bid.DOM, + helpers = bid.Helpers, + complete = helpers.complete, + validation = bid.Validation, + token, + sc, + mustAuth, + verifyFunction; + + function showError(el, oncomplete) { + dom.hide(".hint,#signUpForm"); + $(el).fadeIn(ANIMATION_TIME, oncomplete); + } + + function showRegistrationInfo(info) { + dom.setInner("#email", info.email); + + if (info.origin) { + dom.setInner(".website", info.origin); + dom.show(".siteinfo"); + } + } + + function submit(oncomplete) { + var pass = dom.getInner("#password") || undefined, + valid = !mustAuth || validation.password(pass); + + if (valid) { + user[verifyFunction](token, pass, function(info) { + dom.addClass("body", "complete"); + + var selector = info.valid ? "#congrats" : "#cannotcomplete"; + pageHelpers.replaceFormWithNotice(selector, complete.curry(oncomplete, info.valid)); + }, pageHelpers.getFailure(errors.verifyEmail, oncomplete)); + } + else { + complete(oncomplete, false); + } + } + + function startVerification(oncomplete) { + user.tokenInfo(token, function(info) { + if(info) { + showRegistrationInfo(info); + + mustAuth = info.must_auth; + + if (mustAuth) { + dom.addClass("body", "enter_password"); + complete(oncomplete, true); + } + else { + submit(oncomplete); + } + } + else { + showError("#cannotconfirm"); + complete(oncomplete, false); + } + }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete)); + } + + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + this.checkRequired(options, "token", "verifyFunction"); + + token = options.token; + verifyFunction = options.verifyFunction; + + startVerification(options.ready); + + sc.start.call(this, options); + }, + + submit: submit + }); + + sc = Module.sc; + + return Module; +}()); diff --git a/resources/static/shared/history.js b/resources/static/shared/history.js index 6cbaa68fe1a7deeeeba5ed898029a78acab8233d..8c7653c3aaa3df1fbf4016d93a89fc45c3bd8b73 100644 --- a/resources/static/shared/history.js +++ b/resources/static/shared/history.js @@ -29,6 +29,7 @@ BrowserID.History = (function() { return this.current; }, + // XXX this should be renamed to pushState saveState: function() { this.history.push(this.current); }, diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js index a86f40cf99d4dfafd53bcb26f1a74a257aa43841..3a0186f0ada2dcda42a8ec543134c4f666cee25a 100644 --- a/resources/static/shared/modules/page_module.js +++ b/resources/static/shared/modules/page_module.js @@ -51,8 +51,9 @@ BrowserID.Modules.PageModule = (function() { start: function(options) { var self=this; + self.options = options || {}; + self.bind("form", "submit", cancelEvent(onSubmit)); - self.click("#thisIsNotMe", self.close.bind(self, "notme")); }, stop: function() { @@ -153,6 +154,7 @@ BrowserID.Modules.PageModule = (function() { submit: function() { }, + // XXX maybe we should not get rid of this. close: function(message) { this.destroy(); if (message) { diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index d1e0cfc1d4391ae2f1408be4985de6ada84683e2..23d92ff38fe92c090c89dc073d6e780ea455151b 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -203,16 +203,18 @@ BrowserID.Network = (function() { /** * Create a new user. Requires a user to verify identity. * @method createUser - * @param {string} email - Email address to prepare. + * @param {string} email + * @param {string} password * @param {string} origin - site user is trying to sign in to. * @param {function} [onComplete] - Callback to call when complete. * @param {function} [onFailure] - Called on XHR failure. */ - createUser: function(email, origin, onComplete, onFailure) { + createUser: function(email, password, origin, onComplete, onFailure) { post({ url: "/wsapi/stage_user", data: { email: email, + pass: password, site : origin }, success: function(status) { @@ -279,7 +281,7 @@ BrowserID.Network = (function() { * Complete user registration, give user a password * @method completeUserRegistration * @param {string} token - token to register for. - * @param {string} password - password to register for account. + * @param {string} password * @param {function} [onComplete] - Called when complete. * @param {function} [onFailure] - Called on XHR failure. */ @@ -301,7 +303,7 @@ BrowserID.Network = (function() { * Call with a token to prove an email address ownership. * @method completeEmailRegistration * @param {string} token - token proving email ownership. - * @param {string} password - password to set if necessary. If not necessary, set to undefined. + * @param {string} password * @param {function} [onComplete] - Callback to call when complete. Called * with one boolean parameter that specifies the validity of the token. * @param {function} [onFailure] - Called on XHR failure. @@ -323,13 +325,15 @@ BrowserID.Network = (function() { /** * Request a password reset for the given email address. * @method requestPasswordReset - * @param {string} email - email address to reset password for. + * @param {string} email + * @param {string} password + * @param {string} origin * @param {function} [onComplete] - Callback to call when complete. * @param {function} [onFailure] - Called on XHR failure. */ - requestPasswordReset: function(email, origin, onComplete, onFailure) { + requestPasswordReset: function(email, password, origin, onComplete, onFailure) { if (email) { - Network.createUser(email, origin, onComplete, onFailure); + Network.createUser(email, password, origin, onComplete, onFailure); } else { // TODO: if no email is provided, then what? throw "no email provided to password reset"; @@ -439,16 +443,18 @@ BrowserID.Network = (function() { /** * Add a secondary email to the current user's account. * @method addSecondaryEmail - * @param {string} email - Email address to add. - * @param {string} origin - site user is trying to sign in to. + * @param {string} email + * @param {string} password + * @param {string} origin * @param {function} [onComplete] - called when complete. * @param {function} [onFailure] - called on xhr failure. */ - addSecondaryEmail: function(email, origin, onComplete, onFailure) { + addSecondaryEmail: function(email, password, origin, onComplete, onFailure) { post({ url: "/wsapi/stage_email", data: { email: email, + pass: password, site: origin }, success: function(response) { @@ -659,7 +665,10 @@ BrowserID.Network = (function() { withContext(function() { try { // set a test cookie with a duration of 1 second. - // NOTE - The Android 3.3 default browser will still pass this. + // NOTE - The Android 3.3 and 4.0 default browsers will still pass + // this check. This causes the Android browsers to only display the + // cookies diabled error screen only after the user has entered and + // submitted input. // http://stackoverflow.com/questions/8509387/android-browser-not-respecting-cookies-disabled/9264996#9264996 document.cookie = "test=true; max-age=1"; var enabled = document.cookie.indexOf("test") > -1; diff --git a/resources/static/shared/state_machine.js b/resources/static/shared/state_machine.js index 1b2c90b370af8258cf213fc3167c5b696dde180f..c6dcafdcc6db6f2f1c4a8e58754d0f859b02bcca 100644 --- a/resources/static/shared/state_machine.js +++ b/resources/static/shared/state_machine.js @@ -53,6 +53,7 @@ BrowserID.StateMachine = (function() { // only save the current state when a new state comes in. var cmd = history.getCurrent(); if(cmd && cmd.save) { + // XXX saveState should be renamed to pushState history.saveState(); } diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js index dd5c8ce0f17105f804b70af5a1a03c3d89e851a9..a6a066509d501585d6d5fc6c7390457f064d1f87 100644 --- a/resources/static/shared/storage.js +++ b/resources/static/shared/storage.js @@ -3,12 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ BrowserID.Storage = (function() { + "use strict"; var jwk, - ONE_DAY_IN_MS = (1000 * 60 * 60 * 24); + ONE_DAY_IN_MS = (1000 * 60 * 60 * 24), + storage; try { - var storage = localStorage; + storage = localStorage; } catch(e) { // Fx with cookies disabled will except while trying to access @@ -66,6 +68,18 @@ BrowserID.Storage = (function() { storeEmails(emails); } + function addPrimaryEmail(email, obj) { + obj = obj || {}; + obj.type = "primary"; + addEmail(email, obj); + } + + function addSecondaryEmail(email, obj) { + obj = obj || {}; + obj.type = "secondary"; + addEmail(email, obj); + } + function removeEmail(email) { var emails = getEmails(); if(emails[email]) { @@ -212,7 +226,7 @@ BrowserID.Storage = (function() { if (lastState !== currentState) { callback(); lastState = currentState; - }; + } } // IE8 does not have addEventListener, nor does it support storage events. @@ -350,7 +364,7 @@ BrowserID.Storage = (function() { function clearUsersComputerOwnershipStatus(userid) { try { - allInfo = JSON.parse(storage.usersComputer); + var allInfo = JSON.parse(storage.usersComputer); if (typeof allInfo !== 'object') throw 'bogus'; var userInfo = allInfo[userid]; @@ -435,6 +449,16 @@ BrowserID.Storage = (function() { * @method addEmail */ addEmail: addEmail, + /** + * Add a primary address + * @method addPrimaryEmail + */ + addPrimaryEmail: addPrimaryEmail, + /** + * Add a secondary address + * @method addSecondaryEmail + */ + addSecondaryEmail: addSecondaryEmail, /** * Get all email addresses and their associated key pairs * @method getEmails diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index aee3065b955d292649396333666c01dfa03ae811..fb95b25567fa639e3bee1fe83d68ea4cd919600f 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -280,27 +280,30 @@ BrowserID.User = (function() { /** * Create a user account - this creates an user account that must be verified. * @method createSecondaryUser - * @param {string} email - Email address. + * @param {string} email + * @param {string} password * @param {function} [onComplete] - Called on completion. * @param {function} [onFailure] - Called on error. */ - createSecondaryUser: function(email, onComplete, onFailure) { - // remember this for later + createSecondaryUser: function(email, password, onComplete, onFailure) { + // Used on the main site when the user verifies - we try to show them + // what URL they came from. + + // XXX - this will have to be updated to either store both the hostname + // and the exact URL of the RP or just the URL of the RP and the origin + // is extracted from that. storage.setStagedOnBehalfOf(User.getHostname()); - network.createUser(email, origin, onComplete, onFailure); + network.createUser(email, password, origin, onComplete, onFailure); }, /** - * Create a user. Works for both primaries and secondaries. + * Create a primary user. * @method createUser - * @param {string} email + * @param {object} info * @param {function} onComplete - function to call on complettion. Called * with two parameters - status and info. * Status can be: - * secondary.already_added - * secondary.verify - * secondary.could_not_add * primary.already_added * primary.verified * primary.verify @@ -309,62 +312,23 @@ BrowserID.User = (function() { * info is passed on primary.verify and contains the info necessary to * verify the user with the IdP */ - createUser: function(email, onComplete, onFailure) { - User.addressInfo(email, function(info) { - User.createUserWithInfo(email, info, onComplete, onFailure); - }, onFailure); - }, - - /** - * Attempt to create a user with the info returned from - * network.addressInfo. Attempts to create both primary and secondary - * based users depending on info.type. - * @method createUserWithInfo - * @param {string} email - * @param {object} info - contains fields returned from network.addressInfo - * @param {function} [onComplete] - * @param {function} [onFailure] - */ - createUserWithInfo: function(email, info, onComplete, onFailure) { - function attemptAddSecondary(email, info) { - if (info.known) { - onComplete("secondary.already_added"); - } - else { - User.createSecondaryUser(email, function(success) { - if (success) { - onComplete("secondary.verify"); + createPrimaryUser: function(info, onComplete, onFailure) { + var email = info.email; + User.provisionPrimaryUser(email, info, function(status, provInfo) { + if (status === "primary.verified") { + network.authenticateWithAssertion(email, provInfo.assertion, function(status) { + if (status) { + onComplete("primary.verified"); } else { - onComplete("secondary.could_not_add"); + onComplete("primary.could_not_add"); } }, onFailure); } - } - - function attemptAddPrimary(email, info) { - User.provisionPrimaryUser(email, info, function(status, provInfo) { - if (status === "primary.verified") { - network.authenticateWithAssertion(email, provInfo.assertion, function(status) { - if (status) { - onComplete("primary.verified"); - } - else { - onComplete("primary.could_not_add"); - } - }, onFailure); - } - else { - onComplete(status, provInfo); - } - }, onFailure); - } - - if (info.type === 'secondary') { - attemptAddSecondary(email, info); - } else { - attemptAddPrimary(email, info); - } + else { + onComplete(status, provInfo); + } + }, onFailure); }, /** @@ -533,8 +497,10 @@ BrowserID.User = (function() { * Verify a user * @method verifyUser * @param {string} token - token to verify. - * @param {string} password - password to set for account. - * @param {function} [onComplete] - Called to give status updates. + * @param {string} password + * @param {function} [onComplete] - Called on completion. + * Called with an object with valid, email, and origin if valid, called + * with valid=false otw. * @param {function} [onFailure] - Called on error. */ verifyUser: function(token, password, onComplete, onFailure) { @@ -542,9 +508,14 @@ BrowserID.User = (function() { var invalidInfo = { valid: false }; if (info) { network.completeUserRegistration(token, password, function (valid) { - info.valid = valid; - storage.setStagedOnBehalfOf(""); - if (onComplete) onComplete(info); + var result = invalidInfo; + + if(valid) { + result = _.extend({ valid: valid, origin: storage.getStagedOnBehalfOf() }, info); + storage.setStagedOnBehalfOf(""); + } + + complete(onComplete, result); }, onFailure); } else if (onComplete) { onComplete(invalidInfo); @@ -592,17 +563,18 @@ BrowserID.User = (function() { /** * Request a password reset for the given email address. * @method requestPasswordReset - * @param {string} email - email address to reset password for. + * @param {string} email + * @param {string} password * @param {function} [onComplete] - Callback to call when complete, called * with a single object, info. * info.status {boolean} - true or false whether request was successful. * info.reason {string} - if status false, reason of failure. * @param {function} [onFailure] - Called on XHR failure. */ - requestPasswordReset: function(email, onComplete, onFailure) { + requestPasswordReset: function(email, password, onComplete, onFailure) { User.isEmailRegistered(email, function(registered) { if (registered) { - network.requestPasswordReset(email, origin, function(reset) { + network.requestPasswordReset(email, password, origin, function(reset) { var status = { success: reset }; @@ -829,12 +801,13 @@ BrowserID.User = (function() { * does not add the new email address/keypair to the local list of * valid identities. * @method addEmail - * @param {string} email - Email address. + * @param {string} email + * @param {string} password * @param {function} [onComplete] - Called on successful completion. * @param {function} [onFailure] - Called on error. */ - addEmail: function(email, onComplete, onFailure) { - network.addSecondaryEmail(email, origin, function(added) { + addEmail: function(email, password, onComplete, onFailure) { + network.addSecondaryEmail(email, password, origin, function(added) { if (added) storage.setStagedOnBehalfOf(User.getHostname()); // we no longer send the keypair, since we will certify it later. @@ -842,6 +815,27 @@ BrowserID.User = (function() { }, onFailure); }, + /** + * Check whether a password is needed to add a secondary email address to + * an already existing account. + * @method passwordNeededToAddSecondaryEmail + * @param {function} [onComplete] - Called on successful completion, called + * with true if password is needed, false otw. + * @param {function} [onFailure] - Called on error. + */ + passwordNeededToAddSecondaryEmail: function(onComplete, onFailure) { + var emails = storage.getEmails(), + passwordNeeded = true; + + for(var key in emails) { + if(emails[key].type === "secondary") { + passwordNeeded = false; + } + } + + complete(onComplete, passwordNeeded); + }, + /** * Wait for the email registration to complete * @method waitForEmailValidation @@ -865,23 +859,17 @@ BrowserID.User = (function() { * Verify a users email address given by the token * @method verifyEmail * @param {string} token + * @param {string} password * @param {function} [onComplete] - Called on completion. * Called with an object with valid, email, and origin if valid, called - * with only valid otw. + * with valid=false otw. * @param {function} [onFailure] - Called on error. */ - verifyEmailNoPassword: function(token, onComplete, onFailure) { - User.verifyEmailWithPassword(token, undefined, onComplete, onFailure); - }, - - verifyEmailWithPassword: function(token, pass, onComplete, onFailure) { - function complete(status) { - onComplete && onComplete(status); - } + verifyEmail: function(token, password, onComplete, onFailure) { network.emailForVerificationToken(token, function (info) { var invalidInfo = { valid: false }; if (info) { - network.completeEmailRegistration(token, pass, function (valid) { + network.completeEmailRegistration(token, password, function (valid) { var result = invalidInfo; if(valid) { @@ -889,10 +877,10 @@ BrowserID.User = (function() { storage.setStagedOnBehalfOf(""); } - complete(result); + complete(onComplete, result); }, onFailure); } else { - complete(invalidInfo); + complete(onComplete, invalidInfo); } }, onFailure); }, @@ -1035,7 +1023,9 @@ BrowserID.User = (function() { sortedIdentities = []; for(var key in identities) { - sortedIdentities.push({ address: key, info: identities[key] }); + if(identities.hasOwnProperty(key)) { + sortedIdentities.push({ address: key, info: identities[key] }); + } } sortedIdentities.sort(function(a, b) { diff --git a/resources/static/test/cases/controllers/actions.js b/resources/static/test/cases/controllers/actions.js index 0e0881e2748005970320b4f1d6ac170276af0a04..11c58cd960ce705d77743c6f011ba3b466e0e1a1 100644 --- a/resources/static/test/cases/controllers/actions.js +++ b/resources/static/test/cases/controllers/actions.js @@ -84,7 +84,7 @@ asyncTest("doCannotVerifyRequiredPrimary - show the error screen", function() { createController({ ready: function() { - controller.doCannotVerifyRequiredPrimary({ email: "testuser@testuser.com"}); + controller.doCannotVerifyRequiredPrimary({ email: TEST_EMAIL}); testHelpers.testErrorVisible(); start(); @@ -112,5 +112,26 @@ testActionStartsModule('doGenerateAssertion', { email: TEST_EMAIL }, "generate_assertion"); }); + + asyncTest("doStageUser with successful creation - trigger user_staged", function() { + createController({ + ready: function() { + var email; + testHelpers.register("user_staged", function(msg, info) { + email = info.email; + }); + + controller.doStageUser({ email: TEST_EMAIL, password: "password", ready: function(status) { + equal(status, true, "correct status"); + equal(email, TEST_EMAIL, "user successfully staged"); + start(); + }}); + } + }); + }); + + asyncTest("doForgotPassword - call the set_password controller with reset_password true", function() { + testActionStartsModule('doForgotPassword', { email: TEST_EMAIL }, "set_password"); + }); }()); diff --git a/resources/static/test/cases/controllers/add_email.js b/resources/static/test/cases/controllers/add_email.js index b18696f31079038a463fff7341c767e4901a2ab8..b490b519d105fb666a0014bb403a8583eccfd672 100644 --- a/resources/static/test/cases/controllers/add_email.js +++ b/resources/static/test/cases/controllers/add_email.js @@ -10,6 +10,7 @@ el = $("body"), bid = BrowserID, user = bid.User, + storage = bid.Storage, xhr = bid.Mocks.xhr, modules = bid.Modules, testHelpers = bid.TestHelpers, @@ -55,7 +56,7 @@ ok($("#newEmail").val(), "testuser@testuser.com", "email prepopulated"); }); - asyncTest("addEmail with valid unknown secondary email", function() { + asyncTest("addEmail with first valid unknown secondary email - trigger add_email_submit_with_secondary", function() { createController(); xhr.useResult("unknown_secondary"); @@ -63,21 +64,38 @@ $("#newEmail").val("unregistered@testuser.com"); - register("email_staged", function(msg, info) { - equal(info.email, "unregistered@testuser.com", "email_staged called with correct email"); + register("add_email_submit_with_secondary", function(msg, info) { + equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email"); start(); }); controller.addEmail(); }); - asyncTest("addEmail with valid unknown secondary email with leading/trailing whitespace", function() { + asyncTest("addEmail with second valid unknown secondary email - trigger add_email_submit_with_secondary", function() { + createController(); + xhr.useResult("unknown_secondary"); + + equal($("#addEmail").length, 1, "control rendered correctly"); + + $("#newEmail").val("unregistered@testuser.com"); + + register("add_email_submit_with_secondary", function(msg, info) { + equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email"); + start(); + }); + + storage.addSecondaryEmail("testuser@testuser.com"); + controller.addEmail(); + }); + + asyncTest("addEmail with valid unknown secondary email with leading/trailing whitespace - allows address, triggers add_email_submit_with_secondary", function() { createController(); xhr.useResult("unknown_secondary"); $("#newEmail").val(" unregistered@testuser.com "); - register("email_staged", function(msg, info) { - equal(info.email, "unregistered@testuser.com", "email_staged called with correct email"); + register("add_email_submit_with_secondary", function(msg, info) { + equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email"); start(); }); controller.addEmail(); @@ -88,12 +106,12 @@ $("#newEmail").val("unregistered"); var handlerCalled = false; - register("email_staged", function(msg, info) { + register("add_email_submit_with_secondary", function(msg, info) { handlerCalled = true; - ok(false, "email_staged should not be called on invalid email"); + ok(false, "add_email_submit_with_secondary should not be called on invalid email"); }); controller.addEmail(function() { - equal(handlerCalled, false, "the email_staged handler should have never been called"); + equal(handlerCalled, false, "the add_email_submit_with_secondary handler should have never been called"); start(); }); }); @@ -103,8 +121,8 @@ $("#newEmail").val("registered@testuser.com"); - register("email_staged", function(msg, info) { - ok(false, "unexpected email_staged message"); + register("add_email_submit_with_secondary", function(msg, info) { + ok(false, "unexpected add_email_submit_with_secondary message"); }); // simulate the email being already added. @@ -119,13 +137,13 @@ }); }); - asyncTest("addEmail with secondary email belonging to another user - allows for account consolidation", function() { + asyncTest("addEmail with first secondary email belonging to another user - allows for account consolidation", function() { createController(); xhr.useResult("known_secondary"); $("#newEmail").val("registered@testuser.com"); - register("email_staged", function(msg, info) { - equal(info.email, "registered@testuser.com", "email_staged called with correct email"); + register("add_email_submit_with_secondary", function(msg, info) { + equal(info.email, "registered@testuser.com", "add_email_submit_with_secondary called with correct email"); start(); }); controller.addEmail(); diff --git a/resources/static/test/cases/controllers/authenticate.js b/resources/static/test/cases/controllers/authenticate.js index dedd5352ca678c405250f5e39f0be16168d3648b..e410de5e57adfa512fbdd34e96339a1528a49930 100644 --- a/resources/static/test/cases/controllers/authenticate.js +++ b/resources/static/test/cases/controllers/authenticate.js @@ -80,29 +80,32 @@ }); function testUserUnregistered() { - register("create_user", function() { - ok(true, "email was valid, user not registered"); + register("new_user", function(msg, info, rehydrate) { + ok(info.email, "new_user triggered with info.email"); + // rehydration email used to go back to authentication controller if + // the user cancels one of the next steps. + ok(rehydrate.email, "new_user triggered with rehydrate.email"); start(); }); controller.checkEmail(); } - asyncTest("checkEmail with unknown secondary email, expect 'create_user' message", function() { + asyncTest("checkEmail with unknown secondary email - 'new_user' message", function() { $("#email").val("unregistered@testuser.com"); xhr.useResult("unknown_secondary"); testUserUnregistered(); }); - asyncTest("checkEmail with email with leading/trailing whitespace, user not registered, expect 'create_user' message", function() { + asyncTest("checkEmail with email with leading/trailing whitespace, user not registered - 'new_user' message", function() { $("#email").val(" unregistered@testuser.com "); xhr.useResult("unknown_secondary"); testUserUnregistered(); }); - asyncTest("checkEmail with normal email, user registered, expect 'enter_password' message", function() { + asyncTest("checkEmail with normal email, user registered - 'enter_password' message", function() { $("#email").val("registered@testuser.com"); xhr.useResult("known_secondary"); @@ -114,7 +117,7 @@ controller.checkEmail(); }); - asyncTest("checkEmail with email that has IdP support, expect 'primary_user' message", function() { + asyncTest("checkEmail with email that has IdP support - 'primary_user' message", function() { $("#email").val("unregistered@testuser.com"); xhr.useResult("primary"); @@ -165,8 +168,8 @@ $("#email").val("unregistered@testuser.com"); xhr.useResult("unknown_secondary"); - register("user_staged", function(msg, info) { - equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered"); + register("new_user", function(msg, info) { + equal(info.email, "unregistered@testuser.com", "new_user with correct email triggered"); start(); }); @@ -177,43 +180,12 @@ $("#email").val("unregistered"); var handlerCalled = false; - register("user_staged", function(msg, info) { + register("new_user", function(msg, info) { handlerCalled = true; }); controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with invalid email"); - start(); - }); - }); - - asyncTest("createUser with valid email but throttling", function() { - $("#email").val("unregistered@testuser.com"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - xhr.useResult("throttle"); - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with throttling"); - equal(bid.Tooltip.shown, true, "tooltip is shown"); - start(); - }); - }); - - asyncTest("createUser with valid email, XHR error", function() { - $("#email").val("unregistered@testuser.com"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - xhr.useResult("ajaxError"); - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with XHR error"); + equal(handlerCalled, false, "bad jiji, new_user should not have been called with invalid email"); start(); }); }); diff --git a/resources/static/test/cases/controllers/forgot_password.js b/resources/static/test/cases/controllers/forgot_password.js index 77c24dd8939eb7c3f1898178a051febb00ca82e2..d81d8b1de331c895aab433830b56b639d8744411 100644 --- a/resources/static/test/cases/controllers/forgot_password.js +++ b/resources/static/test/cases/controllers/forgot_password.js @@ -40,9 +40,9 @@ equal($("#email").val(), "registered@testuser.com", "email prefilled"); }); - asyncTest("resetPassword raises 'reset_password' with email address", function() { - register("reset_password", function(msg, info) { - equal(info.email, "registered@testuser.com", "reset_password raised with correct email address"); + asyncTest("resetPassword raises 'password_reset' with email address", function() { + register("password_reset", function(msg, info) { + equal(info.email, "registered@testuser.com", "password_reset raised with correct email address"); start(); }); diff --git a/resources/static/test/cases/controllers/pick_email.js b/resources/static/test/cases/controllers/pick_email.js index 7b560c6b5e4df29a3990cb0ee95b857bc849af4e..555d2f4631c63ae0c9949339feab0e389c180e2c 100644 --- a/resources/static/test/cases/controllers/pick_email.js +++ b/resources/static/test/cases/controllers/pick_email.js @@ -137,5 +137,16 @@ equal($("#email_0").is(":checked"), true, "radio button is correctly selected"); }); + asyncTest("click on not me button - trigger notme message", function() { + createController(); + + register("notme", function(msg, info) { + ok(true, "notme triggered"); + start(); + }); + + $("#thisIsNotMe").click(); + }); + }()); diff --git a/resources/static/test/cases/controllers/required_email.js b/resources/static/test/cases/controllers/required_email.js index 5a92da544ebd7de3e4a013ac63f7138dfed7475b..5251469d8b86069c82f8333ffe99ab1acca70ad4 100644 --- a/resources/static/test/cases/controllers/required_email.js +++ b/resources/static/test/cases/controllers/required_email.js @@ -423,18 +423,18 @@ }); - asyncTest("verifyAddress of authenticated user, address belongs to another user - redirects to 'email_staged'", function() { + asyncTest("verifyAddress of authenticated user, secondary address belongs to another user - redirects to 'add_email_submit_with_secondary'", function() { var email = "registered@testuser.com"; xhr.useResult("known_secondary"); - testMessageReceived(email, "email_staged"); + testMessageReceived(email, "add_email_submit_with_secondary"); }); - asyncTest("verifyAddress of authenticated user, unknown address - redirects to 'email_staged'", function() { + asyncTest("verifyAddress of authenticated user, unknown address - redirects to 'add_email_submit_with_secondary'", function() { var email = "unregistered@testuser.com"; xhr.useResult("unknown_secondary"); - testMessageReceived(email, "email_staged"); + testMessageReceived(email, "add_email_submit_with_secondary"); }); asyncTest("verifyAddress of un-authenticated user, forgot password - redirect to 'forgot_password'", function() { diff --git a/resources/static/test/cases/controllers/set_password.js b/resources/static/test/cases/controllers/set_password.js new file mode 100644 index 0000000000000000000000000000000000000000..bfdda1c0bdee52084d84a0eb773fcf7898e1678a --- /dev/null +++ b/resources/static/test/cases/controllers/set_password.js @@ -0,0 +1,69 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +(function() { + "use strict"; + + var controller, + el = $("body"), + bid = BrowserID, + testHelpers = bid.TestHelpers, + register = testHelpers.register, + controller; + + function createController(options) { + controller = bid.Modules.SetPassword.create(); + controller.start(options); + } + + module("controllers/set_password", { + setup: function() { + testHelpers.setup(); + createController(); + }, + + teardown: function() { + controller.destroy(); + testHelpers.teardown(); + } + }); + + + test("create with no options - show template, user must verify email", function() { + ok($("#set_password").length, "set_password template added"); + equal($("#verify_user").length, 1, "correct button shown"); + }); + + test("create with password_reset option - show template, show reset password button", function() { + controller.destroy(); + createController({ password_reset: true }); + ok($("#set_password").length, "set_password template added"); + equal($("#password_reset").length, 1, "correct button shown"); + }); + + asyncTest("submit with good password/vpassword - password_set message raised", function() { + $("#password").val("password"); + $("#vpassword").val("password"); + + var password; + register("password_set", function(msg, info) { + password = info.password; + }); + + controller.submit(function() { + equal(password, "password", "password_set message raised with correct password"); + start(); + }); + }); + + asyncTest("cancel - cancel_state message raised", function() { + register("cancel_state", function(msg, info) { + ok(true, "state cancelled"); + start(); + }); + + $("#cancel").click(); + }); +}()); diff --git a/resources/static/test/cases/pages/forgot.js b/resources/static/test/cases/pages/forgot.js index 22491ef900b829917dda7841d34b89671b0a492f..d1b1466193204d8a576cd6fc0a99bdc23ab57b39 100644 --- a/resources/static/test/cases/pages/forgot.js +++ b/resources/static/test/cases/pages/forgot.js @@ -24,60 +24,99 @@ } }); - function testEmailNotSent(extraTests) { + function testEmailNotSent(config) { + config = config || {}; bid.forgot.submit(function() { equal($(".emailsent").is(":visible"), false, "email not sent"); - if (extraTests) extraTests(); + if(config.checkTooltip !== false) testHelpers.testTooltipVisible(); + if (config.ready) config.ready(); else start(); }); } asyncTest("requestPasswordReset with invalid email", function() { $("#email").val("invalid"); + $("#password,#vpassword").val("password"); xhr.useResult("invalid"); testEmailNotSent(); }); - asyncTest("requestPasswordReset with known email", function() { + asyncTest("requestPasswordReset with known email, happy case - show email sent notice", function() { $("#email").val("registered@testuser.com"); + $("#password,#vpassword").val("password"); + bid.forgot.submit(function() { ok($(".emailsent").is(":visible"), "email sent successfully"); start(); }); }); - asyncTest("requestPasswordReset with known email with leading/trailing whitespace", function() { + asyncTest("requestPasswordReset with known email with leading/trailing whitespace - show email sent notice", function() { $("#email").val(" registered@testuser.com "); + $("#password,#vpassword").val("password"); + bid.forgot.submit(function() { ok($(".emailsent").is(":visible"), "email sent successfully"); start(); }); }); + asyncTest("requestPasswordReset with missing password", function() { + $("#email").val("unregistered@testuser.com"); + $("#vpassword").val("password"); + + testEmailNotSent(); + }); + + asyncTest("requestPasswordReset with too short of a password", function() { + $("#email").val("unregistered@testuser.com"); + $("#password,#vpassword").val("fail"); + + testEmailNotSent(); + }); + + asyncTest("requestPasswordReset with too long of a password", function() { + $("#email").val("unregistered@testuser.com"); + $("#password,#vpassword").val(testHelpers.generateString(81)); + + testEmailNotSent(); + }); + + asyncTest("requestPasswordReset with missing vpassword", function() { + $("#email").val("unregistered@testuser.com"); + $("#password").val("password"); + + testEmailNotSent(); + }); + asyncTest("requestPasswordReset with unknown email", function() { $("#email").val("unregistered@testuser.com"); + $("#password,#vpassword").val("password"); testEmailNotSent(); }); asyncTest("requestPasswordReset with throttling", function() { - xhr.useResult("throttle"); - - $("#email").val("throttled@testuser.com"); + $("#email").val("registered@testuser.com"); + $("#password,#vpassword").val("password"); + xhr.useResult("throttle"); testEmailNotSent(); }); asyncTest("requestPasswordReset with XHR Error", function() { - xhr.useResult("ajaxError"); - $("#email").val("testuser@testuser.com"); + $("#password,#vpassword").val("password"); - testEmailNotSent(function() { - testHelpers.testErrorVisible(); - start(); + xhr.useResult("ajaxError"); + testEmailNotSent({ + ready: function() { + testHelpers.testErrorVisible(); + start(); + }, + checkTooltip: false }); }); diff --git a/resources/static/test/cases/pages/signup.js b/resources/static/test/cases/pages/signup.js index 6fe113b2b6ad8e6c5f08a01da9a3d7bc1733a6f7..92c2cd00573462efdfb27549a73f4711ab56fc7f 100644 --- a/resources/static/test/cases/pages/signup.js +++ b/resources/static/test/cases/pages/signup.js @@ -13,7 +13,8 @@ WinChanMock = bid.Mocks.WinChan, testHelpers = bid.TestHelpers, provisioning = bid.Mocks.Provisioning, - winchan; + winchan, + controller; module("pages/signup", { setup: function() { @@ -22,18 +23,20 @@ $(".emailsent").hide(); $(".notification").hide() winchan = new WinChanMock(); - bid.signUp({ + controller = bid.signUp.create(); + controller.start({ winchan: winchan }); }, teardown: function() { testHelpers.teardown(); - bid.signUp.reset(); + controller.reset(); + controller.destroy(); } }); - function testNotRegistered(extraTests) { - bid.signUp.submit(function(status) { + function testPasswordNotShown(extraTests) { + controller.submit(function(status) { strictEqual(status, false, "address was not registered"); equal($(".emailsent").is(":visible"), false, "email not sent, notice not visible"); @@ -42,64 +45,69 @@ }); } - asyncTest("signup with valid unregistered secondary email", function() { - xhr.useResult("unknown_secondary"); - + asyncTest("signup with valid unregistered secondary email - show password", function() { $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function() { - equal($(".emailsent").is(":visible"), true, "email sent, notice visible"); + controller.submit(function() { + equal($("body").hasClass("enter_password"), true, "new email, password section shown"); + start(); }); }); - asyncTest("signup with valid unregistered email with leading/trailing whitespace", function() { - xhr.useResult("unknown_secondary"); + asyncTest("submit with valid unregistered email with leading/trailing whitespace", function() { $("#email").val(" unregistered@testuser.com "); - bid.signUp.submit(function() { - equal($(".emailsent").is(":visible"), true, "email sent, notice visible"); + controller.submit(function() { + equal($("body").hasClass("enter_password"), true, "new email, password section shown"); start(); }); }); - asyncTest("signup with valid registered email", function() { - xhr.useResult("known_secondary"); + asyncTest("submit with valid registered email", function() { $("#email").val("registered@testuser.com"); - testNotRegistered(); + testPasswordNotShown(); }); - asyncTest("signup with invalid email address", function() { + asyncTest("submit with invalid email address", function() { $("#email").val("invalid"); - testNotRegistered(); + testPasswordNotShown(); }); - asyncTest("signup with throttling", function() { - xhr.useResult("throttle"); - + asyncTest("submit with XHR error", function() { + xhr.useResult("ajaxError"); $("#email").val("unregistered@testuser.com"); - testNotRegistered(); + testPasswordNotShown(function() { + testHelpers.testErrorVisible(); + }); }); - asyncTest("signup with XHR error", function() { - xhr.useResult("invalid"); + + asyncTest("passwordSubmit with throttling", function() { $("#email").val("unregistered@testuser.com"); + $("#password, #vpassword").val("password"); - testNotRegistered(function() { - testHelpers.testErrorVisible(); + xhr.useResult("throttle"); + controller.passwordSubmit(function(userStaged) { + equal(userStaged, false, "email throttling took effect, user not staged"); + start(); }); }); - asyncTest("signup with unregistered secondary email and cancel button pressed", function() { - xhr.useResult("unknown_secondary"); + asyncTest("passwordSubmit happy case, check back button too", function() { $("#email").val("unregistered@testuser.com"); + $("#password, #vpassword").val("password"); - bid.signUp.submit(function() { - bid.signUp.back(function() { + controller.passwordSubmit(function(userStaged) { + equal(userStaged, true, "user has been staged"); + equal($(".emailsent").is(":visible"), true, "email sent, notice visible"); + + // check back button + controller.back(function() { equal($(".notification:visible").length, 0, "no notifications are visible - visible: " + $(".notification:visible").attr("id")); ok($(".forminputs:visible").length, "form inputs are again visible"); equal($("#email").val(), "unregistered@testuser.com", "email address restored"); @@ -108,6 +116,7 @@ }); }); + asyncTest("signup with primary email address, provisioning failure - expect error screen", function() { xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); @@ -116,7 +125,7 @@ msg: "doowap" }); - bid.signUp.submit(function(status) { + controller.submit(function(status) { equal(status, false, "provisioning failure, status false"); testHelpers.testErrorVisible(); start(); @@ -128,7 +137,7 @@ $("#email").val("unregistered@testuser.com"); provisioning.setStatus(provisioning.AUTHENTICATED); - bid.signUp.submit(function(status) { + controller.submit(function(status) { equal(status, true, "primary addition success - true status"); equal($("#congrats:visible").length, 1, "success notification is visible"); start(); @@ -139,7 +148,7 @@ xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function(status) { + controller.submit(function(status) { equal($("#primary_verify:visible").length, 1, "success notification is visible"); equal($("#primary_email").text(), "unregistered@testuser.com", "correct email shown"); equal(status, false, "user must authenticate, some action needed."); @@ -151,8 +160,8 @@ xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function(status) { - bid.signUp.authWithPrimary(function() { + controller.submit(function(status) { + controller.authWithPrimary(function() { ok(winchan.oncomplete, "winchan set up"); start(); }); @@ -160,7 +169,7 @@ }); asyncTest("primaryAuthComplete with error, expect incorrect status", function() { - bid.signUp.primaryAuthComplete("error", "", function(status) { + controller.primaryAuthComplete("error", "", function(status) { equal(status, false, "correct status for could not complete"); testHelpers.testErrorVisible(); start(); @@ -171,15 +180,15 @@ xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function(status) { - bid.signUp.authWithPrimary(function() { + controller.submit(function(status) { + controller.authWithPrimary(function() { // In real life the user would now be authenticated. provisioning.setStatus(provisioning.AUTHENTICATED); // Before primaryAuthComplete is called, we reset the user caches to // force re-fetching of what could have been stale user data. user.resetCaches(); - bid.signUp.primaryAuthComplete(null, "success", function(status) { + controller.primaryAuthComplete(null, "success", function(status) { equal(status, true, "correct status"); equal($("#congrats:visible").length, 1, "success notification is visible"); start(); diff --git a/resources/static/test/cases/pages/verify_email_address_test.js b/resources/static/test/cases/pages/verify_email_address_test.js deleted file mode 100644 index c11ae2574bd2b46e071d12037a981eed2ca1907c..0000000000000000000000000000000000000000 --- a/resources/static/test/cases/pages/verify_email_address_test.js +++ /dev/null @@ -1,148 +0,0 @@ -/*jshint browsers:true, forin: true, laxbreak: true */ -/*global test: true, start: true, module: true, ok: true, equal: true, BrowserID:true */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -(function() { - "use strict"; - - var bid = BrowserID, - network = bid.Network, - storage = bid.Storage, - xhr = bid.Mocks.xhr, - testHelpers = bid.TestHelpers, - testTooltipVisible = testHelpers.testTooltipVisible, - validToken = true; - - module("pages/verify_email_address", { - setup: function() { - testHelpers.setup(); - bid.Renderer.render("#page_head", "site/verify_email_address", {}); - }, - teardown: function() { - testHelpers.teardown(); - } - }); - - asyncTest("verifyEmailAddress with good token and site", function() { - storage.setStagedOnBehalfOf("browserid.org"); - - bid.verifyEmailAddress("token", function() { - equal($("#email").val(), "testuser@testuser.com", "email set"); - ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is"); - equal($(".website:nth(0)").text(), "browserid.org", "origin is updated"); - start(); - }); - }); - - asyncTest("verifyEmailAddress with good token and nosite", function() { - $(".siteinfo").hide(); - storage.setStagedOnBehalfOf(""); - - bid.verifyEmailAddress("token", function() { - equal($("#email").val(), "testuser@testuser.com", "email set"); - equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it"); - equal($(".siteinfo .website").text(), "", "origin is not updated"); - start(); - }); - }); - - asyncTest("verifyEmailAddress with bad token", function() { - xhr.useResult("invalid"); - - bid.verifyEmailAddress("token", function() { - ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible"); - start(); - }); - }); - - asyncTest("verifyEmailAddress with emailForVerficationToken XHR failure", function() { - xhr.useResult("ajaxError"); - bid.verifyEmailAddress("token", function() { - testHelpers.testErrorVisible(); - start(); - }); - }); - - asyncTest("submit with good token, both passwords", function() { - bid.verifyEmailAddress("token", function() { - $("#password").val("password"); - $("#vpassword").val("password"); - - bid.verifyEmailAddress.submit(function() { - equal($("#congrats").is(":visible"), true, "congrats is visible, we are complete"); - start(); - }); - }); - }); - - asyncTest("submit with good token, missing password", function() { - bid.verifyEmailAddress("token", function() { - $("#password").val(""); - $("#vpassword").val("password"); - - bid.verifyEmailAddress.submit(function() { - equal($("#congrats").is(":visible"), false, "congrats is not visible, missing password"); - testTooltipVisible(); - start(); - }); - }); - }); - - asyncTest("submit with good token, too short of a password", function() { - bid.verifyEmailAddress("token", function() { - var pass = testHelpers.generateString(6); - $("#password").val(pass); - $("#vpassword").val(pass); - - bid.verifyEmailAddress.submit(function() { - equal($("#congrats").is(":visible"), false, "congrats is not visible, too short of a password"); - testTooltipVisible(); - start(); - }); - }); - }); - - asyncTest("submit with good token, too long of a password", function() { - bid.verifyEmailAddress("token", function() { - var pass = testHelpers.generateString(81); - $("#password").val(pass); - $("#vpassword").val(pass); - - bid.verifyEmailAddress.submit(function() { - equal($("#congrats").is(":visible"), false, "congrats is not visible, too long of a password"); - testTooltipVisible(); - start(); - }); - }); - }); - - asyncTest("submit with good token, missing verification password", function() { - bid.verifyEmailAddress("token"); - - - $("#password").val("password"); - $("#vpassword").val(""); - - bid.verifyEmailAddress.submit(function() { - equal($("#congrats").is(":visible"), false, "congrats is not visible, missing verification password"); - testTooltipVisible(); - start(); - }); - - }); - - asyncTest("submit with good token, different passwords", function() { - bid.verifyEmailAddress("token"); - - $("#password").val("password"); - $("#vpassword").val("pass"); - - bid.verifyEmailAddress.submit(function() { - equal($("#congrats").is(":visible"), false, "congrats is not visible, different passwords"); - testTooltipVisible(); - start(); - }); - - }); -}()); diff --git a/resources/static/test/cases/pages/add_email_address_test.js b/resources/static/test/cases/pages/verify_secondary_address.js similarity index 69% rename from resources/static/test/cases/pages/add_email_address_test.js rename to resources/static/test/cases/pages/verify_secondary_address.js index 79fe35d5e22cb38c4888550cd30c91643f728d3d..a770237db60734b31e39a6d9f82553bb917b9bc5 100644 --- a/resources/static/test/cases/pages/add_email_address_test.js +++ b/resources/static/test/cases/pages/verify_secondary_address.js @@ -11,13 +11,15 @@ xhr = bid.Mocks.xhr, dom = bid.DOM, testHelpers = bid.TestHelpers, + testHasClass = testHelpers.testHasClass, validToken = true, controller, config = { - token: "token" + token: "token", + verifyFunction: "verifyEmail" }; - module("pages/add_email_address", { + module("pages/verify_secondary_address", { setup: function() { testHelpers.setup(); bid.Renderer.render("#page_head", "site/add_email_address", {}); @@ -29,14 +31,14 @@ }); function createController(options, callback) { - controller = BrowserID.addEmailAddress.create(); + controller = BrowserID.verifySecondaryAddress.create(); options = options || {}; options.ready = callback; controller.start(options); } function expectTooltipVisible() { - xhr.useResult("needsPassword"); + xhr.useResult("mustAuth"); createController(config, function() { controller.submit(function() { testHelpers.testTooltipVisible(); @@ -46,7 +48,7 @@ } function testEmail() { - equal(dom.getInner(".email"), "testuser@testuser.com", "correct email shown"); + equal(dom.getInner("#email"), "testuser@testuser.com", "correct email shown"); } function testCannotConfirm() { @@ -71,7 +73,7 @@ testEmail(); ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is"); equal($(".website:nth(0)").text(), "browserid.org", "origin is updated"); - equal($("body").hasClass("complete"), true, "body has complete class"); + testHasClass("body", "complete"); start(); }); }); @@ -102,59 +104,21 @@ }); }); - asyncTest("password: first secondary address added", function() { - xhr.useResult("needsPassword"); - createController(config, function() { - equal($("body").hasClass("enter_password"), true, "enter_password added to body"); - testEmail(); - start(); - }); - }); - asyncTest("password: missing password", function() { $("#password").val(); - $("#vpassword").val("password"); - - expectTooltipVisible(); - }); - - asyncTest("password: missing verify password", function() { - $("#password").val("password"); - $("#vpassword").val(); - - expectTooltipVisible(); - }); - - asyncTest("password: too short of a password", function() { - $("#password").val("pass"); - $("#vpassword").val("pass"); - - expectTooltipVisible(); - }); - - asyncTest("password: too long of a password", function() { - var tooLong = testHelpers.generateString(81); - $("#password").val(tooLong); - $("#vpassword").val(tooLong); - - expectTooltipVisible(); - }); - - asyncTest("password: mismatched passwords", function() { - $("#password").val("passwords"); - $("#vpassword").val("password"); expectTooltipVisible(); }); asyncTest("password: good password", function() { $("#password").val("password"); - $("#vpassword").val("password"); + xhr.useResult("mustAuth"); createController(config, function() { + xhr.useResult("valid"); controller.submit(function(status) { equal(status, true, "correct status"); - equal($("body").hasClass("complete"), true, "body has complete class"); + testHasClass("body", "complete"); start(); }); }); @@ -162,7 +126,6 @@ asyncTest("password: good password bad token", function() { $("#password").val("password"); - $("#vpassword").val("password"); xhr.useResult("invalid"); createController(config, function() { diff --git a/resources/static/test/cases/resources/helpers.js b/resources/static/test/cases/resources/helpers.js index 279f7dee8576cfa7b24cb10fc43f6b678da5d73e..8ab3ed0a89d4127db4b6101a8a7aa2025328a103 100644 --- a/resources/static/test/cases/resources/helpers.js +++ b/resources/static/test/cases/resources/helpers.js @@ -21,7 +21,7 @@ badError = testHelpers.unexpectedXHRFailure; var controllerMock = { - close: function(message, info) { + publish: function(message, info) { closeCB && closeCB(message, info); }, @@ -116,7 +116,7 @@ xhr.useResult("unknown_secondary"); closeCB = expectedClose("user_staged", "email", "unregistered@testuser.com"); - dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) { + dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", "password", function(staged) { equal(staged, true, "user was staged"); start(); }); @@ -126,7 +126,7 @@ closeCB = badClose; xhr.useResult("throttle"); - dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) { + dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", "password", function(staged) { equal(staged, false, "user was not staged"); start(); }); @@ -136,7 +136,7 @@ errorCB = expectedError; xhr.useResult("ajaxError"); - dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess); + dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", "password", testHelpers.unexpectedSuccess); }); asyncTest("addEmail with primary email happy case, expects primary_user message", function() { @@ -148,19 +148,11 @@ }); }); - asyncTest("addEmail with unknown secondary email happy case", function() { + asyncTest("addEmail with secondary email - trigger add_email_submit_with_secondary", function() { xhr.useResult("unknown_secondary"); - closeCB = expectedClose("email_staged", "email", "unregistered@testuser.com"); - dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(status) { - ok(status, "correct status"); - start(); - }); - }); - - asyncTest("addEmail throttled", function() { - xhr.useResult("throttle"); - dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(added) { - equal(added, false, "email not added"); + closeCB = expectedClose("add_email_submit_with_secondary", "email", "unregistered@testuser.com"); + dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(success) { + equal(success, true, "success status"); start(); }); }); @@ -185,8 +177,8 @@ }); asyncTest("resetPassword happy case", function() { - closeCB = expectedClose("reset_password", "email", "registered@testuser.com"); - dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", function(reset) { + closeCB = expectedClose("password_reset", "email", "registered@testuser.com"); + dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", "password", function(reset) { ok(reset, "password reset"); start(); }); @@ -195,7 +187,7 @@ asyncTest("resetPassword throttled", function() { xhr.useResult("throttle"); - dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", function(reset) { + dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", "password", function(reset) { equal(reset, false, "password not reset"); start(); }); @@ -205,7 +197,7 @@ errorCB = expectedError; xhr.useResult("ajaxError"); - dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", function(reset) { + dialogHelpers.resetPassword.call(controllerMock, "registered@testuser.com", "password", function(reset) { ok(false, "unexpected close"); start(); }); diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js index 83e6c7cf6164e71f77b0befcd479e5a3754928ef..a8d859f1953e82c540e3723183dbc00dcd168b77 100644 --- a/resources/static/test/cases/resources/state.js +++ b/resources/static/test/cases/resources/state.js @@ -78,10 +78,44 @@ equal(error, "start: controller must be specified", "creating a state machine without a controller fails"); }); + test("new_user - call doSetPassword with correct email", function() { + mediator.publish("new_user", { email: TEST_EMAIL }); + + equal(actions.info.doSetPassword.email, TEST_EMAIL, "correct email sent to doSetPassword"); + }); + + test("cancel new user password_set flow - go back to the authentication screen", function() { + mediator.publish("authenticate"); + mediator.publish("new_user", undefined, { email: TEST_EMAIL }); + mediator.publish("password_set"); + actions.info.doAuthenticate = {}; + mediator.publish("cancel_state"); + equal(actions.info.doAuthenticate.email, TEST_EMAIL, "authenticate called with the correct email"); + }); + + test("password_set for new user - call doStageUser with correct email", function() { + mediator.publish("new_user", { email: TEST_EMAIL }); + mediator.publish("password_set"); + + equal(actions.info.doStageUser.email, TEST_EMAIL, "correct email sent to doStageUser"); + }); + + test("password_set for add secondary email - call doStageEmail with correct email", function() { + mediator.publish("add_email_submit_with_secondary", { email: TEST_EMAIL }); + mediator.publish("password_set"); + + equal(actions.info.doStageEmail.email, TEST_EMAIL, "correct email sent to doStageEmail"); + }); + + test("password_set for reset password - call doResetPassword with correct email", function() { + mediator.publish("forgot_password", { email: TEST_EMAIL }); + mediator.publish("password_set"); + + equal(actions.info.doResetPassword.email, TEST_EMAIL, "correct email sent to doResetPassword"); + }); + test("user_staged - call doConfirmUser", function() { - mediator.publish("user_staged", { - email: TEST_EMAIL - }); + mediator.publish("user_staged", { email: TEST_EMAIL }); equal(actions.info.doConfirmUser.email, TEST_EMAIL, "waiting for email confirmation for testuser@testuser.com"); }); @@ -212,13 +246,13 @@ equal(actions.info.doForgotPassword.requiredEmail, true, "correct requiredEmail passed"); }); - test("reset_password to user_confirmed - call doResetPassword then doEmailConfirmed", function() { - // reset_password indicates the user has verified that they want to reset + test("password_reset to user_confirmed - call doUserStaged then doEmailConfirmed", function() { + // password_reset indicates the user has verified that they want to reset // their password. - mediator.publish("reset_password", { + mediator.publish("password_reset", { email: TEST_EMAIL }); - equal(actions.info.doResetPassword.email, TEST_EMAIL, "reset password with the correct email"); + equal(actions.info.doConfirmUser.email, TEST_EMAIL, "doConfirmUser with the correct email"); // At this point the user should be displayed the "go confirm your address" // screen. @@ -236,13 +270,13 @@ }); - test("cancel reset_password flow - go two steps back", function() { + test("cancel password_reset flow - go two steps back", function() { // we want to skip the "verify" screen of reset password and instead go two // screens back. Do do this, we are simulating the steps necessary to get - // to the reset_password flow. + // to the password_reset flow. mediator.publish("authenticate"); mediator.publish("forgot_password", undefined, { email: TEST_EMAIL }); - mediator.publish("reset_password", { email: TEST_EMAIL }); + mediator.publish("password_reset"); actions.info.doAuthenticate = {}; mediator.publish("cancel_state"); equal(actions.info.doAuthenticate.email, TEST_EMAIL, "authenticate called with the correct email"); @@ -451,4 +485,35 @@ equal(actions.info.doPickEmail.tosURL, "http://example.com/tos.html", "tosURL preserved"); }); + test("add_email - call doAddEmail", function() { + mediator.publish("add_email", { + complete: function() { + equal(actions.called.doAddEmail, true, "doAddEmail called"); + start(); + } + }); + }); + + test("add_email_submit_with_secondary - first secondary email - call doSetPassword", function() { + mediator.publish("add_email", { + complete: function() { + equal(actions.called.doSetPassword, true, "doSetPassword called"); + start(); + } + }); + }); + + + test("add_email_submit_with_secondary - second secondary email - call doStageEmail", function() { + storage.addSecondaryEmail("testuser@testuser.com"); + + mediator.publish("add_email", { + complete: function() { + equal(actions.called.doStageEmail, true, "doStageEmail called"); + start(); + } + }); + }); + + }()); diff --git a/resources/static/test/cases/shared/helpers.js b/resources/static/test/cases/shared/helpers.js index 8ebdd3f7dd92c4d01e0ad85ad3785759df892254..c29b406421024f1d23dfbc8ba8b0ab994b4c9a37 100644 --- a/resources/static/test/cases/shared/helpers.js +++ b/resources/static/test/cases/shared/helpers.js @@ -13,7 +13,7 @@ module("shared/helpers", { setup: function() { testHelpers.setup(); - bid.Renderer.render("#page_head", "site/add_email_address", {}); + bid.Renderer.render("#page_head", "site/signin", {}); }, teardown: function() { diff --git a/resources/static/test/cases/shared/network.js b/resources/static/test/cases/shared/network.js index 1fc02f083dcd60aa6bec431d9312f8faed97066e..e98a049029126e6eb623820d7a95fae563f7a7ea 100644 --- a/resources/static/test/cases/shared/network.js +++ b/resources/static/test/cases/shared/network.js @@ -11,7 +11,9 @@ transport = bid.Mocks.xhr, testHelpers = bid.TestHelpers, TEST_EMAIL = "testuser@testuser.com", - failureCheck = testHelpers.failureCheck; + TEST_PASSWORD = "password", + failureCheck = testHelpers.failureCheck, + testObjectValuesEqual = testHelpers.testObjectValuesEqual; var network = BrowserID.Network; @@ -172,6 +174,13 @@ }, testHelpers.unexpectedXHRFailure); }); + asyncTest("completeEmailRegistration with valid token, missing password", function() { + transport.useResult("missing_password"); + network.completeEmailRegistration("token", undefined, + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure); + }); + asyncTest("completeEmailRegistration with invalid token", function() { transport.useResult("invalid"); network.completeEmailRegistration("badtoken", "password", function onSuccess(proven) { @@ -185,7 +194,7 @@ }); asyncTest("createUser with valid user", function() { - network.createUser("validuser", "origin", function onSuccess(created) { + network.createUser("validuser", "password", "origin", function onSuccess(created) { ok(created); start(); }, testHelpers.unexpectedFailure); @@ -193,7 +202,7 @@ asyncTest("createUser with invalid user", function() { transport.useResult("invalid"); - network.createUser("invaliduser", "origin", function onSuccess(created) { + network.createUser("invaliduser", "password", "origin", function onSuccess(created) { equal(created, false); start(); }, testHelpers.unexpectedFailure); @@ -202,14 +211,14 @@ asyncTest("createUser throttled", function() { transport.useResult("throttle"); - network.createUser("validuser", "origin", function onSuccess(added) { + network.createUser("validuser", "password", "origin", function onSuccess(added) { equal(added, false, "throttled email returns onSuccess but with false as the value"); start(); }, testHelpers.unexpectedFailure); }); asyncTest("createUser with XHR failure", function() { - failureCheck(network.createUser, "validuser", "origin"); + failureCheck(network.createUser, "validuser", "password", "origin"); }); asyncTest("checkUserRegistration returns pending - pending status, user is not logged in", function() { @@ -264,7 +273,21 @@ failureCheck(network.checkUserRegistration, "registered@testuser.com"); }); - asyncTest("completeUserRegistration with valid token", function() { + asyncTest("completeUserRegistration with valid token, no password required", function() { + network.completeUserRegistration("token", undefined, function(registered) { + ok(registered); + start(); + }, testHelpers.unexpectedFailure); + }); + + asyncTest("completeUserRegistration with valid token, missing password", function() { + transport.useResult("missing_password"); + network.completeUserRegistration("token", undefined, + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure); + }); + + asyncTest("completeUserRegistration with valid token, password required", function() { network.completeUserRegistration("token", "password", function(registered) { ok(registered); start(); @@ -327,7 +350,7 @@ asyncTest("addSecondaryEmail valid", function() { - network.addSecondaryEmail("address", "origin", function onSuccess(added) { + network.addSecondaryEmail(TEST_EMAIL, TEST_PASSWORD, "origin", function onSuccess(added) { ok(added); start(); }, testHelpers.unexpectedFailure); @@ -335,7 +358,7 @@ asyncTest("addSecondaryEmail invalid", function() { transport.useResult("invalid"); - network.addSecondaryEmail("address", "origin", function onSuccess(added) { + network.addSecondaryEmail(TEST_EMAIL, TEST_PASSWORD, "origin", function onSuccess(added) { equal(added, false); start(); }, testHelpers.unexpectedFailure); @@ -344,14 +367,14 @@ asyncTest("addSecondaryEmail throttled", function() { transport.useResult("throttle"); - network.addSecondaryEmail("address", "origin", function onSuccess(added) { + network.addSecondaryEmail(TEST_EMAIL, TEST_PASSWORD, "origin", function onSuccess(added) { equal(added, false, "throttled email returns onSuccess but with false as the value"); start(); }, testHelpers.unexpectedFailure); }); asyncTest("addSecondaryEmail with XHR failure", function() { - failureCheck(network.addSecondaryEmail, "address", "origin"); + failureCheck(network.addSecondaryEmail, TEST_EMAIL, TEST_PASSWORD, "origin"); }); asyncTest("checkEmailRegistration pending", function() { @@ -377,7 +400,7 @@ }); asyncTest("checkEmailRegistration with XHR failure", function() { - failureCheck(network.checkEmailRegistration, "address"); + failureCheck(network.checkEmailRegistration, TEST_EMAIL); }); @@ -415,12 +438,11 @@ }, testHelpers.unexpectedXHRFailure); }); - asyncTest("emailForVerificationToken that needs password - returns needs_password and email address", function() { - transport.useResult("needsPassword"); + asyncTest("emailForVerificationToken that must authenticate - returns must_auth and email address", function() { + transport.useResult("mustAuth"); network.emailForVerificationToken("token", function(result) { - equal(result.needs_password, true, "needs_password correctly set to true"); - equal(result.email, "testuser@testuser.com", "email address correctly added"); + testObjectValuesEqual(result, { must_auth: true, email: TEST_EMAIL }); start(); }, testHelpers.unexpectedXHRFailure); }); @@ -428,7 +450,7 @@ asyncTest("emailForVerificationToken that does not need password", function() { network.emailForVerificationToken("token", function(result) { equal(result.needs_password, false, "needs_password correctly set to false"); - equal(result.email, "testuser@testuser.com", "email address correctly added"); + equal(result.email, TEST_EMAIL, "email address correctly added"); start(); }, testHelpers.unexpectedXHRFailure); }); @@ -456,16 +478,15 @@ }); - asyncTest("requestPasswordReset", function() { - network.requestPasswordReset("address", "origin", function onSuccess() { - // XXX need a test here; - ok(true); + asyncTest("requestPasswordReset - true status", function() { + network.requestPasswordReset(TEST_EMAIL, "password", "origin", function onSuccess(status) { + equal(status, true, "password reset request success"); start(); }, testHelpers.unexpectedFailure); }); asyncTest("requestPasswordReset with XHR failure", function() { - failureCheck(network.requestPasswordReset, "address", "origin"); + failureCheck(network.requestPasswordReset, TEST_EMAIL, "password", "origin"); }); asyncTest("setPassword happy case expects true status", function() { @@ -596,7 +617,7 @@ }); asyncTest("prolongSession with authenticated user, success - call complete", function() { - network.authenticate("testuser@testuser.com", "password", function() { + network.authenticate(TEST_EMAIL, "password", function() { network.prolongSession(function() { ok(true, "prolongSession completed"); start(); diff --git a/resources/static/test/cases/shared/storage.js b/resources/static/test/cases/shared/storage.js index c6f459e6264d103e8fc589b9479047dc287e0fb6..b9b230b5e362aacd4174960d173810eabea5faa7 100644 --- a/resources/static/test/cases/shared/storage.js +++ b/resources/static/test/cases/shared/storage.js @@ -34,6 +34,20 @@ equal("key", id.priv, "email that was added is retrieved"); }); + test("addPrimaryEmail", function() { + storage.addPrimaryEmail("testuser@testuser.com"); + + var email = storage.getEmail("testuser@testuser.com"); + equal(email.type, "primary", "email type set correctly"); + }); + + test("addSecondaryEmail", function() { + storage.addSecondaryEmail("testuser@testuser.com"); + + var email = storage.getEmail("testuser@testuser.com"); + equal(email.type, "secondary", "email type set correctly"); + }); + test("removeEmail, getEmails", function() { storage.addEmail("testuser@testuser.com", {priv: "key"}); storage.removeEmail("testuser@testuser.com"); diff --git a/resources/static/test/cases/shared/user.js b/resources/static/test/cases/shared/user.js index a1269fb9305ca913aa41344474b19a4f1f3b52a8..455736004a20fc4b675c16c70b135ba6d6b5fc01 100644 --- a/resources/static/test/cases/shared/user.js +++ b/resources/static/test/cases/shared/user.js @@ -132,53 +132,32 @@ var vep = require("./vep"); equal(0, count, "after clearing, there are no identities"); }); - asyncTest("createSecondaryUser", function() { - lib.createSecondaryUser(TEST_EMAIL, function(status) { + asyncTest("createSecondaryUser success - callback with true status", function() { + lib.createSecondaryUser(TEST_EMAIL, "password", function(status) { ok(status, "user created"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("createSecondaryUser with user creation refused", function() { + asyncTest("createSecondaryUser throttled - callback with false status", function() { xhr.useResult("throttle"); - lib.createSecondaryUser(TEST_EMAIL, function(status) { + lib.createSecondaryUser(TEST_EMAIL, "password", function(status) { equal(status, false, "user creation refused"); start(); }, testHelpers.unexpectedXHRFailure); }); asyncTest("createSecondaryUser with XHR failure", function() { - failureCheck(lib.createSecondaryUser, TEST_EMAIL); + failureCheck(lib.createSecondaryUser, TEST_EMAIL, "password"); }); - asyncTest("createUser with unknown secondary happy case - expect 'secondary.verify'", function() { - xhr.useResult("unknown_secondary"); - - lib.createUser("unregistered@testuser.com", function(status) { - equal(status, "secondary.verify", "secondary user must be verified"); - start(); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("createUser with unknown secondary, throttled - expect status='secondary.could_not_add'", function() { - xhr.useResult("throttle"); - lib.createUser("unregistered@testuser.com", function(status) { - equal(status, "secondary.could_not_add", "user creation refused"); - start(); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("createUser with unknown secondary, XHR failure - expect failure call", function() { - failureCheck(lib.createUser, "unregistered@testuser.com"); - }); - - asyncTest("createUser with primary, user verified with primary - expect 'primary.verified'", function() { + asyncTest("createPrimaryUser with primary, user verified with primary - expect 'primary.verified'", function() { xhr.useResult("primary"); provisioning.setStatus(provisioning.AUTHENTICATED); - lib.createUser("unregistered@testuser.com", function(status) { + lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) { equal(status, "primary.verified", "primary user is already verified, correct status"); network.checkAuth(function(authenticated) { equal(authenticated, "assertion", "after provisioning user, user should be automatically authenticated to BrowserID"); @@ -187,33 +166,28 @@ var vep = require("./vep"); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("createUser with primary, user must authenticate with primary - expect 'primary.verify'", function() { + asyncTest("createPrimaryUser with primary, user must authenticate with primary - expect 'primary.verify'", function() { xhr.useResult("primary"); - lib.createUser("unregistered@testuser.com", function(status) { + lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) { equal(status, "primary.verify", "primary must verify with primary, correct status"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("createUser with primary, unknown provisioning failure, expect XHR failure callback", function() { + asyncTest("createPrimaryUser with primary, unknown provisioning failure, expect XHR failure callback", function() { xhr.useResult("primary"); provisioning.setFailure({ code: "primaryError", msg: "some error" }); - lib.createUser("unregistered@testuser.com", + lib.createPrimaryUser({email: "unregistered@testuser.com"}, testHelpers.unexpectedSuccess, testHelpers.expectedXHRFailure ); }); - asyncTest("createUserWithInfo", function() { - ok(true, "For development speed and reduced duplication of tests, tested via createUser"); - start(); - }); - asyncTest("provisionPrimaryUser authenticated with IdP, expect primary.verified", function() { xhr.useResult("primary"); provisioning.setStatus(provisioning.AUTHENTICATED); @@ -489,24 +463,24 @@ var vep = require("./vep"); ); }); - asyncTest("requestPasswordReset with known email", function() { - lib.requestPasswordReset("registered@testuser.com", function(status) { + asyncTest("requestPasswordReset with known email - true status", function() { + lib.requestPasswordReset("registered@testuser.com", "password", function(status) { equal(status.success, true, "password reset for known user"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("requestPasswordReset with unknown email", function() { - lib.requestPasswordReset("unregistered@testuser.com", function(status) { + asyncTest("requestPasswordReset with unknown email - false status, invalid_user", function() { + lib.requestPasswordReset("unregistered@testuser.com", "password", function(status) { equal(status.success, false, "password not reset for unknown user"); equal(status.reason, "invalid_user", "invalid_user is the reason"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("requestPasswordReset with throttle", function() { + asyncTest("requestPasswordReset with throttle - false status, throttle", function() { xhr.useResult("throttle"); - lib.requestPasswordReset("registered@testuser.com", function(status) { + lib.requestPasswordReset("registered@testuser.com", "password", function(status) { equal(status.success, false, "password not reset for throttle"); equal(status.reason, "throttle", "password reset was throttled"); start(); @@ -514,7 +488,7 @@ var vep = require("./vep"); }); asyncTest("requestPasswordReset with XHR failure", function() { - failureCheck(lib.requestPasswordReset, "registered@testuser.com"); + failureCheck(lib.requestPasswordReset, "registered@testuser.com", "password"); }); @@ -614,8 +588,36 @@ var vep = require("./vep"); failureCheck(lib.isEmailRegistered, "registered"); }); + asyncTest("passwordNeededToAddSecondaryEmail, account only has primaries - call callback with true", function() { + storage.addEmail("testuser@testuser.com", { type: "primary" }); + + lib.passwordNeededToAddSecondaryEmail(function(passwordNeeded) { + equal(passwordNeeded, true, "password correctly needed"); + start(); + }); + }); + + asyncTest("passwordNeededToAddSecondaryEmail, account already has secondary - call callback with false", function() { + storage.addEmail("testuser@testuser.com", { type: "secondary" }); + + lib.passwordNeededToAddSecondaryEmail(function(passwordNeeded) { + equal(passwordNeeded, false, "password not needed"); + start(); + }); + }); + + asyncTest("passwordNeededToAddSecondaryEmail, mix of types - call callback with false", function() { + storage.addEmail("testuser@testuser.com", { type: "primary" }); + storage.addEmail("testuser1@testuser.com", { type: "secondary" }); + + lib.passwordNeededToAddSecondaryEmail(function(passwordNeeded) { + equal(passwordNeeded, false, "password not needed"); + start(); + }); + }); + asyncTest("addEmail", function() { - lib.addEmail("testemail@testemail.com", function(added) { + lib.addEmail("testemail@testemail.com", "password", function(added) { ok(added, "user was added"); var identities = lib.getStoredEmailKeypairs(); @@ -630,7 +632,7 @@ var vep = require("./vep"); asyncTest("addEmail with addition refused", function() { xhr.useResult("throttle"); - lib.addEmail("testemail@testemail.com", function(added) { + lib.addEmail("testemail@testemail.com", "password", function(added) { equal(added, false, "user addition was refused"); var identities = lib.getStoredEmailKeypairs(); @@ -643,7 +645,7 @@ var vep = require("./vep"); }); asyncTest("addEmail with XHR failure", function() { - failureCheck(lib.addEmail, "testemail@testemail.com"); + failureCheck(lib.addEmail, "testemail@testemail.com", "password"); }); @@ -713,42 +715,9 @@ var vep = require("./vep"); }, 500); }); - asyncTest("verifyEmailNoPassword with a good token - callback with email, orgiin, and valid", function() { - storage.setStagedOnBehalfOf(testOrigin); - lib.verifyEmailNoPassword("token", function onSuccess(info) { - - ok(info.valid, "token was valid"); - equal(info.email, TEST_EMAIL, "email part of info"); - equal(info.origin, testOrigin, "origin in info"); - equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed"); - - start(); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("verifyEmailNoPassword with a bad token - callback with valid: false", function() { - xhr.useResult("invalid"); - - lib.verifyEmailNoPassword("token", function onSuccess(info) { - equal(info.valid, false, "bad token calls onSuccess with a false validity"); - - start(); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("verifyEmailNoPassword with an XHR failure", function() { - xhr.useResult("ajaxError"); - - lib.verifyEmailNoPassword( - "token", - testHelpers.unexpectedSuccess, - testHelpers.expectedXHRFailure - ); - }); - - asyncTest("verifyEmailWithPassword with a good token - callback with email, origin, valid", function() { + asyncTest("verifyEmail with a good token - callback with email, origin, valid", function() { storage.setStagedOnBehalfOf(testOrigin); - lib.verifyEmailWithPassword("token", "password", function onSuccess(info) { + lib.verifyEmail("token", "password", function onSuccess(info) { ok(info.valid, "token was valid"); equal(info.email, TEST_EMAIL, "email part of info"); @@ -759,20 +728,20 @@ var vep = require("./vep"); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("verifyEmailWithPassword with a bad token - callback with valid: false", function() { + asyncTest("verifyEmail with a bad token - callback with valid: false", function() { xhr.useResult("invalid"); - lib.verifyEmailWithPassword("token", "password", function onSuccess(info) { + lib.verifyEmail("token", "password", function onSuccess(info) { equal(info.valid, false, "bad token calls onSuccess with a false validity"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("verifyEmailWithPassword with an XHR failure", function() { + asyncTest("verifyEmail with an XHR failure", function() { xhr.useResult("ajaxError"); - lib.verifyEmailWithPassword( + lib.verifyEmail( "token", "password", testHelpers.unexpectedSuccess, diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js index d9048e7bfd80a82e99b6b6f8525c8c0e1c2f7630..8b505f997d4680903d922e8de6e2a90d78f08cc1 100644 --- a/resources/static/test/mocks/xhr.js +++ b/resources/static/test/mocks/xhr.js @@ -33,7 +33,7 @@ BrowserID.Mocks.xhr = (function() { // the flag contextAjaxError. "get /wsapi/session_context contextAjaxError": undefined, "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" }, - "get /wsapi/email_for_token?token=token needsPassword": { email: "testuser@testuser.com", needs_password: true }, + "get /wsapi/email_for_token?token=token mustAuth": { email: "testuser@testuser.com", must_auth: true }, "get /wsapi/email_for_token?token=token invalid": { success: false }, "post /wsapi/authenticate_user valid": { success: true, userid: 1 }, "post /wsapi/authenticate_user invalid": { success: false }, @@ -47,6 +47,7 @@ BrowserID.Mocks.xhr = (function() { "post /wsapi/cert_key invalid": undefined, "post /wsapi/cert_key ajaxError": undefined, "post /wsapi/complete_email_addition valid": { success: true }, + "post /wsapi/complete_email_addition missing_password": 401, "post /wsapi/complete_email_addition invalid": { success: false }, "post /wsapi/complete_email_addition ajaxError": undefined, "post /wsapi/stage_user unknown_secondary": { success: true }, @@ -60,6 +61,7 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/user_creation_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" }, "get /wsapi/user_creation_status?email=registered%40testuser.com ajaxError": undefined, "post /wsapi/complete_user_creation valid": { success: true }, + "post /wsapi/complete_user_creation missing_password": 401, "post /wsapi/complete_user_creation invalid": { success: false }, "post /wsapi/complete_user_creation ajaxError": undefined, "post /wsapi/logout valid": { success: true }, @@ -69,6 +71,7 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/have_email?email=registered%40testuser.com throttle": { email_known: true }, "get /wsapi/have_email?email=registered%40testuser.com ajaxError": undefined, "get /wsapi/have_email?email=unregistered%40testuser.com valid": { email_known: false }, + "get /wsapi/have_email?email=unregistered%40testuser.com primary": { email_known: false }, "post /wsapi/remove_email valid": { success: true }, "post /wsapi/remove_email invalid": { success: false }, "post /wsapi/remove_email multiple": { success: true }, @@ -105,6 +108,7 @@ BrowserID.Mocks.xhr = (function() { "post /wsapi/update_password invalid": undefined, "get /wsapi/address_info?email=unregistered%40testuser.com invalid": undefined, "get /wsapi/address_info?email=unregistered%40testuser.com throttle": { type: "secondary", known: false }, + "get /wsapi/address_info?email=unregistered%40testuser.com valid": { type: "secondary", known: false }, "get /wsapi/address_info?email=unregistered%40testuser.com unknown_secondary": { type: "secondary", known: false }, "get /wsapi/address_info?email=registered%40testuser.com known_secondary": { type: "secondary", known: true }, "get /wsapi/address_info?email=registered%40testuser.com primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" }, diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js index 910db36bd27f94bb2265896c3c2e103077c7a510..287ba8393c039d0519f9c892fcc79495f62d051f 100644 --- a/resources/static/test/testHelpers/helpers.js +++ b/resources/static/test/testHelpers/helpers.js @@ -29,7 +29,7 @@ BrowserID.TestHelpers = (function() { } calls[msg] = true; - cb && cb(msg, info); + cb && cb.apply(null, arguments); })); } @@ -203,6 +203,17 @@ BrowserID.TestHelpers = (function() { for(var i=0, key; key=expected[i]; ++i) { ok(key in objToTest, msg || ("object contains " + key)); } + }, + + testObjectValuesEqual: function(objToTest, expected, msg) { + for(var key in expected) { + equal(objToTest[key], expected[key], key + " set to: " + expected[key] + (msg ? " - " + msg : "")); + } + }, + + testHasClass: function(selector, className, msg) { + ok($(selector).hasClass(className), + selector + " has className " + className + " - " + msg); } }; diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs index 979cea68aa1953636faecadd5c2370d81a366722..fa6fbd891b20f14c23532b9497da17adf3127e16 100644 --- a/resources/views/add_email_address.ejs +++ b/resources/views/add_email_address.ejs @@ -14,14 +14,14 @@ <h1 class="serif"><%= gettext('Email Verification') %></h1> - <ul class="inputs password_entry"> + <ul class="inputs"> <li> <label class="serif" for="email"><%= gettext('Email Address') %></label> - <input class="youraddress sans email" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" /> + <input class="youraddress sans" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" /> </li> - <li> - <label class="serif" for="password"><%= gettext('New Password') %></label> - <input class="sans" id="password" placeholder="<%= gettext('Enter a Password') %>" type="password" autofocus maxlength=80 /> + <li class="password_entry"> + <label class="serif" for="password"><%= gettext('Password') %></label> + <input class="sans" id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 /> <div class="tooltip" id="password_required" for="password"> <%= gettext('Password is required.') %> @@ -31,18 +31,6 @@ <%= gettext('Password must be between 8 and 80 characters long.') %> </div> </li> - <li> - <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label> - <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength=80 /> - - <div class="tooltip" id="vpassword_required" for="vpassword"> - <%= gettext('Verification password is required.') %> - </div> - - <div class="tooltip" id="passwords_no_match" for="vpassword"> - <%= gettext('Passwords do not match.') %> - </div> - </li> </ul> <div class="submit cf password_entry"> diff --git a/resources/views/forgot.ejs b/resources/views/forgot.ejs index f249dc610c694a19aaf15ed8c894aab0b6cad483..fea0f0c37a75d4f66421a65586bebf887bb0756c 100644 --- a/resources/views/forgot.ejs +++ b/resources/views/forgot.ejs @@ -46,6 +46,36 @@ </div> </li> + <li> + <label class="serif" for="password">Password</label> + <input class="sans" id="password" placeholder="Password" type="password" maxlength="80"> + + <div id="password_required" class="tooltip" for="password"> + Password is required. + </div> + + <div class="tooltip" id="password_length" for="password"> + Password must be between 8 and 80 characters long. + </div> + + <div id="could_not_add" class="tooltip" for="password"> + We just sent an email to that address! If you really want to send another, wait a minute or two and try again. + </div> + </li> + + <li> + <label class="serif" for="vpassword">Verify Password</label> + <input class="sans" id="vpassword" placeholder="Repeat Password" type="password" maxlength="80"> + + <div id="password_required" class="tooltip" for="vpassword"> + Verification password is required. + </div> + + <div class="tooltip" id="passwords_no_match" for="vpassword"> + Passwords do not match. + </div> + + </li> </ul> diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs index 51b280608d0900893cf11ab0a38532e94bd25328..6e786017f8f383c8af4415c7b49de23fb741a312 100644 --- a/resources/views/signup.ejs +++ b/resources/views/signup.ejs @@ -50,6 +50,37 @@ We just sent an email to that address! If you really want to send another, wait a minute or two and try again. </div> </li> + + <li class="password_entry"> + <label class="serif" for="password">Password</label> + <input class="sans" id="password" placeholder="Password" type="password" tabindex="2" maxlength="80"> + + <div id="password_required" class="tooltip" for="password"> + Password is required. + </div> + + <div class="tooltip" id="password_length" for="password"> + Password must be between 8 and 80 characters long. + </div> + + <div id="could_not_add" class="tooltip" for="password"> + We just sent an email to that address! If you really want to send another, wait a minute or two and try again. + </div> + </li> + + <li class="password_entry"> + <label class="serif" for="vpassword">Verify Password</label> + <input class="sans" id="vpassword" placeholder="Repeat Password" type="password" tabindex="2" maxlength="80"> + + <div id="password_required" class="tooltip" for="vpassword"> + Verification password is required. + </div> + + <div class="tooltip" id="passwords_no_match" for="vpassword"> + Passwords do not match. + </div> + + </li> </ul> <div class="submit cf forminputs"> diff --git a/resources/views/test.ejs b/resources/views/test.ejs index 3d55b817e19316d0226c033ec9dbec85ddc13020..4e0e1960d5644540dbf57728692c58e0b0e1f22d 100644 --- a/resources/views/test.ejs +++ b/resources/views/test.ejs @@ -120,21 +120,20 @@ <script src="/dialog/controllers/dialog.js"></script> <script src="/dialog/controllers/check_registration.js"></script> <script src="/dialog/controllers/authenticate.js"></script> - <script src="/dialog/controllers/forgot_password.js"></script> <script src="/dialog/controllers/required_email.js"></script> <script src="/dialog/controllers/verify_primary_user.js"></script> <script src="/dialog/controllers/generate_assertion.js"></script> <script src="/dialog/controllers/provision_primary_user.js"></script> <script src="/dialog/controllers/primary_user_provisioned.js"></script> <script src="/dialog/controllers/is_this_your_computer.js"></script> + <script src="/dialog/controllers/set_password.js"></script> <script src="/pages/page_helpers.js"></script> - <script src="/pages/add_email_address.js"></script> + <script src="/pages/verify_secondary_address.js"></script> <script src="/pages/forgot.js"></script> <script src="/pages/manage_account.js"></script> <script src="/pages/signin.js"></script> <script src="/pages/signup.js"></script> - <script src="/pages/verify_email_address.js"></script> <script src="testHelpers/helpers.js"></script> @@ -164,8 +163,7 @@ <script src="cases/pages/browserid.js"></script> <script src="cases/pages/page_helpers.js"></script> - <script src="cases/pages/add_email_address_test.js"></script> - <script src="cases/pages/verify_email_address_test.js"></script> + <script src="cases/pages/verify_secondary_address.js"></script> <script src="cases/pages/forgot.js"></script> <script src="cases/pages/signin.js"></script> <script src="cases/pages/signup.js"></script> @@ -180,13 +178,13 @@ <script src="cases/controllers/add_email.js"></script> <script src="cases/controllers/check_registration.js"></script> <script src="cases/controllers/authenticate.js"></script> - <script src="cases/controllers/forgot_password.js"></script> <script src="cases/controllers/required_email.js"></script> <script src="cases/controllers/verify_primary_user.js"></script> <script src="cases/controllers/generate_assertion.js"></script> <script src="cases/controllers/provision_primary_user.js"></script> <script src="cases/controllers/primary_user_provisioned.js"></script> <script src="cases/controllers/is_this_your_computer.js"></script> + <script src="cases/controllers/set_password.js"></script> <!-- must go last or all other tests will fail. --> <script src="cases/controllers/dialog.js"></script> diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs index e86a1f5b6a41afa004449cae41925c9636ca6104..a73c80ead95230830d13bc8f218e5e84e878c314 100644 --- a/resources/views/verify_email_address.ejs +++ b/resources/views/verify_email_address.ejs @@ -6,7 +6,6 @@ <div id="signUpFormWrap"> <ul class="notifications"> <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li> - <li class="notification error" id="cannotcommunicate"><%= gettext('Error communicating with server.') %></li> <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li> </ul> @@ -19,9 +18,9 @@ <label class="serif" for="email"><%= gettext('Email Address') %></label> <input class="youraddress sans" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" /> </li> - <li> - <label class="serif" for="password"><%= gettext('New Password') %></label> - <input class="sans" id="password" placeholder="<%= gettext('Enter a Password') %>" type="password" autofocus maxlength=80 /> + <li class="password_entry"> + <label class="serif" for="password"><%= gettext('Password') %></label> + <input class="sans" id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 /> <div class="tooltip" id="password_required" for="password"> <%= gettext('Password is required.') %> @@ -31,21 +30,9 @@ <%= gettext('Password must be between 8 and 80 characters long.') %> </div> </li> - <li> - <label class="serif" for="vpassword"><%= gettext('Verify Password') %></label> - <input class="sans" id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength=80 /> - - <div class="tooltip" id="vpassword_required" for="vpassword"> - <%= gettext('Verification password is required.') %> - </div> - - <div class="tooltip" id="passwords_no_match" for="vpassword"> - <%= gettext('Passwords do not match.') %> - </div> - </li> </ul> - <div class="submit cf"> + <div class="submit cf password_entry"> <button><%= gettext('finish') %></button> </div> diff --git a/scripts/deploy/vm.js b/scripts/deploy/vm.js index 5508d1b790e98e7af3354a90304f8bba6864f020..c637fd27abcc4039616ee2e7f9d6145c3af7a8a1 100644 --- a/scripts/deploy/vm.js +++ b/scripts/deploy/vm.js @@ -4,7 +4,7 @@ jsel = require('JSONSelect'), key = require('./key.js'), sec = require('./sec.js'); -const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-c38f57aa'; +const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-c2a401ab'; function extractInstanceDeets(horribleBlob) { var instance = {}; diff --git a/tests/add-email-with-assertion-test.js b/tests/add-email-with-assertion-test.js index aca86c04c874d112f634a5a0b13d8e1bbb992dbb..19a58f0ae405b8c595101759dea3e47c14d5997e 100755 --- a/tests/add-email-with-assertion-test.js +++ b/tests/add-email-with-assertion-test.js @@ -112,6 +112,7 @@ suite.addBatch({ "stage an account": { topic: wsapi.post('/wsapi/stage_user', { email: TEST_FIRST_ACCT, + pass: 'fakepass', site:'http://fakesite.com:652' }), "works": function(err, r) { @@ -126,7 +127,7 @@ suite.addBatch({ }, "can be used": { topic: function(token) { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'fakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "to verify email ownership": function(err, r) { assert.equal(r.code, 200); diff --git a/tests/cert-emails-test.js b/tests/cert-emails-test.js index 3bfba17e5ef1e0ffe4d14fdca5acd4a66c151937..a8f746f568d1fd78242098618fe9c393984a77e2 100755 --- a/tests/cert-emails-test.js +++ b/tests/cert-emails-test.js @@ -32,7 +32,7 @@ suite.addBatch({ "staging an account": { topic: wsapi.post('/wsapi/stage_user', { email: 'syncer@somehost.com', - pubkey: 'fakekey', + pass: 'fakepass', site:'http://fakesite.com' }), "succeeds": function(err, r) { @@ -57,7 +57,7 @@ suite.addBatch({ suite.addBatch({ "verifying account ownership": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'fakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "works": function(err, r) { assert.equal(r.code, 200); diff --git a/tests/db-test.js b/tests/db-test.js index 65abaffa2926f260fd0157a7ee27dbf9f36d5565..6c8ec1ea54b157fb05f29e74918049d7fd4cead2 100755 --- a/tests/db-test.js +++ b/tests/db-test.js @@ -73,7 +73,7 @@ suite.addBatch({ suite.addBatch({ "stage a user for creation pending verification": { topic: function() { - db.stageUser('lloyd@nowhe.re', this.callback); + db.stageUser('lloyd@nowhe.re', 'biglonghashofapassword', this.callback); }, "staging returns a valid secret": function(err, r) { assert.isNull(err); @@ -85,8 +85,8 @@ suite.addBatch({ topic: function(err, secret) { db.emailForVerificationSecret(secret, this.callback); }, - "matches expected email": function(err, r) { - assert.strictEqual(r.email, 'lloyd@nowhe.re'); + "matches expected email": function(err, email, uid) { + assert.strictEqual(email, 'lloyd@nowhe.re'); } }, "fetch secret for email": { @@ -125,7 +125,7 @@ suite.addBatch({ suite.addBatch({ "upon receipt of a secret": { topic: function() { - db.gotVerificationSecret(secret, 'fakepasswordhash', this.callback); + db.gotVerificationSecret(secret, this.callback); }, "gotVerificationSecret completes without error": function (err, r) { assert.isNull(err); @@ -164,7 +164,7 @@ suite.addBatch({ }, "the correct password": function(err, r) { assert.isNull(err); - assert.strictEqual(r, "fakepasswordhash"); + assert.strictEqual(r, "biglonghashofapassword"); } } }); @@ -200,7 +200,7 @@ suite.addBatch({ }, "then staging an email": { topic: function(err, uid) { - db.stageEmail(uid, 'lloyd@somewhe.re', this.callback); + db.stageEmail(uid, 'lloyd@somewhe.re', 'biglonghashofapassword', this.callback); }, "yields a valid secret": function(err, secret) { assert.isNull(err); @@ -215,7 +215,7 @@ suite.addBatch({ "makes it visible via isStaged": function(sekret, r) { assert.isTrue(r); }, "lets you verify it": { topic: function(secret, r) { - db.gotVerificationSecret(secret, undefined, this.callback); + db.gotVerificationSecret(secret, this.callback); }, "successfully": function(err, r) { assert.isNull(err); diff --git a/tests/email-throttling-test.js b/tests/email-throttling-test.js index 4cceed04d5c83ee9c07d7a4e9bd3d87586d5207a..9b7e4560cfe8149e06d57272d946c9e042d3ee5d 100755 --- a/tests/email-throttling-test.js +++ b/tests/email-throttling-test.js @@ -24,6 +24,7 @@ suite.addBatch({ "staging a registration": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', + pass: 'firstfakepass', site:'https://fakesite.com:443' }), "returns 200": function(err, r) { @@ -49,6 +50,7 @@ suite.addBatch({ "immediately staging another": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', + pass: 'firstfakepass', site:'http://fakesite.com:80' }), "is throttled": function(err, r) { @@ -60,7 +62,7 @@ suite.addBatch({ suite.addBatch({ "finishing creating the first account": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "works": function(err, r) { assert.equal(r.code, 200); diff --git a/tests/forgotten-email-test.js b/tests/forgotten-email-test.js index d08c98dad2cd25a056a07e14afa70443cdbbb6ef..c2e6aa3823be90f8c8093e013cc5697f09d6e67e 100755 --- a/tests/forgotten-email-test.js +++ b/tests/forgotten-email-test.js @@ -25,6 +25,7 @@ suite.addBatch({ "staging an account": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', + pass: 'firstfakepass', site:'http://localhost:123' }), "works": function(err, r) { @@ -49,7 +50,7 @@ suite.addBatch({ suite.addBatch({ "create first account": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "account created": function(err, r) { assert.equal(r.code, 200); @@ -137,6 +138,7 @@ suite.addBatch({ "re-stage first account": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', + pass: 'secondfakepass', site:'https://otherfakesite.com' }), "works": function(err, r) { @@ -187,7 +189,7 @@ suite.addBatch({ suite.addBatch({ "re-create first email address": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'secondfakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "account created": function(err, r) { assert.equal(r.code, 200); @@ -196,7 +198,7 @@ suite.addBatch({ } }); -// now we should be able to sign into the first email address with the second +// now we should be able to sign into the first email address with the first // password, and all other combinations should fail suite.addBatch({ "first email, first pass bad": { diff --git a/tests/list-emails-wsapi-test.js b/tests/list-emails-wsapi-test.js index 09fa35775cd57cbfc53ae6fa6c475dc0839e7bce..037bed0b812225b6618a231c75f94ec25e12bf33 100755 --- a/tests/list-emails-wsapi-test.js +++ b/tests/list-emails-wsapi-test.js @@ -27,6 +27,7 @@ suite.addBatch({ "stage an account": { topic: wsapi.post('/wsapi/stage_user', { email: 'syncer@somehost.com', + pass: 'fakepass', site:'https://foobar.fakesite.com' }), "works": function(err, r) { @@ -51,7 +52,7 @@ suite.addBatch({ suite.addBatch({ "verifying account ownership": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'fakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "works": function(err, r) { assert.equal(r.code, 200); diff --git a/tests/no-cookie-test.js b/tests/no-cookie-test.js index 33a2db02f237d4a816503b19be3cdfbf01e611f9..1689b22946e2faeca777c710a3a7570b6f908283 100755 --- a/tests/no-cookie-test.js +++ b/tests/no-cookie-test.js @@ -27,6 +27,7 @@ suite.addBatch({ "start registration": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', + pass: 'firstfakepass', site:'http://fakesite.com:123' }), "returns 200": function(err, r) { @@ -51,7 +52,7 @@ suite.addBatch({ suite.addBatch({ "completing user creation": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "works": function(err, r) { assert.equal(r.code, 200); diff --git a/tests/password-bcrypt-update-test.js b/tests/password-bcrypt-update-test.js index df5eae39034b6ef26c86fe86f4c3812e491f23ab..033e9109f29269645e38ae45e1c3476881c14616 100755 --- a/tests/password-bcrypt-update-test.js +++ b/tests/password-bcrypt-update-test.js @@ -46,6 +46,7 @@ suite.addBatch({ "account staging": { topic: wsapi.post('/wsapi/stage_user', { email: TEST_EMAIL, + pass: TEST_PASSWORD, site:'https://fakesite.com' }), "works": function(err, r) { @@ -72,8 +73,7 @@ suite.addBatch({ "setting password": { topic: function() { wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: TEST_PASSWORD + token: token }).call(this); }, "works just fine": function(err, r) { diff --git a/tests/password-length-test.js b/tests/password-length-test.js index 2294e3b306819392fdd67a96c18847ea8a5bfd81..dc7a2b02230b9dfac5438c093f1ab6a4369537a1 100755 --- a/tests/password-length-test.js +++ b/tests/password-length-test.js @@ -37,69 +37,50 @@ suite.addBatch({ // first stage the account suite.addBatch({ - "account staging": { + "a password that is non-existent": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', site:'https://fakesite.com:123' }), - "works": function(err, r) { - assert.equal(r.code, 200); - } - } -}); - -// wait for the token -suite.addBatch({ - "a token": { - topic: function() { - start_stop.waitForToken(this.callback); - }, - "is obtained": function (t) { - assert.strictEqual(typeof t, 'string'); - token = t; + "causes a HTTP error response": function(err, r) { + assert.equal(r.code, 400); + assert.equal(r.body, "Bad Request: missing 'pass' argument"); } - } -}); - - -// create a new account via the api with (first address) -suite.addBatch({ + }, "a password that is too short": { - topic: function() { - wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: '0123456' // less than 8 chars, invalid - }).call(this) - }, + topic: wsapi.post('/wsapi/stage_user', { + email: 'first@fakeemail.com', + pass: '0123456', // less than 8 chars, invalid + site:'https://fakesite.com:123' + }), "causes a HTTP error response": function(err, r) { assert.equal(r.code, 400); assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars"); } }, "a password that is too long": { - topic: function() { - wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: '012345678901234567890123456789012345678901234567890123456789012345678901234567891', // more than 81 chars, invalid. - }).call(this); - }, + topic: wsapi.post('/wsapi/stage_user', { + email: 'first@fakeemail.com', + pass: '012345678901234567890123456789012345678901234567890123456789012345678901234567891', // more than 81 chars, invalid. + site:'https://fakesite.com:123' + }), "causes a HTTP error response": function(err, r) { assert.equal(r.code, 400); assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars"); } }, "but a password that is just right": { - topic: function() { - wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: 'ahhh. this is just right.' - }).call(this); - }, + topic: wsapi.post('/wsapi/stage_user', { + email: 'first@fakeemail.com', + pass: 'ahhh. this is just right.', + site:'https://fakesite.com:123' + }), "works just fine": function(err, r) { assert.equal(r.code, 200); } } }); + start_stop.addShutdownBatches(suite); // run or export the suite. diff --git a/tests/password-update-test.js b/tests/password-update-test.js index b04a4951fd7bf0f2a1161a56fc7dc02a92b41c47..98ca7384226b05d41c59f7f245209544bbce2eee 100755 --- a/tests/password-update-test.js +++ b/tests/password-update-test.js @@ -34,6 +34,7 @@ suite.addBatch({ "account staging": { topic: wsapi.post('/wsapi/stage_user', { email: TEST_EMAIL, + pass: OLD_PASSWORD, site: 'https://fakesite.com:123' }), "works": function(err, r) { @@ -60,8 +61,7 @@ suite.addBatch({ "setting password": { topic: function() { wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: OLD_PASSWORD + token: token }).call(this); }, "works just fine": function(err, r) { diff --git a/tests/primary-then-secondary-test.js b/tests/primary-then-secondary-test.js index 11104396191720812da44e195cb6b2020aea0751..c09e023b04c68520d03c5229878611cbed93d32c 100755 --- a/tests/primary-then-secondary-test.js +++ b/tests/primary-then-secondary-test.js @@ -75,7 +75,6 @@ suite.addBatch({ }); var token; - // now we have a new account. let's add a secondary to it suite.addBatch({ "add a new email address to our account": { @@ -83,40 +82,37 @@ suite.addBatch({ email: SECONDARY_EMAIL, site:'https://fakesite.com' }), - "works": function(err, r) { + "fails without a password": function(err, r) { assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, false); }, - "and get a token": { - topic: function() { - start_stop.waitForToken(this.callback); - }, - "successfully": function (t) { - this._token = t; - assert.strictEqual(typeof t, 'string'); + "with a password": { + topic: wsapi.post('/wsapi/stage_email', { + email: SECONDARY_EMAIL, + pass: TEST_PASS, + site:'https://fakesite.com' + }), + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); }, - "and complete": { - topic: function(t) { - wsapi.get('/wsapi/email_for_token', { - token: t - }).call(this); + "and get a token": { + topic: function() { + start_stop.waitForToken(this.callback); }, - "we need to set our password": function (err, r) { - r = JSON.parse(r.body); - assert.ok(r.needs_password); + "successfully": function (t) { + this._token = t; + assert.strictEqual(typeof t, 'string'); }, - "with": { - topic: function() { - wsapi.post('/wsapi/complete_email_addition', { token: this._token }).call(this); + "and complete": { + topic: function(t) { + wsapi.get('/wsapi/email_for_token', { + token: t + }).call(this); }, - "no password fails": function(err, r) { - assert.equal(r.code, 200); - assert.strictEqual(JSON.parse(r.body).success, false); - }, - "a password": { + "which then": { topic: function() { wsapi.post('/wsapi/complete_email_addition', { - token: this._token, - pass: TEST_PASS + token: this._token }).call(this); }, "succeeds": function(err, r) { @@ -140,62 +136,58 @@ suite.addBatch({ } }); - -// after a small delay, we can authenticate with our password +// now we can authenticate with our password suite.addBatch({ - "after a small delay": { - topic: function() { setTimeout(this.callback, 1500); }, - "authenticating with our newly set password" : { - topic: wsapi.post('/wsapi/authenticate_user', { - email: TEST_EMAIL, - pass: TEST_PASS, - ephemeral: false - }), - "works": function(err, r) { - assert.strictEqual(r.code, 200); - } + "authenticating with our newly set password" : { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); } } }); - // adding a second secondary will not let us set the password suite.addBatch({ - "add a new email address to our account": { + "add a second secondary to account with": { topic: wsapi.post('/wsapi/stage_email', { email: SECOND_SECONDARY_EMAIL, + pass: TEST_PASS, site:'http://fakesite.com:123' }), - "works": function(err, r) { + "a password fails": function(err, r) { assert.strictEqual(r.code, 200); + var body = JSON.parse(r.body); + assert.strictEqual(body.success, false); + assert.strictEqual(body.reason, 'a password may not be set at this time'); }, - "and get a token": { - topic: function() { - start_stop.waitForToken(this.callback); - }, - "successfully": function (t) { - this._token = t; - assert.strictEqual(typeof t, 'string'); + "but with no password specified": { + topic: wsapi.post('/wsapi/stage_email', { + email: SECOND_SECONDARY_EMAIL, + site:'http://fakesite.com:123' + }), + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, true); }, - "and to complete": { - topic: function(t) { - wsapi.get('/wsapi/email_for_token', { - token: t - }).call(this); + "and get a token": { + topic: function() { + start_stop.waitForToken(this.callback); }, - "we do not need to set our password": function (err, r) { - r = JSON.parse(r.body); - assert.isFalse(r.needs_password); + "successfully": function (t) { + this._token = t; + assert.strictEqual(typeof t, 'string'); }, - "with": { - topic: function() { - wsapi.post('/wsapi/complete_email_addition', { token: this._token, pass: TEST_PASS }).call(this); + "and to complete": { + topic: function(t) { + wsapi.get('/wsapi/email_for_token', { + token: t + }).call(this); }, - "a password fails": function(err, r) { - assert.equal(r.code, 200); - assert.strictEqual(JSON.parse(r.body).success, false); - }, - "no password succeeds": { + "with a token": { topic: function() { wsapi.post('/wsapi/complete_email_addition', { token: this._token @@ -231,11 +223,10 @@ suite.addBatch({ }), "works": function(err, r) { assert.strictEqual(r.code, 200); - }, + } } }); - // shut the server down and cleanup start_stop.addShutdownBatches(suite); diff --git a/tests/registration-status-wsapi-test.js b/tests/registration-status-wsapi-test.js index 9857e9e39a80172b4a8852e79c15ccb3280a7e2d..10944b1ae2388549ddf83fad688ca8fff0996228 100755 --- a/tests/registration-status-wsapi-test.js +++ b/tests/registration-status-wsapi-test.js @@ -51,6 +51,7 @@ suite.addBatch({ "start registration": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', + pass: 'firstfakepass', site:'https://fakesite.com' }), "returns 200": function(err, r) { @@ -110,7 +111,7 @@ suite.addBatch({ suite.addBatch({ "completing user creation": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "works": function(err, r) { assert.equal(r.code, 200); @@ -170,6 +171,7 @@ suite.addBatch({ "re-registering an existing email": { topic: wsapi.post('/wsapi/stage_user', { email: 'first@fakeemail.com', + pass: 'secondfakepass', site:'http://secondfakesite.com' }), "yields a HTTP 200": function (err, r) { @@ -206,7 +208,7 @@ suite.addBatch({ suite.addBatch({ "proving email ownership causes account re-creation": { topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'secondfakepass' }).call(this); + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); }, "and returns a 200 code": function(err, r) { assert.equal(r.code, 200); diff --git a/tests/session-context-test.js b/tests/session-context-test.js index 87a42ac40ce34faf8658bcf6a5b20b68b232eb61..6c2fe805a1c2a519ff1a7a9bccb434f018c06e5a 100755 --- a/tests/session-context-test.js +++ b/tests/session-context-test.js @@ -32,6 +32,7 @@ suite.addBatch({ "account staging": { topic: wsapi.post('/wsapi/stage_user', { email: TEST_EMAIL, + pass: PASSWORD, site: 'https://fakesite.com' }), "works": function(err, r) { @@ -58,8 +59,7 @@ suite.addBatch({ "setting password": { topic: function() { wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: PASSWORD + token: token }).call(this); }, "works just fine": function(err, r) { @@ -89,7 +89,7 @@ suite.addBatch({ var resp = JSON.parse(r.body); assert.strictEqual(typeof resp.csrf_token, 'string'); var serverTime = new Date(resp.server_time); - assert.ok(new Date() - serverTime < 5000); + assert.ok(new Date() - serverTime < 5000); assert.strictEqual(resp.authenticated, true); assert.strictEqual(resp.auth_level, 'password'); var domainKeyCreation = new Date(resp.domain_key_creation_time); diff --git a/tests/session-duration-test.js b/tests/session-duration-test.js index cedec0f0857870940ede1492dfdbad94beae19bc..232ad6320a6f7979a6f5f5d39f800eb377229a9a 100755 --- a/tests/session-duration-test.js +++ b/tests/session-duration-test.js @@ -106,6 +106,7 @@ suite.addBatch({ "account staging": { topic: wsapi.post('/wsapi/stage_user', { email: TEST_EMAIL, + pass: PASSWORD, site: 'http://a.really.fakesite123.com:999' }), "works": function(err, r) { @@ -132,8 +133,7 @@ suite.addBatch({ "setting password": { topic: function() { wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: PASSWORD + token: token }).call(this); }, "works just fine": function(err, r) { diff --git a/tests/session-prolong-test.js b/tests/session-prolong-test.js index adf193e2764b7d6adb53aa9d7603a585e1ef0dce..2df5cc4d2a2a7e1ad80cb847f233479389f5e9b5 100755 --- a/tests/session-prolong-test.js +++ b/tests/session-prolong-test.js @@ -32,6 +32,7 @@ suite.addBatch({ "account staging": { topic: wsapi.post('/wsapi/stage_user', { email: TEST_EMAIL, + pass: PASSWORD, site: 'http://fakesite.com' }), "works": function(err, r) { @@ -58,8 +59,7 @@ suite.addBatch({ "setting password": { topic: function() { wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: PASSWORD + token: token }).call(this); }, "works just fine": function(err, r) { diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js index 0bdd2b4b4b670b034120f3f47c886d5ced31bac8..e75fbad678cf03961d3a2a32bafe626768e4b1df 100755 --- a/tests/stalled-mysql-test.js +++ b/tests/stalled-mysql-test.js @@ -130,7 +130,7 @@ suite.addBatch({ "complete_user_creation": { topic: wsapi.post('/wsapi/complete_user_creation', { token: 'bogus', - pass: 'fakefake' + pass: 'alsobogus' }), "fails with 503": function(err, r) { assert.strictEqual(r.code, 503); @@ -147,6 +147,7 @@ suite.addBatch({ "stage_user": { topic: wsapi.post('/wsapi/stage_user', { email: 'bogus@bogus.edu', + pass: 'a_password', site: 'https://whatev.er' }), "fails with 503": function(err, r) { @@ -176,6 +177,7 @@ suite.addBatch({ "account staging": { topic: wsapi.post('/wsapi/stage_user', { email: "stalltest@whatev.er", + pass: 'a_password', site: 'http://fakesite.com' }), "works": function(err, r) { @@ -195,8 +197,7 @@ suite.addBatch({ "setting password": { topic: function(token) { wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: "somepass" + token: token }).call(this); }, "works just fine": function(err, r) { @@ -266,6 +267,7 @@ suite.addBatch({ "stage_email": { topic: wsapi.post('/wsapi/stage_email', { email: "test2@whatev.er", + pass: 'a_password', site: "https://foo.com" }), "fails with 503": function(err, r) { diff --git a/tests/verifier-test.js b/tests/verifier-test.js index 09ea032801dc5fdff489040a1b281666efdb0a47..efa098a102eec4110daff9b8dc582218e1e2eb6d 100755 --- a/tests/verifier-test.js +++ b/tests/verifier-test.js @@ -41,6 +41,7 @@ suite.addBatch({ "account staging": { topic: wsapi.post('/wsapi/stage_user', { email: TEST_EMAIL, + pass: TEST_PASSWORD, site: TEST_ORIGIN }), "works": function(err, r) { @@ -65,8 +66,7 @@ suite.addBatch({ "setting password and creating the account": { topic: function() { wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: TEST_PASSWORD + token: token }).call(this); }, "works just fine": function(err, r) { @@ -714,7 +714,7 @@ suite.addBatch({ }); // now verify that assertions from a primary who does not have browserid support -// will fail to verify +// will fail to verify function make_other_issuer_tests(new_style) { var title = "generating an assertion from a cert signed by some other domain with " + (new_style ? "new style" : "old style"); var tests = { diff --git a/tests/verify-in-different-browser-test.js b/tests/verify-in-different-browser-test.js new file mode 100755 index 0000000000000000000000000000000000000000..6ebd2fe15cbf02556009865a404aa998d831ea7b --- /dev/null +++ b/tests/verify-in-different-browser-test.js @@ -0,0 +1,353 @@ +#!/usr/bin/env node + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +require('./lib/test_env.js'); + +const +assert = require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'), +primary = require('./lib/primary.js'); + +var suite = vows.describe('verify-in-different-browser'); + +// start up a pristine server +start_stop.addStartupBatches(suite); + +// This test ensures that when email verification of a secondary address +// occurs in a browsing context other than the one that initiated it, +// the user must re-provide their password. + +// first we'll need to authenticate a user with an assertion from a +// primary IdP + +const TEST_DOMAIN = 'example.domain', + TEST_EMAIL = 'testuser@' + TEST_DOMAIN, + TEST_ORIGIN = 'http://127.0.0.1:10002', + TEST_PASS = 'fakepass', + SECONDARY_EMAIL = 'secondary@notexample.domain', + SECOND_SECONDARY_EMAIL = 'secondsecondary@notexample.domain', + THIRD_SECONDARY_EMAIL = 'thirdsecondary@notexample.domain', + FOURTH_SECONDARY_EMAIL = 'fourthsecondary@notexample.domain'; + +var primaryUser = new primary({ + email: TEST_EMAIL, + domain: TEST_DOMAIN +}); + +// first we'll create an account without a password by using +// a primary address. +suite.addBatch({ + "generating an assertion": { + topic: function() { + return primaryUser.getAssertion(TEST_ORIGIN); + }, + "succeeds": function(r) { + assert.isString(r); + }, + "and logging in with the assertion": { + topic: function(assertion) { + wsapi.post('/wsapi/auth_with_assertion', { + assertion: assertion, + ephemeral: true + }).call(this); + }, + "succeeds": function(err, r) { + var resp = JSON.parse(r.body); + assert.isObject(resp); + assert.isTrue(resp.success); + } + } + } +}); + +var token; + +// let's add a secondary email to this account +suite.addBatch({ + "add a new email address to our account": { + topic: wsapi.post('/wsapi/stage_email', { + email: SECONDARY_EMAIL, + pass: TEST_PASS, + site:'https://fakesite.com' + }), + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); + }, + "and get a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "successfully": function (t) { + this._token = t; + assert.strictEqual(typeof t, 'string'); + }, + "then clearing cookies and completing": { + topic: function() { + wsapi.clearCookies(); + wsapi.post('/wsapi/complete_email_addition', { + token: this._token + }).call(this); + }, + "fails without a password": function(err, r) { + assert.strictEqual(r.code, 401); + }, + "but succeeds": { + topic: function() { + wsapi.post('/wsapi/complete_email_addition', { + token: this._token, + pass: TEST_PASS + }).call(this); + }, + "with one": function(err, r) { + assert.strictEqual(r.code, 200); + } + } + } + } + } +}); + +// after adding a secondary and setting password, we're password auth'd +suite.addBatch({ + "auth_level": { + topic: wsapi.get('/wsapi/session_context'), + "is 'password' after authenticating with password" : function(err, r) { + assert.strictEqual(JSON.parse(r.body).auth_level, 'password'); + } + } +}); + +// we can authenticate with our password +suite.addBatch({ + "authenticating with our newly set password" : { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + } + } +}); + +// let's add another secondary email, again by confirming the address on +// "a different browser". This time, the server will have to authenticate +// us by pulling our password out of our user record rather than out of +// the stage table. +suite.addBatch({ + "add a new email address to our account": { + topic: wsapi.post('/wsapi/stage_email', { + email: SECOND_SECONDARY_EMAIL, + site:'https://fakesite.com' + }), + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); + }, + "and get a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "successfully": function (t) { + this._token = t; + assert.strictEqual(typeof t, 'string'); + }, + "then clearing cookies and completing": { + topic: function() { + wsapi.clearCookies(); + wsapi.post('/wsapi/complete_email_addition', { + token: this._token + }).call(this); + }, + "fails without a password": function(err, r) { + assert.strictEqual(r.code, 401); + }, + "but succeeds": { + topic: function() { + wsapi.post('/wsapi/complete_email_addition', { + token: this._token, + pass: TEST_PASS + }).call(this); + }, + "with one": function(err, r) { + assert.strictEqual(r.code, 200); + } + } + } + } + } +}); + +// we're password auth'd +suite.addBatch({ + "auth_level": { + topic: wsapi.get('/wsapi/session_context'), + "is 'password' after authenticating with password" : function(err, r) { + assert.strictEqual(JSON.parse(r.body).auth_level, 'password'); + } + } +}); + + +// we can still authenticate with our password +suite.addBatch({ + "authenticating with our newly set password" : { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + } + } +}); + +// now we've tested proper restrictions on the add email, flow, how about +// new account creation? + +// creating a new account and verifying in "a different browser" requires password +suite.addBatch({ + "staging a new account": { + topic: wsapi.post('/wsapi/stage_user', { + email: THIRD_SECONDARY_EMAIL, + pass: TEST_PASS, + site: 'http://fakesite.com:1235' + }), + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, true); + }, + "yields a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "successfully": function (t) { + this._token = t; + assert.strictEqual(typeof t, 'string'); + }, + "then clearing cookies and completing": { + topic: function() { + wsapi.clearCookies(); + wsapi.post('/wsapi/complete_user_creation', { + token: this._token + }).call(this); + }, + "fails without a password": function(err, r) { + assert.strictEqual(r.code, 401); + }, + "but succeeds": { + topic: function() { + wsapi.post('/wsapi/complete_email_addition', { + token: this._token, + pass: TEST_PASS + }).call(this); + }, + "with one": function(err, r) { + assert.strictEqual(r.code, 200); + } + } + } + } + } +}); + +// creating a new account and verifying in "the same browser" requires no password +suite.addBatch({ + "staging a new account": { + topic: wsapi.post('/wsapi/stage_user', { + email: FOURTH_SECONDARY_EMAIL, + pass: TEST_PASS, + site: 'http://fakesite.com:1235' + }), + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, true); + }, + "yields a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "successfully": function (t) { + this._token = t; + assert.strictEqual(typeof t, 'string'); + }, + "and completion with only a token": { + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { + token: this._token + }).call(this); + }, + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); + } + } + } + } +}); + +suite.addBatch({ + "authentication with first email": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + }, + }, + "authentication with second email": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: SECONDARY_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + } + }, + "authentication with third email": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: SECOND_SECONDARY_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + } + }, + "authentication with fourth email": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: THIRD_SECONDARY_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + } + }, + "authentication with fifth email": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: FOURTH_SECONDARY_EMAIL, + pass: TEST_PASS, + ephemeral: false + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + } + } +}); + + +// shut the server down and cleanup +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module);