diff --git a/lib/db.js b/lib/db.js index 1f08f6e99c5d421bb53c4c4a35b5a116e452cf37..0dca5d2f9f7e01f37ef48b3a1a505e80f733ed88 100644 --- a/lib/db.js +++ b/lib/db.js @@ -79,6 +79,7 @@ exports.onReady = function(f) { 'emailKnown', 'emailToUID', 'emailType', + 'emailIsVerified', 'emailsBelongToSameAccount', 'haveVerificationSecret', 'isStaged', @@ -101,7 +102,9 @@ exports.onReady = function(f) { [ 'stageUser', 'stageEmail', - 'gotVerificationSecret', + 'completeCreateUser', + 'completeConfirmEmail', + 'completePasswordReset', 'removeEmail', 'cancelAccount', 'updatePassword', diff --git a/lib/db/json.js b/lib/db/json.js index 0bcfbf8a1fa9bcc5fdb8fab56c40705ff1b01e23..a1d8623e4265f1aab1dbf2ba919c75647b4da9e4 100644 --- a/lib/db/json.js +++ b/lib/db/json.js @@ -111,6 +111,15 @@ exports.emailKnown = function(email, cb) { process.nextTick(function() { cb(null, m.length > 0) }); }; +exports.emailIsVerified = function(email, cb) { + sync(); + var m = jsel.match(".emails ." + ESC(email), db.users); + process.nextTick(function() { + if (!m.length) cb("no such email"); + else cb(null, m[0].verified); + }); +}; + exports.emailType = function(email, cb) { sync(); var m = jsel.match(".emails ." + ESC(email), db.users); @@ -175,7 +184,7 @@ function addEmailToAccount(userID, email, type, cb) { sync(); var emails = jsel.match(":has(.id:expr(x="+ ESC(userID) +")) > .emails", db.users); if (emails && emails.length > 0) { - emails[0][email] = { type: type }; + emails[0][email] = { type: type, verified: true }; flush(); } cb(null); @@ -218,7 +227,7 @@ exports.stageEmail = function(existing_user, new_email, hash, cb) { exports.createUserWithPrimaryEmail = function(email, cb) { var emailVal = { }; - emailVal[email] = { type: 'primary' }; + emailVal[email] = { type: 'primary', verified: true }; var uid = getNextUserID(); db.users.push({ id: uid, @@ -270,7 +279,7 @@ exports.verificationSecretForEmail = function(email, cb) { }, 0); }; -exports.gotVerificationSecret = function(secret, cb) { +function getAndDeleteRowForSecret(secret, cb) { sync(); if (!db.staged.hasOwnProperty(secret)) return cb("unknown secret"); @@ -279,11 +288,48 @@ exports.gotVerificationSecret = function(secret, cb) { delete db.staged[secret]; delete db.stagedEmails[o.email]; flush(); - if (o.type === 'add_account') { + + process.nextTick(function() { cb(null, o); }); +} + +// either a email re-verification, or an email addition - we treat these things +// the same +exports.completeConfirmEmail = function(secret, cb) { + getAndDeleteRowForSecret(secret, function(err, o) { + exports.emailKnown(o.email, function(err, known) { + function addIt() { + addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) { + var hash = o.passwd; + if(e || typeof hash !== 'string') 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) { + removeEmailNoCheck(o.email, function (err) { + if (err) cb(err); + else addIt(); + }); + } else { + addIt(); + } + }); + }); +} + +exports.completeCreateUser = function(secret, cb) { + getAndDeleteRowForSecret(secret, function(err, o) { exports.emailKnown(o.email, function(err, known) { function createAccount() { var emailVal = {}; - emailVal[o.email] = { type: 'secondary' }; + emailVal[o.email] = { type: 'secondary', verified: true }; var uid = getNextUserID(); var hash = o.passwd; db.users.push({ @@ -310,35 +356,41 @@ exports.gotVerificationSecret = function(secret, cb) { createAccount(); } }); - } else if (o.type === 'add_email') { + }); +}; + +exports.completePasswordReset = function(secret, cb) { + getAndDeleteRowForSecret(secret, function(err, o) { exports.emailKnown(o.email, function(err, known) { - function addIt() { - addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) { - var hash = o.passwd; - if(e || hash === null) return cb(e, o.email, o.existing_user); + if (err) return cb(err); - // 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.emailToUID(o.email, function(err, uid) { + if (err) return cb(err); - exports.updatePassword(uid, hash, function(err) { - cb(err || null, o.email, o.existing_user); - }); - }); + // if for some reason the email is associated with a different user now than when + // the action was initiated, error out. + if (uid !== o.existing_user) { + return cb("cannot update password, data inconsistency"); + } + + sync(); + // flip the verification bit on all emails for the user other than the one just verified + var emails = jsel.match(":has(.id:expr(x=?)) > .emails", [ uid ], db.users)[0]; + + Object.keys(emails).forEach(function(email) { + if (email != o.email && emails[email].type === 'secondary') { + emails[email].verified = false; + } }); - } - if (known) { - removeEmailNoCheck(o.email, function (err) { - if (err) cb(err); - else addIt(); + flush(); + + // update the password! + exports.updatePassword(uid, o.passwd, function(err) { + cb(err, o.email, uid); }); - } else { - addIt(); - } + }); }); - } else { - cb("internal error"); - } + }); }; exports.addPrimaryEmailToAccount = function(userID, emailToAdd, cb) { @@ -442,7 +494,7 @@ exports.addTestUser = function(email, hash, cb) { sync(); removeEmailNoCheck(email, function() { var emailVal = {}; - emailVal[email] = { type: 'secondary' }; + emailVal[email] = { type: 'secondary', verified: true }; db.users.push({ id: getNextUserID(), password: hash, diff --git a/lib/db/mysql.js b/lib/db/mysql.js index f9d37b753a33d1c05ca30d775060a8201156e550..4b3cc13a2adaea54e84ce205be8c4a0ef2de4677 100644 --- a/lib/db/mysql.js +++ b/lib/db/mysql.js @@ -15,6 +15,7 @@ * | string passwd | \- |*int user | * +---------------+ |*string address| * | enum type | + * | bool verified | * +---------------+ * * @@ -72,6 +73,7 @@ const schemas = [ "user BIGINT NOT NULL," + "address VARCHAR(255) UNIQUE NOT NULL," + "type ENUM('secondary', 'primary') DEFAULT 'secondary' NOT NULL," + + "verified BOOLEAN DEFAULT TRUE NOT NULL, " + "FOREIGN KEY user_fkey (user) REFERENCES user(id)" + ") ENGINE=InnoDB;", @@ -224,6 +226,17 @@ exports.emailType = function(email, cb) { ); } +exports.emailIsVerified = function(email, cb) { + client.query( + "SELECT verified FROM email WHERE address = ?", [ email ], + function(err, rows) { + if (rows && rows.length > 0) cb(err, !!rows[0].verified); + else cb('no such email'); + } + ); +}; + + exports.isStaged = function(email, cb) { client.query( "SELECT COUNT(*) as N FROM staged WHERE email = ?", [ email ], @@ -331,8 +344,7 @@ function addEmailToUser(userID, email, type, cb) { }); } - -exports.gotVerificationSecret = function(secret, cb) { +function getAndDeleteRowForSecret(secret, cb) { client.query( "SELECT * FROM staged WHERE secret = ?", [ secret ], function(err, rows) { @@ -342,45 +354,91 @@ exports.gotVerificationSecret = function(secret, cb) { } else if (rows.length === 0) { cb("unknown secret"); } else { - var o = rows[0]; - // delete the record client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]); + cb(null, rows[0]); + } + }); +} - 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(?)", - [ hash ], - function(err, info) { - if (err) return cb(err); - addEmailToUser(info.insertId, o.email, 'secondary', cb); - }); - } else { - // ensure the expected existing_user field is populated, which it must always be when - // new_acct is false - if (typeof o.existing_user !== 'number') { - return cb("data inconsistency, no numeric existing user associated with staged email address"); - } +exports.completeCreateUser = function(secret, cb) { + getAndDeleteRowForSecret(secret, function(err, o) { + if (err) return cb(err); + + if (!o.new_acct) return cb("this verification link is not for a new account"); - // we're adding an email address to an existing user account. add appropriate entries into - // email table - var hash = o.passwd; - var uid = o.existing_user; - 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); - } - } - }; + // we're creating a new account, add appropriate entries into user and email tables. + client.query( + "INSERT INTO user(passwd) VALUES(?)", + [ o.passwd ], + function(err, info) { + if (err) return cb(err); + addEmailToUser(info.insertId, o.email, 'secondary', cb); + }); + }); +}; + +// either a email re-verification, or an email addition - we treat these things +// the same +exports.completeConfirmEmail = function(secret, cb) { + getAndDeleteRowForSecret(secret, function(err, o) { + if (err) return cb(err); + + if (o.new_acct) return cb("this verification link is not for an email addition"); + + // ensure the expected existing_user field is populated, which it must always be when + // new_acct is false + if (typeof o.existing_user !== 'number') { + return cb("data inconsistency, no numeric existing user associated with staged email address"); + } + + // we're adding or reverifying an email address to an existing user account. add appropriate + // entries into email table. + if (o.passwd) { + exports.updatePassword(o.existing_user, o.passwd, function(err) { + if (err) return cb('could not set user\'s password'); + addEmailToUser(o.existing_user, o.email, 'secondary', cb); + }); + } else { + addEmailToUser(o.existing_user, o.email, 'secondary', cb); + } + }); +}; + +exports.completePasswordReset = function(secret, cb) { + getAndDeleteRowForSecret(secret, function(err, o) { + if (err) return cb(err); + + if (o.new_acct || !o.passwd || !o.existing_user) { + return cb("this verification link is not for a password reset"); } - ); -} + + // verify that the email still exists in the database, and the the user with whom it is + // associated is the same as the user in the database + exports.emailToUID(o.email, function(err, uid) { + if (err) return cb(err); + + // if for some reason the email is associated with a different user now than when + // the action was initiated, error out. + if (uid !== o.existing_user) { + return cb("cannot update password, data inconsistency"); + } + + // flip the verification bit on all emails for the user other than the one just verified + client.query( + 'UPDATE email SET verified = FALSE WHERE user = ? AND type = ? AND address != ?', + [ uid, 'secondary', o.email ], + function(err) { + if (err) return cb(err); + + // update the password! + exports.updatePassword(uid, o.passwd, function(err) { + cb(err, o.email, uid); + }); + }); + }); + }); +}; exports.addPrimaryEmailToAccount = function(uid, emailToAdd, cb) { // we're adding an email address to an existing user account. add appropriate entries into @@ -471,18 +529,20 @@ exports.updatePassword = function(uid, hash, cb) { */ exports.listEmails = function(uid, cb) { client.query( - 'SELECT address, type FROM email WHERE user = ?', + 'SELECT address, type, verified FROM email WHERE user = ?', [ uid ], function (err, rows) { if (err) cb(err); else { var emails = {}; - // eventually we'll have fields in here - for (var i = 0; i < rows.length; i++) - emails[rows[i].address] = { - type: rows[i].type - }; + for (var i = 0; i < rows.length; i++) { + var o = { type: rows[i].type }; + if (o.type === 'secondary') { + o.verified = rows[i].verified ? true : false; + } + emails[rows[i].address] = o; + } cb(null,emails); } diff --git a/lib/email.js b/lib/email.js index f87e03e404bee549e7431cc0372d3b43af8f4cd3..13d24ffbb45147cbf63c130d6dc1d111d9e5b897 100644 --- a/lib/email.js +++ b/lib/email.js @@ -1,4 +1,4 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public +/* 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/. */ @@ -41,14 +41,14 @@ const templates = { template: fs.readFileSync(path.join(TEMPLATE_PATH, 'new.ejs')), }, "reset": { - landing: 'verify_email_address', + landing: 'reset_password', subject: _("Reset Persona password"), template: fs.readFileSync(path.join(TEMPLATE_PATH, 'reset.ejs')), }, - "add": { - landing: 'add_email_address', + "confirm": { + landing: 'confirm', subject: _("Confirm email address for Persona"), - template: fs.readFileSync(path.join(TEMPLATE_PATH, 'add.ejs')), + template: fs.readFileSync(path.join(TEMPLATE_PATH, 'confirm.ejs')), } }; @@ -118,8 +118,8 @@ exports.sendNewUserEmail = function(email, site, secret, langContext) { doSend('new', email, site, secret, langContext); }; -exports.sendAddAddressEmail = function(email, site, secret, langContext) { - doSend('add', email, site, secret, langContext); +exports.sendConfirmationEmail = function(email, site, secret, langContext) { + doSend('confirm', email, site, secret, langContext); }; exports.sendForgotPasswordEmail = function(email, site, secret, langContext) { diff --git a/lib/load_gen/activities/add_email.js b/lib/load_gen/activities/add_email.js index a79fc564da48605d8fdc50e9efbb986e52f6d8f7..9e240d8f6ef86541e6d3689ec8dd945940917274 100644 --- a/lib/load_gen/activities/add_email.js +++ b/lib/load_gen/activities/add_email.js @@ -19,7 +19,7 @@ exports.startFunc = function(cfg, cb) { // 5. email_addition_status is invoked some number of times while the dialog polls // 6. landing page is loaded: // 6a. session_context - // 6b. complete_email_addition + // 6b. complete_email_confirmation // 7. email_addition_status returns 'complete' // 8. a key is generated and added @@ -76,7 +76,7 @@ exports.startFunc = function(cfg, cb) { var token = r.body; // and simulate clickthrough - wcli.post(cfg, '/wsapi/complete_email_addition', context, { + wcli.post(cfg, '/wsapi/complete_email_confirmation', context, { token: token }, function (err, r) { try { diff --git a/lib/static/email_templates/add.ejs b/lib/static/email_templates/confirm.ejs similarity index 66% rename from lib/static/email_templates/add.ejs rename to lib/static/email_templates/confirm.ejs index fb928499b4f11710331a8ae83d7d8b4de035e59a..ad8758855cdd0d04cc17432dd8e30e0fc16a3b0d 100644 --- a/lib/static/email_templates/add.ejs +++ b/lib/static/email_templates/confirm.ejs @@ -3,7 +3,7 @@ <%= format(gettext('Click to confirm this email address and automatically sign in to %s'), [site]) %> <%= link %> -<%= format(gettext('Note: If you are NOT trying to sign into this website, please ignore this email.'), [site]) %> +<%= gettext('Note: If you are NOT trying to sign into this website, please ignore this email.') %> <%= gettext('Thank you,') %> <%= gettext('The Persona team') %> diff --git a/lib/static/views.js b/lib/static/views.js index 65a5d64eb9b8d3f599f76563fc5103fd5ba96b38..957bfb09c92d0b3d1e6ec25d0392c9222d6f2530 100644 --- a/lib/static/views.js +++ b/lib/static/views.js @@ -174,10 +174,23 @@ exports.setup = function(app) { }); }); + // This page can be removed a couple weeks after this code ships into production, + // we're leaving it here to not break outstanding emails app.get("/add_email_address", function(req,res) { - renderCachableView(req, res, 'add_email_address.ejs', {title: 'Verify Email Address', fullpage: false}); + renderCachableView(req, res, 'confirm.ejs', {title: 'Verify Email Address', fullpage: false}); }); + + app.get("/reset_password", function(req,res) { + renderCachableView(req, res, 'confirm.ejs', {title: 'Reset Password'}); + }); + + app.get("/confirm", function(req,res) { + renderCachableView(req, res, 'confirm.ejs', {title: 'Confirm Email'}); + }); + + + // serve up testing templates. but NOT in staging or production. see GH-1044 if ([ 'https://login.persona.org', 'https://login.anosrep.org' ].indexOf(config.get('public_url')) === -1) { // serve test.ejs to /test or /test/ or /test/index.html diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js index 0f0c81ef3f8d7336db02f0ad90a6719fcc086957..1f373b92a0a5377351cf2d56619d8eef1ed4e694 100644 --- a/lib/wsapi/cert_key.js +++ b/lib/wsapi/cert_key.js @@ -24,15 +24,22 @@ exports.process = function(req, res) { // not same account? big fat error if (!owned) return httputils.badRequest(res, "that email does not belong to you"); - // forward to the keysigner! - var keysigner = urlparse(config.get('keysigner_url')); - keysigner.path = '/wsapi/cert_key'; - forward(keysigner, req, res, function(err) { - if (err) { - logger.error("error forwarding request to keysigner: " + err); - httputils.serverError(res, "can't contact keysigner"); - return; - } + // secondary addresses in the database may be "unverified". this occurs when + // a user forgets their password. We will not issue certs for unverified email + // addresses + db.emailIsVerified(req.body.email, function(err, verified) { + if (!verified) return httputils.forbidden(res, "that email requires (re)verification"); + + // forward to the keysigner! + var keysigner = urlparse(config.get('keysigner_url')); + keysigner.path = '/wsapi/cert_key'; + forward(keysigner, req, res, function(err) { + if (err) { + logger.error("error forwarding request to keysigner: " + err); + httputils.serverError(res, "can't contact keysigner"); + return; + } + }); }); }); }; diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_confirmation.js similarity index 85% rename from lib/wsapi/complete_email_addition.js rename to lib/wsapi/complete_email_confirmation.js index 53ddb3625200c603ec1396aceb3b79a415143a07..fefdd1fc31d6fd9da4f76aadf908f6cfa6441165 100644 --- a/lib/wsapi/complete_email_addition.js +++ b/lib/wsapi/complete_email_confirmation.js @@ -2,6 +2,12 @@ * 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/. */ +/* This api is hit in two cases: + * + the final step in adding a new email to your account + * + the final step in re-verifying an email in your account after + * password reset + */ + const db = require('../db.js'), logger = require('../logging.js').logger, @@ -18,11 +24,10 @@ exports.args = ['token']; exports.i18n = false; exports.process = function(req, res) { - // in order to complete an email addition, one of the following must be true: + // in order to complete an email confirmation, 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); @@ -47,7 +52,7 @@ exports.process = function(req, res) { } function postAuthentication() { - db.gotVerificationSecret(req.body.token, function(e, email, uid) { + db.completeConfirmEmail(req.body.token, function(e, email, uid) { if (e) { logger.warn("couldn't complete email verification: " + e); wsapi.databaseDown(res, e); diff --git a/lib/wsapi/complete_reset.js b/lib/wsapi/complete_reset.js new file mode 100644 index 0000000000000000000000000000000000000000..b48f6582e94814fa91c730f4e68c760a6c1b4ae2 --- /dev/null +++ b/lib/wsapi/complete_reset.js @@ -0,0 +1,88 @@ +/* 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/. */ + +const +db = require('../db.js'), +logger = require('../logging.js').logger, +wsapi = require('../wsapi.js'), +bcrypt = require('../bcrypt.js'), +httputils = require('../httputils.js'), +config = require('../configuration.js'); + +exports.method = 'post'; +exports.writes_db = true; +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) +exports.args = ['token']; +exports.i18n = true; + +exports.process = function(req, res) { + // in order to complete a password reset, 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 + + // is this the same browser? + if (typeof req.session.pendingReset === 'string' && + req.body.token === req.session.pendingReset) { + return postAuthentication(); + } + // is a password provided? + else if (typeof req.body.pass === 'string') { + return db.authForVerificationSecret(req.body.token, function(err, hash) { + if (err) { + 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(res, "server is too busy"); + } else if (!success) { + return httputils.authRequired(res, "password mismatch"); + } else { + return postAuthentication(); + } + }); + }); + } else { + return httputils.authRequired(res, 'Provide your password'); + } + + function postAuthentication() { + db.haveVerificationSecret(req.body.token, function(err, known) { + if (err) return wsapi.databaseDown(res, err); + + if (!known) { + // clear the pendingReset token from the session if we find no such + // token in the database + delete req.session.pendingReset; + return res.json({ success: false} ); + } + + db.completePasswordReset(req.body.token, function(err, email, uid) { + if (err) { + logger.warn("couldn't complete email verification: " + err); + wsapi.databaseDown(res, err); + } else { + // clear the pendingReset token from the session once we + // successfully complete password reset + delete req.session.pendingReset; + + // At this point, the user is either on the same browser with a token from + // their email address, OR they've provided their account password. It's + // safe to grant them an authenticated session. + wsapi.authenticateSession(req.session, uid, 'password', + config.get('ephemeral_session_duration_ms')); + + res.json({ success: true }); + } + }); + }); + } +}; diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js index 7a65ec488f52322b0010ca8bfd33210324749bcf..5e253d38a5f7ca5b766796c6dcdab5936e07adf3 100644 --- a/lib/wsapi/complete_user_creation.js +++ b/lib/wsapi/complete_user_creation.js @@ -68,7 +68,7 @@ exports.process = function(req, res) { return res.json({ success: false} ); } - db.gotVerificationSecret(req.body.token, function(err, email, uid) { + db.completeCreateUser(req.body.token, function(err, email, uid) { if (err) { logger.warn("couldn't complete email verification: " + err); wsapi.databaseDown(res, err); diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js index ac0a5d9543514ac4cb74c1ab3986eded0ad63421..41a3c40272fc8c9139433c775373ce1097927d03 100644 --- a/lib/wsapi/email_for_token.js +++ b/lib/wsapi/email_for_token.js @@ -4,12 +4,18 @@ const db = require('../db.js'), -httputils = require('../httputils.js'); +httputils = require('../httputils.js'), +logger = require('../logging.js').logger; -/* First half of account creation. Stages a user account for creation. - * this involves creating a secret url that must be delivered to the - * user via their claimed email address. Upon timeout expiry OR clickthrough - * the staged user account transitions to a valid user account +/* Given a verification secret (a "token", delivered via email), return the + * email address associated with this token. + * + * This call also returns a hint to the UI, regarding whether completing the + * email verification that this token will require the user to enter their + * password. + * + * These two things are conflated into a single call as a performance + * optimization. */ exports.method = 'get'; @@ -19,75 +25,66 @@ exports.args = ['token']; exports.i18n = false; exports.process = function(req, res) { + db.emailForVerificationSecret(req.query.token, function(err, email, uid, hash) { if (err) { if (err === 'database unavailable') { - httputils.serviceUnavailable(res, err); + return httputils.serviceUnavailable(res, err); } else { - res.json({ + return res.json({ success: false, reason: err }); } - } else { - function checkMustAuth() { - // 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; - } + function checkMustAuth() { + // 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; - res.json({ - success: true, - email: email, - must_auth: must_auth - }); + if (uid && req.session.userid === uid) { + must_auth = false; } - - // backwards compatibility - issue #1592 - // if there is no password in the user record, and no password in the staged - // table, then we require a password be fetched from the user upon verification. - // these checks are temporary and should disappear in 1 trains time. - function needsPassword() { - // no password is set neither in the user table nor in the staged record. - // the user must pick a password - res.json({ - success: true, - email: email, - needs_password: true - }); + else if (!uid && typeof req.session.pendingCreation === 'string' && + req.query.token === req.session.pendingCreation) { + must_auth = false; } + else if (typeof req.session.pendingReset === 'string' && + req.query.token === req.session.pendingReset) + { + must_auth = false; + } + // NOTE: for reverification, we require you're authenticated. it's not enough + // to be on the same browser - that path is nonsensical because you must be + // authenticated to initiate a re-verification. - if (!hash) { - if (!uid) { - needsPassword(); - } else { - db.checkAuth(uid, function(err, hash) { - if (err) { - return res.json({ - success: false, - reason: err - }); - } + res.json({ + success: true, + email: email, + must_auth: must_auth + }); + } - if (!hash) { - needsPassword(); - } else { - checkMustAuth(); - } + if (!hash) { + // if no password is set in the stage table, this is probably an email addition + db.checkAuth(uid, function(err, hash) { + if (err) { + return res.json({ + success: false, + reason: err + }); + } else if (!hash) { + return res.json({ + success: false, + reason: "missing password for user" }); } - } else { checkMustAuth(); - } - + }); + } else { + checkMustAuth(); } }); }; diff --git a/lib/wsapi/email_reverify_status.js b/lib/wsapi/email_reverify_status.js new file mode 100644 index 0000000000000000000000000000000000000000..5068459c3312e7f0fc3baf2e48b5a6a9319f2b1c --- /dev/null +++ b/lib/wsapi/email_reverify_status.js @@ -0,0 +1,33 @@ +/* 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/. */ + +const +db = require('../db.js'), +wsapi = require('../wsapi.js'); + +/* A polled API which returns whether the user has completed reverification + * of an email address + */ + +exports.method = 'get'; +exports.writes_db = false; +exports.authed = 'assertion'; +exports.args = ['email']; +exports.i18n = false; + +exports.process = function(req, res) { + var email = req.query.email; + + // For simplicity, all we check is if an email is verified. We do not check that + // the email is owned by the currently authenticated user, nor that the verification + // secret still exists. These checks would require more database interactions, and + // other calls will fail in such circumstances. + + // is the address verified? + db.emailIsVerified(email, function(err, verified) { + if (err) return wsapi.databaseDown(res, err); + + res.json({ status: verified ? 'complete' : 'pending' }); + }); +}; diff --git a/lib/wsapi/password_reset_status.js b/lib/wsapi/password_reset_status.js new file mode 100644 index 0000000000000000000000000000000000000000..6059eac063238f9507b52c893ea01fc663c19428 --- /dev/null +++ b/lib/wsapi/password_reset_status.js @@ -0,0 +1,50 @@ +/* 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/. */ + +const +db = require('../db.js'), +wsapi = require('../wsapi.js'), +logger = require('../logging.js').logger, +httputils = require('../httputils.js'), +sanitize = require('../sanitize.js'); + +exports.method = 'get'; +exports.writes_db = false; +exports.authed = false; +exports.args = ['email']; +exports.i18n = false; + +exports.process = function(req, res) { + var email = req.query.email; + + try { + sanitize(email).isEmail(); + } catch(e) { + var msg = "invalid arguments: " + e; + logger.warn("bad request received: " + msg); + return httputils.badRequest(res, msg); + } + + // if the email is in the staged table, we are not complete yet. + // if the email is not in the staged table - + // * if we are authenticated as the owner of the email we're done + // * if we are not authenticated as the owner of the email, we must auth + db.isStaged(email, function(err, staged) { + if (err) wsapi.databaseDown(res, err); + + if (staged) { + return res.json({ status: 'pending' }); + } else { + if (wsapi.isAuthed(req, 'assertion')) { + db.userOwnsEmail(req.session.userid, email, function(err, owned) { + if (err) wsapi.databaseDown(res, err); + else if (owned) res.json({ status: 'complete', userid: req.session.userid }); + else res.json({ status: 'mustAuth' }); + }); + } else { + return res.json({ status: 'mustAuth' }); + } + } + }); +}; diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js index 9ad39e994ae716a7dc4d2bf312c6e902c7eaa38d..1e9d8cdb4f7cdad18008714885ff85100b1fc512 100644 --- a/lib/wsapi/stage_email.js +++ b/lib/wsapi/stage_email.js @@ -11,11 +11,7 @@ email = require('../email.js'), sanitize = require('../sanitize'), config = require('../configuration'); -/* First half of account creation. Stages a user account for creation. - * this involves creating a secret url that must be delivered to the - * user via their claimed email address. Upon timeout expiry OR clickthrough - * the staged user account transitions to a valid user account - */ +/* Stage an email for addition to a user's account. Causes email to be sent. */ exports.method = 'post'; exports.writes_db = true; @@ -92,7 +88,7 @@ exports.process = function(req, res) { res.json({ success: true }); // let's now kick out a verification email! - email.sendAddAddressEmail(req.body.email, req.body.site, secret, langContext); + email.sendConfirmationEmail(req.body.email, req.body.site, secret, langContext); }); } catch(e) { // we should differentiate tween' 400 and 500 here. diff --git a/lib/wsapi/stage_reset.js b/lib/wsapi/stage_reset.js new file mode 100644 index 0000000000000000000000000000000000000000..a8aefbbcbdba61397c637ad9f41761ac0bb340ec --- /dev/null +++ b/lib/wsapi/stage_reset.js @@ -0,0 +1,105 @@ +/* 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/. */ + +const +db = require('../db.js'), +wsapi = require('../wsapi.js'), +httputils = require('../httputils'), +logger = require('../logging.js').logger, +email = require('../email.js'), +sanitize = require('../sanitize'), +config = require('../configuration'); + +/* First half of account creation. Stages a user account for creation. + * this involves creating a secret url that must be delivered to the + * user via their claimed email address. Upon timeout expiry OR clickthrough + * the staged user account transitions to a valid user account + */ + +exports.method = 'post'; +exports.writes_db = true; +exports.authed = false; +exports.args = ['email','site','pass']; +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(); + sanitize(req.body.site).isOrigin(); + } catch(e) { + var msg = "invalid arguments: " + e; + logger.warn("bad request received: " + 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(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(res, "Too many emails sent to that address, try again later."); + } + + db.emailToUID(req.body.email, function(err, uid) { + if (err) { + logger.info("reset password fails: " + err); + return res.json({ success: false }); + } + + if (!uid) { + return res.json({ + reason: "No such email address.", + success: false + }); + } + + // staging a user logs you out. + wsapi.clearAuthenticatedUser(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(res, "server is too busy"); + } + logger.error("can't bcrypt: " + err); + return res.json({ success: false }); + } + + // on failure stageEmail may throw + try { + db.stageEmail(uid, 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.pendingReset = secret; + + res.json({ success: true }); + + // let's now kick out a verification email! + email.sendForgotPasswordEmail(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_reverify.js b/lib/wsapi/stage_reverify.js new file mode 100644 index 0000000000000000000000000000000000000000..885fcb57ce7c05081dc3c8c06d70ecb16ce0110a --- /dev/null +++ b/lib/wsapi/stage_reverify.js @@ -0,0 +1,68 @@ +/* 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/. */ + +const +db = require('../db.js'), +wsapi = require('../wsapi.js'), +httputils = require('../httputils'), +logger = require('../logging.js').logger, +email = require('../email.js'), +sanitize = require('../sanitize'), +config = require('../configuration'); + +/* Stage an email for re-verification (i.e. after account password reset). + * Causes an email to be sent. */ + +exports.method = 'post'; +exports.writes_db = true; +exports.authed = 'assertion'; +exports.args = ['email','site']; +exports.i18n = true; + +exports.process = function(req, res) { + // validate + try { + sanitize(req.body.email).isEmail(); + sanitize(req.body.site).isOrigin(); + } catch(e) { + var msg = "invalid arguments: " + e; + logger.warn("bad request received: " + msg); + return httputils.badRequest(res, msg); + } + + // Note, we do no throttling of emails in this case. Because this call requires + // authentication, protect a user from themselves could cause more harm than good, + // specifically we would be removing a user available workaround (i.e. a cosmic ray + // hits our email delivery, user doesn't get an email in 30s. User tries again.) + + // one may only reverify an email that is owned and unverified + db.userOwnsEmail(req.session.userid, req.body.email, function(err, owned) { + if (err) return res.json({ success: false, reason: err }); + if (!owned) return res.json({ success: false, reason: 'you don\'t control that email address' }); + + db.emailIsVerified(req.body.email, function(err, verified) { + if (err) return res.json({ success: false, reason: err }); + if (verified) return res.json({ success: false, reason: 'email is already verified' }); + + try { + // on failure stageEmail may throw + db.stageEmail(req.session.userid, req.body.email, undefined, function(err, secret) { + if (err) return wsapi.databaseDown(res, err); + + var langContext = wsapi.langContext(req); + + // store the email being reverified + req.session.pendingReverification = secret; + + res.json({ success: true }); + // let's now kick out a verification email! + email.sendConfirmationEmail(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/common/js/network.js b/resources/static/common/js/network.js index 26397bc3ea7beb72971b3b55455628da763a44b4..ee7de6e0cbaeb01a0e44f7a05757a5a4bb70b0a5 100644 --- a/resources/static/common/js/network.js +++ b/resources/static/common/js/network.js @@ -83,6 +83,54 @@ BrowserID.Network = (function() { } } + function stageAddressForVerification(data, wsapiName, onComplete, onFailure) { + post({ + url: wsapiName, + data: data, + success: function(status) { + complete(onComplete, status.success); + }, + error: function(info) { + // 429 is throttling. + if (info.network.status === 429) { + complete(onComplete, false); + } + else complete(onFailure, info); + } + }); + } + + function handleAddressVerifyCheckResponse(onComplete, status, textStatus, jqXHR) { + if (status.status === 'complete') { + // The user at this point can ONLY be logged in with password + // authentication. Once the registration is complete, that means + // the server has updated the user's cookies and the user is + // officially authenticated. + auth_status = 'password'; + + if (status.userid) setUserID(status.userid); + } + complete(onComplete, status.status); + } + + function completeAddressVerification(wsapiName, token, password, onComplete, onFailure) { + post({ + url: wsapiName, + data: { + token: token, + pass: password + }, + success: function(status, textStatus, jqXHR) { + // If the user has successfully completed an address verification, + // they are authenticated to the password status. + if (status.success) auth_status = "password"; + complete(onComplete, status.success); + }, + error: onFailure + }); + + } + var Network = { /** * Initialize - Clear all context info. Used for testing. @@ -161,7 +209,7 @@ BrowserID.Network = (function() { }, withContext: function(onComplete, onFailure) { - withContext(onComplete, onFailure) + withContext(onComplete, onFailure); }, /** @@ -213,24 +261,12 @@ BrowserID.Network = (function() { * @param {function} [onFailure] - Called on XHR failure. */ createUser: function(email, password, origin, onComplete, onFailure) { - post({ - url: "/wsapi/stage_user", - data: { - email: email, - pass: password, - site : origin - }, - success: function(status) { - complete(onComplete, status.success); - }, - error: function(info) { - // 429 is throttling. - if (info.network.status === 429) { - complete(onComplete, false); - } - else complete(onFailure, info); - } - }); + var postData = { + email: email, + pass: password, + site : origin + }; + stageAddressForVerification(postData, "/wsapi/stage_user", onComplete, onFailure); }, /** @@ -265,17 +301,7 @@ BrowserID.Network = (function() { checkUserRegistration: function(email, onComplete, onFailure) { get({ url: "/wsapi/user_creation_status?email=" + encodeURIComponent(email), - success: function(status, textStatus, jqXHR) { - if (status.status === 'complete' && status.userid) { - // The user at this point can ONLY be logged in with password - // authentication. Once the registration is complete, that means - // the server has updated the user's cookies and the user is - // officially authenticated. - auth_status = 'password'; - setUserID(status.userid); - } - complete(onComplete, status.status); - }, + success: handleAddressVerifyCheckResponse.curry(onComplete), error: onFailure }); }, @@ -288,19 +314,7 @@ BrowserID.Network = (function() { * @param {function} [onComplete] - Called when complete. * @param {function} [onFailure] - Called on XHR failure. */ - completeUserRegistration: function(token, password, onComplete, onFailure) { - post({ - url: "/wsapi/complete_user_creation", - data: { - token: token, - pass: password - }, - success: function(status, textStatus, jqXHR) { - complete(onComplete, status.success); - }, - error: onFailure - }); - }, + completeUserRegistration: completeAddressVerification.curry("/wsapi/complete_user_creation"), /** * Call with a token to prove an email address ownership. @@ -311,19 +325,7 @@ BrowserID.Network = (function() { * with one boolean parameter that specifies the validity of the token. * @param {function} [onFailure] - Called on XHR failure. */ - completeEmailRegistration: function(token, password, onComplete, onFailure) { - post({ - url: "/wsapi/complete_email_addition", - data: { - token: token, - pass: password - }, - success: function(status, textStatus, jqXHR) { - complete(onComplete, status.success); - }, - error: onFailure - }); - }, + completeEmailRegistration: completeAddressVerification.curry("/wsapi/complete_email_confirmation"), /** * Request a password reset for the given email address. @@ -335,14 +337,74 @@ BrowserID.Network = (function() { * @param {function} [onFailure] - Called on XHR failure. */ requestPasswordReset: function(email, password, origin, onComplete, onFailure) { - if (email) { - Network.createUser(email, password, origin, onComplete, onFailure); - } else { - // TODO: if no email is provided, then what? - throw "no email provided to password reset"; - } + var postData = { + email: email, + pass: password, + site : origin + }; + stageAddressForVerification(postData, "/wsapi/stage_reset", onComplete, onFailure); + }, + + /** + * Complete email reset password + * @method completePasswordReset + * @param {string} token - token to register for. + * @param {string} password + * @param {function} [onComplete] - Called when complete. + * @param {function} [onFailure] - Called on XHR failure. + */ + completePasswordReset: completeAddressVerification.curry("/wsapi/complete_reset"), + + /** + * Check the registration status of a password reset + * @method checkPasswordReset + * @param {function} [onsuccess] - called when complete. + * @param {function} [onfailure] - called on xhr failure. + */ + checkPasswordReset: function(email, onComplete, onFailure) { + get({ + url: "/wsapi/password_reset_status?email=" + encodeURIComponent(email), + success: handleAddressVerifyCheckResponse.curry(onComplete), + error: onFailure + }); + }, + + /** + * Stage an email reverification. + * @method requestEmailReverify + * @param {string} email + * @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. + */ + requestEmailReverify: function(email, origin, onComplete, onFailure) { + var postData = { + email: email, + site : origin + }; + stageAddressForVerification(postData, "/wsapi/stage_reverify", onComplete, onFailure); + }, + + // the verification page for reverifying an email and adding an email to an + // account are the same, both are handled by the /confirm page. the + // /confirm page uses the verifyEmail function. completeEmailReverify is + // not needed. + + /** + * Check the registration status of an email reverification + * @method checkEmailReverify + * @param {function} [onsuccess] - called when complete. + * @param {function} [onfailure] - called on xhr failure. + */ + checkEmailReverify: function(email, onComplete, onFailure) { + get({ + url: "/wsapi/email_reverify_status?email=" + encodeURIComponent(email), + success: handleAddressVerifyCheckResponse.curry(onComplete), + error: onFailure + }); }, + /** * Set the password of the current user. * @method setPassword @@ -453,27 +515,14 @@ BrowserID.Network = (function() { * @param {function} [onFailure] - called on xhr failure. */ addSecondaryEmail: function(email, password, origin, onComplete, onFailure) { - post({ - url: "/wsapi/stage_email", - data: { - email: email, - pass: password, - site: origin - }, - success: function(response) { - complete(onComplete, response.success); - }, - error: function(info) { - // 429 is throttling. - if (info.network.status === 429) { - complete(onComplete, false); - } - else complete(onFailure, info); - } - }); + var postData = { + email: email, + pass: password, + site : origin + }; + stageAddressForVerification(postData, "/wsapi/stage_email", onComplete, onFailure); }, - /** * Check the registration status of an email * @method checkEmailRegistration @@ -483,9 +532,7 @@ BrowserID.Network = (function() { checkEmailRegistration: function(email, onComplete, onFailure) { get({ url: "/wsapi/email_addition_status?email=" + encodeURIComponent(email), - success: function(status, textStatus, jqXHR) { - complete(onComplete, status.status); - }, + success: handleAddressVerifyCheckResponse.curry(onComplete), error: onFailure }); }, @@ -666,6 +713,7 @@ BrowserID.Network = (function() { // Make sure we get context first or else we will needlessly send // a cookie to the server. withContext(function() { + var enabled; try { // set a test cookie with a duration of 1 second. // NOTE - The Android 3.3 and 4.0 default browsers will still pass @@ -674,7 +722,7 @@ BrowserID.Network = (function() { // 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; + enabled = document.cookie.indexOf("test") > -1; } catch(e) { enabled = false; } diff --git a/resources/static/common/js/user.js b/resources/static/common/js/user.js index b1be1696d9d8cddb49f66ba00eaa51f88060a168..5438da17921fa3b5a5889d8a37a58614b51af0a0 100644 --- a/resources/static/common/js/user.js +++ b/resources/static/common/js/user.js @@ -11,7 +11,8 @@ BrowserID.User = (function() { bid = BrowserID, network = bid.Network, storage = bid.Storage, - User, pollTimeout, + User, + pollTimeout, provisioning = bid.Provisioning, addressCache = {}, primaryAuthCache = {}, @@ -100,7 +101,53 @@ BrowserID.User = (function() { } } - function registrationPoll(checkFunc, email, onSuccess, onFailure) { + function handleStageAddressVerifictionResponse(onComplete, staged) { + var status = { success: staged }; + + if (!staged) status.reason = "throttle"; + // Used on the main site when the user verifies - once + // verification is complete, the user is redirected back to the + // RP and logged in. + var site = User.getReturnTo(); + if (staged && site) storage.setReturnTo(site); + + complete(onComplete, status); + } + + function completeAddressVerification(completeFunc, token, password, onComplete, onFailure) { + User.tokenInfo(token, function(info) { + var invalidInfo = { valid: false }; + if (info) { + completeFunc(token, password, function (valid) { + var result = invalidInfo; + + if (valid) { + result = _.extend({ valid: valid }, info); + var email = info.email, + idInfo = storage.getEmail(email); + + // Now that the address is verified, its verified bit has to be + // updated as well or else the user will be forced to verify the + // address again. + if (idInfo) { + idInfo.verified = true; + storage.addEmail(email, idInfo); + } + + storage.setReturnTo(""); + } + + complete(onComplete, result); + }, onFailure); + } else if (onComplete) { + onComplete(invalidInfo); + } + }, onFailure); + + } + + + function addressVerificationPoll(checkFunc, email, onSuccess, onFailure) { function poll() { checkFunc(email, function(status) { // registration status checks the status of the last initiated registration, @@ -117,7 +164,7 @@ BrowserID.User = (function() { // To avoid too many address_info requests, returns from each // address_info request are cached. If the user is doing - // a registrationPoll, it means the user was registering the address + // a addressVerificationPoll, it means the user was registering the address // and the registration has completed. Because the status is // "complete" or "known", we know that the address is known, so we // toggle the field to be up to date. If the known field remains @@ -206,21 +253,21 @@ BrowserID.User = (function() { /** * Persist an email address without a keypair * @method persistEmail - * @param {string} email - Email address to persist. - * @param {string} type - Is the email a 'primary' or a 'secondary' address? - * @param {function} [onComplete] - Called on successful completion. - * @param {function} [onFailure] - Called on error. + * @param {object} options - options to save + * @param {string} options.email - Email address to persist. + * @param {string} options.type - Is the email a 'primary' or a 'secondary' address? + * @param {string} options.verified - If the email is 'secondary', is it verified? */ - function persistEmail(email, type, onComplete, onFailure) { - checkEmailType(type); - storage.addEmail(email, { + function persistEmail(options) { + checkEmailType(options.type); + storage.addEmail(options.email, { created: new Date(), - type: type + type: options.type, + verified: options.verified }); - - if (onComplete) onComplete(true); } + User = { init: function(config) { if (config.provisioning) { @@ -302,18 +349,13 @@ BrowserID.User = (function() { * @param {function} [onFailure] - Called on error. */ createSecondaryUser: function(email, password, onComplete, onFailure) { - network.createUser(email, password, origin, function(created) { - // Used on the main site when the user verifies - once verification - // is complete, the user is redirected back to the RP and logged in. - var site = User.getReturnTo(); - if (created && site) storage.setReturnTo(site); - complete(onComplete, created); - }, onFailure); + network.createUser(email, password, origin, + handleStageAddressVerifictionResponse.curry(onComplete), onFailure); }, /** * Create a primary user. - * @method createUser + * @method createPrimaryUser * @param {object} info * @param {function} onComplete - function to call on complettion. Called * with two parameters - status and info. @@ -477,9 +519,7 @@ BrowserID.User = (function() { * @param {function} [onSuccess] - Called to give status updates. * @param {function} [onFailure] - Called on error. */ - waitForUserValidation: function(email, onSuccess, onFailure) { - registrationPoll(network.checkUserRegistration, email, onSuccess, onFailure); - }, + waitForUserValidation: addressVerificationPoll.curry(network.checkUserRegistration), /** * Cancel the waitForUserValidation poll @@ -517,25 +557,7 @@ BrowserID.User = (function() { * with valid=false otw. * @param {function} [onFailure] - Called on error. */ - verifyUser: function(token, password, onComplete, onFailure) { - User.tokenInfo(token, function(info) { - var invalidInfo = { valid: false }; - if (info) { - network.completeUserRegistration(token, password, function (valid) { - var result = invalidInfo; - - if(valid) { - result = _.extend({ valid: valid, returnTo: storage.getReturnTo() }, info); - storage.setReturnTo(""); - } - - complete(onComplete, result); - }, onFailure); - } else if (onComplete) { - onComplete(invalidInfo); - } - }, onFailure); - }, + verifyUser: completeAddressVerification.curry(network.completeUserRegistration), /** * Check if the user can set their password. Only returns true for users @@ -588,18 +610,8 @@ BrowserID.User = (function() { requestPasswordReset: function(email, password, onComplete, onFailure) { User.isEmailRegistered(email, function(registered) { if (registered) { - network.requestPasswordReset(email, password, origin, function(reset) { - var status = { success: reset }; - - if (!reset) status.reason = "throttle"; - // Used on the main site when the user verifies - once - // verification is complete, the user is redirected back to the - // RP and logged in. - var site = User.getReturnTo(); - if (reset && site) storage.setReturnTo(site); - - complete(onComplete, status); - }, onFailure); + network.requestPasswordReset(email, password, origin, + handleStageAddressVerifictionResponse.curry(onComplete), onFailure); } else if (onComplete) { onComplete({ success: false, reason: "invalid_user" }); @@ -607,6 +619,77 @@ BrowserID.User = (function() { }, onFailure); }, + /** + * Verify the password reset for a user. + * @method completePasswordReset + * @param {string} token - token to verify. + * @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. + */ + completePasswordReset: completeAddressVerification.curry(network.completePasswordReset), + + /** + * Wait for the password reset to complete + * @method waitForPasswordResetComplete + * @param {string} email - email address to check. + * @param {function} [onSuccess] - Called to give status updates. + * @param {function} [onFailure] - Called on error. + */ + waitForPasswordResetComplete: addressVerificationPoll.curry(network.checkPasswordReset), + + /** + * Cancel the waitForPasswordResetComplete poll + * @method cancelWaitForPasswordResetComplete + */ + cancelWaitForPasswordResetComplete: cancelRegistrationPoll, + + /** + * Request the reverification of an unverified email address + * @method requestEmailReverify + * @param {string} email + * @param {function} [onComplete] + * @param {function} [onFailure] + */ + requestEmailReverify: function(email, onComplete, onFailure) { + var idInfo = storage.getEmail(email); + if (!idInfo) { + // user does not own this address. + complete(onComplete, { success: false, reason: "invalid_email" }); + } + else if (idInfo.verified) { + // this email is already verified, cannot be reverified. + complete(onComplete, { success: false, reason: "verified_email" }); + } + else if (!idInfo.verified) { + // this address is unverified, try to reverify it. + network.requestEmailReverify(email, origin, + handleStageAddressVerifictionResponse.curry(onComplete), onFailure); + } + }, + + // the verification page for reverifying an email and adding an email to an + // account are the same, both are handled by the /confirm page. the + // /confirm page uses the verifyEmail function. completeEmailReverify is + // not needed. + + /** + * Wait for the email reverification to complete + * @method waitForEmailReverifyComplete + * @param {string} email - email address to check. + * @param {function} [onSuccess] - Called to give status updates. + * @param {function} [onFailure] - Called on error. + */ + waitForEmailReverifyComplete: addressVerificationPoll.curry(network.checkEmailReverify), + + /** + * Cancel the waitForEmailReverifyComplete poll + * @method cancelWaitForEmailReverifyComplete + */ + cancelWaitForEmailReverifyComplete: cancelRegistrationPoll, + /** * Cancel the current user's account. Remove last traces of their * identity. @@ -659,29 +742,39 @@ BrowserID.User = (function() { var emails_to_add = _.difference(server_emails, client_emails); var emails_to_remove = _.difference(client_emails, server_emails); + var emails_to_update = _.intersection(client_emails, server_emails); // remove emails _.each(emails_to_remove, function(email) { storage.removeEmail(email); }); - // keygen for new emails - // asynchronous - function addNextEmail() { - if (!emails_to_add || !emails_to_add.length) { - onComplete(); - return; - } + // these are new emails + _.each(emails_to_add, function(email) { + var emailInfo = emails[email]; - var email = emails_to_add.shift(); + persistEmail({ + email: email, + type: emailInfo.type || "secondary", + verified: emailInfo.verified + }); + }); - // extract the email type from the server response, if it - // doesn't exist, assume secondary - var type = emails[email].type || "secondary"; - persistEmail(email, type, addNextEmail, onFailure); - } + // update the type and verified status of stored emails + _.each(emails_to_update, function(email) { + var emailInfo = emails[email], + storedEmailInfo = storage.getEmail(email); + + _.extend(storedEmailInfo, { + type: emailInfo.type, + verified: emailInfo.verified + }); + + storage.addEmail(email, storedEmailInfo); + }); + + complete(onComplete); - addNextEmail(); }, onFailure); }); }, @@ -891,9 +984,7 @@ BrowserID.User = (function() { * @param {function} [onSuccess] - Called to give status updates. * @param {function} [onFailure] - Called on error. */ - waitForEmailValidation: function(email, onSuccess, onFailure) { - registrationPoll(network.checkEmailRegistration, email, onSuccess, onFailure); - }, + waitForEmailValidation: addressVerificationPoll.curry(network.checkEmailRegistration), /** * Cancel the waitForEmailValidation poll @@ -913,25 +1004,7 @@ BrowserID.User = (function() { * with valid=false otw. * @param {function} [onFailure] - Called on error. */ - verifyEmail: function(token, password, onComplete, onFailure) { - network.emailForVerificationToken(token, function (info) { - var invalidInfo = { valid: false }; - if (info) { - network.completeEmailRegistration(token, password, function (valid) { - var result = invalidInfo; - - if(valid) { - result = _.extend({ valid: valid, returnTo: storage.getReturnTo() }, info); - storage.setReturnTo(""); - } - - complete(onComplete, result); - }, onFailure); - } else { - complete(onComplete, invalidInfo); - } - }, onFailure); - }, + verifyEmail: completeAddressVerification.curry(network.completeEmailRegistration), /** * Remove an email address. diff --git a/resources/static/dialog/js/misc/helpers.js b/resources/static/dialog/js/misc/helpers.js index 6a0cae05bdfa6e35aa536e44c11f813eca400f13..949eac7a2f400aad97af58ff0edca8ffdfc4adef 100644 --- a/resources/static/dialog/js/misc/helpers.js +++ b/resources/static/dialog/js/misc/helpers.js @@ -74,7 +74,7 @@ function createUser(email, password, callback) { var self=this; user.createSecondaryUser(email, password, function(status) { - if (status) { + if (status.success) { var info = { email: email, password: password }; self.publish("user_staged", info, info); complete(callback, true); @@ -92,7 +92,20 @@ var self=this; user.requestPasswordReset(email, password, function(status) { if (status.success) { - self.publish("password_reset", { email: email }); + self.publish("reset_password_staged", { email: email }); + } + else { + tooltip.showTooltip("#could_not_add"); + } + complete(callback, status.success); + }, self.getErrorDialog(errors.requestPasswordReset, callback)); + } + + function reverifyEmail(email, callback) { + var self=this; + user.requestEmailReverify(email, function(status) { + if (status.success) { + self.publish("reverify_email_staged", { email: email }); } else { tooltip.showTooltip("#could_not_add"); @@ -152,6 +165,7 @@ addEmail: addEmail, addSecondaryEmail: addSecondaryEmail, resetPassword: resetPassword, + reverifyEmail: reverifyEmail, cancelEvent: helpers.cancelEvent, animateClose: animateClose, showRPTosPP: showRPTosPP diff --git a/resources/static/dialog/js/misc/state.js b/resources/static/dialog/js/misc/state.js index 65fdc9b0e691806dc8929219fa4acd4b6e9710dd..b9f1f7db346016bf81d7b1a735d7f2348112adab 100644 --- a/resources/static/dialog/js/misc/state.js +++ b/resources/static/dialog/js/misc/state.js @@ -1,5 +1,5 @@ /*jshint browser:true, jquery: true, forin: true, laxbreak:true */ -/*global BrowserID: true */ +/*global BrowserID: true, URLParse: 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/. */ @@ -43,6 +43,20 @@ BrowserID.State = (function() { }, cancelState = self.popState.bind(self); + function handleEmailStaged(actionName, msg, info) { + // The unverified email has been staged, now the user has to confirm + // ownership of the address. Send them off to the "verify your address" + // screen. + var actionInfo = { + email: info.email, + siteName: self.siteName + }; + + self.stagedEmail = info.email; + startAction(actionName, actionInfo); + } + + handleState("start", function(msg, info) { self.hostname = info.hostname; self.siteName = info.siteName || info.hostname; @@ -152,22 +166,18 @@ BrowserID.State = (function() { } else if(self.resetPasswordEmail) { self.resetPasswordEmail = null; - startAction(false, "doResetPassword", info); + startAction(false, "doStageResetPassword", info); } }); - handleState("user_staged", function(msg, info) { - self.stagedEmail = info.email; - - _.extend(info, { - required: !!requiredEmail, - siteName: self.siteName - }); + handleState("user_staged", handleEmailStaged.curry("doConfirmUser")); - startAction("doConfirmUser", info); + handleState("user_confirmed", function() { + self.email = self.stagedEmail; + redirectToState("email_chosen", { email: self.stagedEmail} ); }); - handleState("user_confirmed", function() { + handleState("staged_address_confirmed", function() { self.email = self.stagedEmail; redirectToState("email_chosen", { email: self.stagedEmail} ); }); @@ -274,53 +284,72 @@ BrowserID.State = (function() { complete(info.complete); } - if (idInfo) { - mediator.publish("kpi_data", { email_type: idInfo.type }); + if (!idInfo) { + throw "invalid email"; + } - if (idInfo.type === "primary") { - if (idInfo.cert) { - // Email is a primary and the cert is available - the user can log - // in without authenticating with the IdP. All invalid/expired - // certs are assumed to have been checked and removed by this - // point. - redirectToState("email_valid_and_ready", info); - } - else { - // If the email is a primary and the cert is not available, - // throw the user down the primary flow. The primary flow will - // catch cases where the primary certificate is expired - // and the user must re-verify with their IdP. - redirectToState("primary_user", info); - } + mediator.publish("kpi_data", { email_type: idInfo.type }); + + if (idInfo.type === "primary") { + if (idInfo.cert) { + // Email is a primary and the cert is available - the user can log + // in without authenticating with the IdP. All invalid/expired + // certs are assumed to have been checked and removed by this + // point. + redirectToState("email_valid_and_ready", info); } else { - user.checkAuthentication(function(authentication) { - if (authentication === "assertion") { - // user must authenticate with their password, kick them over to - // the required email screen to enter the password. - startAction("doAuthenticateWithRequiredEmail", { - email: email, - secondary_auth: true, - - // This is a user is already authenticated to the assertion - // level who has chosen a secondary email address from the - // pick_email screen. They would have been shown the - // siteTOSPP there. - siteTOSPP: false - }); - } - else { - redirectToState("email_valid_and_ready", info); - } - oncomplete(); - }, oncomplete); + // If the email is a primary and the cert is not available, + // throw the user down the primary flow. The primary flow will + // catch cases where the primary certificate is expired + // and the user must re-verify with their IdP. + redirectToState("primary_user", info); } } + // Anything below this point means the address is a secondary. + else if (!idInfo.verified) { + // user selected an unverified secondary email, kick them over to the + // verify screen. + redirectToState("stage_reverify_email", info); + } else { - throw "invalid email"; + // Address is verified, check the authentication, if the user is not + // authenticated to the assertion level, force them to enter their + // password. + user.checkAuthentication(function(authentication) { + if (authentication === "assertion") { + // user must authenticate with their password, kick them over to + // the required email screen to enter the password. + startAction("doAuthenticateWithRequiredEmail", { + email: email, + secondary_auth: true, + + // This is a user is already authenticated to the assertion + // level who has chosen a secondary email address from the + // pick_email screen. They would have been shown the + // siteTOSPP there. + siteTOSPP: false + }); + } + else { + redirectToState("email_valid_and_ready", info); + } + oncomplete(); + }, oncomplete); } }); + handleState("stage_reverify_email", function(msg, info) { + // A user has selected an email that has not been verified after + // a password reset. Stage the email again to be re-verified. + var actionInfo = { + email: info.email + }; + startAction("doStageReverifyEmail", actionInfo); + }); + + handleState("reverify_email_staged", handleEmailStaged.curry("doConfirmReverifyEmail")); + handleState("email_valid_and_ready", function(msg, info) { // this state is only called after all checking is done on the email // address. For secondaries, this means the email has been validated and @@ -364,21 +393,14 @@ BrowserID.State = (function() { 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. + // the set_password handler needs to know the resetPasswordEmail so it + // knows how to trigger the reset_password_staged message. At this + // point, the email confirmation screen will be shown. self.resetPasswordEmail = info.email; - startAction(false, "doForgotPassword", info); + startAction(false, "doResetPassword", 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("reset_password_staged", handleEmailStaged.curry("doConfirmResetPassword")); handleState("assertion_generated", function(msg, info) { self.success = true; @@ -404,19 +426,6 @@ BrowserID.State = (function() { redirectToState("email_chosen", 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) { // add_email indicates the user wishes to add an email to the account, // the add_email screen must be displayed. After the user enters the @@ -461,14 +470,7 @@ BrowserID.State = (function() { }); }); - handleState("email_staged", function(msg, info) { - self.stagedEmail = info.email; - _.extend(info, { - required: !!requiredEmail, - siteName: self.siteName - }); - startAction("doConfirmEmail", info); - }); + handleState("email_staged", handleEmailStaged.curry("doConfirmEmail")); handleState("email_confirmed", function() { redirectToState("email_chosen", { email: self.stagedEmail } ); diff --git a/resources/static/dialog/js/modules/actions.js b/resources/static/dialog/js/modules/actions.js index aec8e96a38ca1abfa54e69e683aa2bd7f2134c99..02c6b5748c5eb989656bf8de8e61429172742e91 100644 --- a/resources/static/dialog/js/modules/actions.js +++ b/resources/static/dialog/js/modules/actions.js @@ -32,11 +32,11 @@ BrowserID.Modules.Actions = (function() { return module; } - function startRegCheckService(options, verifier, message, password) { + function startRegCheckService(options, verifier, message) { var controller = startService("check_registration", { verifier: verifier, verificationMessage: message, - password: password, + password: options.password, siteName: options.siteName, email: options.email }); @@ -70,7 +70,7 @@ BrowserID.Modules.Actions = (function() { }, doConfirmUser: function(info) { - startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed", info.password || undefined); + startRegCheckService.call(this, info, "waitForUserValidation", "user_confirmed"); }, doPickEmail: function(info) { @@ -85,6 +85,10 @@ BrowserID.Modules.Actions = (function() { dialogHelpers.addSecondaryEmail.call(this, info.email, info.password, info.ready); }, + doConfirmEmail: function(info) { + startRegCheckService.call(this, info, "waitForEmailValidation", "email_confirmed"); + }, + doAuthenticate: function(info) { startService("authenticate", info); }, @@ -93,16 +97,24 @@ BrowserID.Modules.Actions = (function() { startService("required_email", info); }, - doForgotPassword: function(info) { + doResetPassword: function(info) { startService("set_password", _.extend(info, { password_reset: true })); }, - doResetPassword: function(info) { + doStageResetPassword: function(info) { dialogHelpers.resetPassword.call(this, info.email, info.password, info.ready); }, - doConfirmEmail: function(info) { - startRegCheckService.call(this, info, "waitForEmailValidation", "email_confirmed"); + doConfirmResetPassword: function(info) { + startRegCheckService.call(this, info, "waitForPasswordResetComplete", "staged_address_confirmed"); + }, + + doStageReverifyEmail: function(info) { + dialogHelpers.reverifyEmail.call(this, info.email, info.ready); + }, + + doConfirmReverifyEmail: function(info) { + startRegCheckService.call(this, info, "waitForEmailReverifyComplete", "staged_address_confirmed"); }, doAssertionGenerated: function(info) { diff --git a/resources/static/pages/js/signup.js b/resources/static/pages/js/signup.js index d7d4049ba49380eddc94045312655fd0685d1c5f..49677c8e48721f6fb75f3e2a14726716569617be 100644 --- a/resources/static/pages/js/signup.js +++ b/resources/static/pages/js/signup.js @@ -80,7 +80,7 @@ BrowserID.signUp = (function() { if(valid) { user.createSecondaryUser(this.emailToStage, pass, function(status) { - if(status) { + if(status.success) { pageHelpers.emailSent(oncomplete && oncomplete.curry(true)); } else { diff --git a/resources/static/pages/js/start.js b/resources/static/pages/js/start.js index fdcdcf2f644cdb65cf8b6e335d2192f728bbe3bb..4fa4b83a5e1115f391df2ff709e9e967947edfad 100644 --- a/resources/static/pages/js/start.js +++ b/resources/static/pages/js/start.js @@ -26,7 +26,7 @@ $(function() { XHRDisableForm = modules.XHRDisableForm, Development = modules.Development, ANIMATION_TIME = 500, - checkCookiePaths = [ "/signin", "/signup", "/forgot", "/add_email_address", "/verify_email_address" ]; + checkCookiePaths = [ "/signin", "/signup", "/forgot", "/add_email_address", "/confirm", "/verify_email_address" ]; function shouldCheckCookies(path) { @@ -117,6 +117,14 @@ $(function() { start(true); } + function verifySecondaryAddress(verifyFunction) { + var module = bid.verifySecondaryAddress.create(); + module.start({ + token: token, + verifyFunction: verifyFunction + }); + } + function start(status) { // If cookies are disabled, do not run any of the page specific code and // instead just show the error message. @@ -137,19 +145,22 @@ $(function() { else if (path === "/forgot") { bid.forgot(); } + // START TRANSITION CODE + // add_email_address has been renamed to confirm. Once all outstanding + // emails are verified or expired, this can be removed. This change is + // scheduled to go into train-2012.07.20 else if (path === "/add_email_address") { - var module = bid.verifySecondaryAddress.create(); - module.start({ - token: token, - verifyFunction: "verifyEmail" - }); + verifySecondaryAddress("verifyEmail"); + } + // END TRANSITION CODE + else if (path === "/confirm") { + verifySecondaryAddress("verifyEmail"); } else if (path === "/verify_email_address") { - var module = bid.verifySecondaryAddress.create(); - module.start({ - token: token, - verifyFunction: "verifyUser" - }); + verifySecondaryAddress("verifyUser"); + } + else if (path === "/reset_password") { + verifySecondaryAddress("completePasswordReset"); } else if (path === "/about") { var module = bid.about.create(); diff --git a/resources/static/test/cases/common/js/network.js b/resources/static/test/cases/common/js/network.js index 5fac936df6c34fa014b5c2e2cf59d2b36de319d9..7000cf007a253c8277d90095f023326bef838fa4 100644 --- a/resources/static/test/cases/common/js/network.js +++ b/resources/static/test/cases/common/js/network.js @@ -17,7 +17,7 @@ var network = BrowserID.Network; - module("shared/network", { + module("common/js/network", { setup: function() { testHelpers.setup(); }, @@ -26,6 +26,43 @@ } }); + function testVerificationPending(funcName) { + transport.useResult("pending"); + + network[funcName]("registered@testuser.com", function(status) { + equal(status, "pending"); + start(); + }, testHelpers.unexpectedFailure); + } + + function testVerificationMustAuth(funcName) { + transport.useResult("mustAuth"); + + network.checkAuth(function(auth_status) { + equal(!!auth_status, false, "user not yet authenticated"); + network[funcName]("registered@testuser.com", function(status) { + equal(status, "mustAuth"); + network.checkAuth(function(auth_status) { + equal(!!auth_status, false, "user not yet authenticated"); + start(); + }, testHelpers.unexpectedFailure); + }, testHelpers.unexpectedFailure); + }, testHelpers.unexpectedFailure); + } + + function testVerificationComplete(funcName) { + network.withContext(function() { + transport.useResult("complete"); + network[funcName]("registered@testuser.com", function(status) { + equal(status, "complete"); + network.checkAuth(function(auth_level) { + equal(auth_level, "password", "user can only be authenticated to password level after verification is complete"); + start(); + }); + }, testHelpers.unexpectedFailure); + }); + } + asyncTest("authenticate with valid user", function() { network.authenticate(TEST_EMAIL, "testuser", function onSuccess(authenticated) { @@ -78,7 +115,7 @@ delayInfo = delay_info; }); - var completeInfo + var completeInfo; mediator.subscribe("xhr_complete", function(msg, complete_info) { completeInfo = complete_info; }); @@ -376,27 +413,9 @@ failureCheck(network.addSecondaryEmail, TEST_EMAIL, TEST_PASSWORD, "origin"); }); - asyncTest("checkEmailRegistration pending", function() { - transport.useResult("pending"); - - network.checkEmailRegistration("registered@testuser.com", function(status) { - equal(status, "pending"); - start(); - }, testHelpers.unexpectedFailure); - }); - - asyncTest("checkEmailRegistration complete", function() { - transport.useResult("complete"); - - network.checkEmailRegistration("registered@testuser.com", function(status) { - equal(status, "complete"); - start(); - }, function onFailure() { - ok(false); - start(); - }); - - }); + asyncTest("checkEmailRegistration pending", testVerificationPending.curry("checkEmailRegistration")); + asyncTest("checkEmailRegistration mustAuth", testVerificationMustAuth.curry("checkEmailRegistration")); + asyncTest("checkEmailRegistration complete", testVerificationComplete.curry("checkEmailRegistration")); asyncTest("checkEmailRegistration with XHR failure", function() { failureCheck(network.checkEmailRegistration, TEST_EMAIL); @@ -488,6 +507,61 @@ failureCheck(network.requestPasswordReset, TEST_EMAIL, "password", "origin"); }); + asyncTest("completePasswordReset with valid token, no password required", function() { + network.completePasswordReset("token", undefined, function(registered) { + ok(registered); + start(); + }, testHelpers.unexpectedFailure); + }); + + asyncTest("completePasswordReset with valid token, bad password", function() { + transport.useResult("badPassword"); + network.completePasswordReset("token", "password", + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure); + }); + + asyncTest("completePasswordReset with valid token, password required", function() { + network.completePasswordReset("token", "password", function(registered) { + ok(registered); + start(); + }, testHelpers.unexpectedFailure); + }); + + asyncTest("completePasswordReset with invalid token", function() { + transport.useResult("invalid"); + + network.completePasswordReset("token", "password", function(registered) { + equal(registered, false); + start(); + }, testHelpers.unexpectedFailure); + }); + + asyncTest("completePasswordReset with XHR failure", function() { + failureCheck(network.completePasswordReset, "token", "password"); + }); + + asyncTest("checkPasswordReset pending", testVerificationPending.curry("checkPasswordReset")); + asyncTest("checkPasswordReset mustAuth", testVerificationMustAuth.curry("checkPasswordReset")); + asyncTest("checkPasswordReset complete", testVerificationComplete.curry("checkPasswordReset")); + + + asyncTest("requestEmailReverify - true status", function() { + network.requestEmailReverify(TEST_EMAIL, "origin", function onSuccess(status) { + equal(status, true, "password reset request success"); + start(); + }, testHelpers.unexpectedFailure); + }); + + asyncTest("requestEmailReverify with XHR failure", function() { + failureCheck(network.requestEmailReverify, TEST_EMAIL, "origin"); + }); + + asyncTest("checkEmailReverify pending", testVerificationPending.curry("checkEmailReverify")); + asyncTest("checkEmailReverify mustAuth", testVerificationMustAuth.curry("checkEmailReverify")); + asyncTest("checkEmailReverify complete", testVerificationComplete.curry("checkEmailReverify")); + + asyncTest("setPassword happy case expects true status", function() { network.setPassword("password", function onComplete(status) { equal(status, true, "correct status"); diff --git a/resources/static/test/cases/common/js/user.js b/resources/static/test/cases/common/js/user.js index 585210585e97ad09063006488b9378834322ab08..1911f69a256b1623c871483d2c706c4345c1c3fd 100644 --- a/resources/static/test/cases/common/js/user.js +++ b/resources/static/test/cases/common/js/user.js @@ -3,10 +3,12 @@ /* 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/. */ -var jwcrypto = require("./lib/jwcrypto"); (function() { - var bid = BrowserID, + "use strict"; + + var jwcrypto = require("./lib/jwcrypto"), + bid = BrowserID, lib = bid.User, storage = bid.Storage, network = bid.Network, @@ -17,6 +19,7 @@ var jwcrypto = require("./lib/jwcrypto"); failureCheck = testHelpers.failureCheck, testUndefined = testHelpers.testUndefined, testNotUndefined = testHelpers.testNotUndefined, + testObjectValuesEqual = testHelpers.testObjectValuesEqual, provisioning = bid.Mocks.Provisioning, TEST_EMAIL = "testuser@testuser.com"; @@ -44,7 +47,7 @@ var jwcrypto = require("./lib/jwcrypto"); // Check for parts of the assertion equal(components.payload.aud, testOrigin, "correct audience"); - var expires = parseInt(components.payload.exp); + var expires = parseInt(components.payload.exp, 10); ok(typeof expires === "number" && !isNaN(expires), "expiration date is valid"); // this should be based on server time, not local time. @@ -65,7 +68,7 @@ var jwcrypto = require("./lib/jwcrypto"); }); } - module("shared/user", { + module("common/js/user", { setup: function() { testHelpers.setup(); }, @@ -155,7 +158,7 @@ var jwcrypto = require("./lib/jwcrypto"); asyncTest("createSecondaryUser success - callback with true status", function() { lib.createSecondaryUser(TEST_EMAIL, "password", function(status) { - ok(status, "user created"); + ok(status.success, "user created"); start(); }, testHelpers.unexpectedXHRFailure); }); @@ -164,7 +167,10 @@ var jwcrypto = require("./lib/jwcrypto"); xhr.useResult("throttle"); lib.createSecondaryUser(TEST_EMAIL, "password", function(status) { - equal(status, false, "user creation refused"); + testObjectValuesEqual(status, { + success: false, + reason: "throttle" + }); start(); }, testHelpers.unexpectedXHRFailure); }); @@ -412,13 +418,17 @@ var jwcrypto = require("./lib/jwcrypto"); asyncTest("verifyUser with a good token", function() { storage.setReturnTo(testOrigin); + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); lib.verifyUser("token", "password", function onSuccess(info) { - ok(info.valid, "token was valid"); - equal(info.email, TEST_EMAIL, "email part of info"); - equal(info.returnTo, testOrigin, "returnTo in info"); + testObjectValuesEqual(info, { + valid: true, + email: TEST_EMAIL, + returnTo: testOrigin + }); equal(storage.getReturnTo(), "", "initiating origin was removed"); + equal(storage.getEmail(TEST_EMAIL).verified, true, "email marked as verified"); start(); }, testHelpers.unexpectedXHRFailure); @@ -516,6 +526,99 @@ var jwcrypto = require("./lib/jwcrypto"); failureCheck(lib.requestPasswordReset, "registered@testuser.com", "password"); }); + asyncTest("completePasswordReset with a good token", function() { + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); + storage.setReturnTo(testOrigin); + + lib.completePasswordReset("token", "password", function onSuccess(info) { + testObjectValuesEqual(info, { + valid: true, + email: TEST_EMAIL, + returnTo: testOrigin, + }); + + equal(storage.getReturnTo(), "", "initiating origin was removed"); + equal(storage.getEmail(TEST_EMAIL).verified, true, "email now marked as verified"); + + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("completePasswordReset with a bad token", function() { + xhr.useResult("invalid"); + + lib.completePasswordReset("token", "password", function onSuccess(info) { + equal(info.valid, false, "bad token calls onSuccess with a false validity"); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("completePasswordReset with an XHR failure", function() { + xhr.useResult("ajaxError"); + + lib.completePasswordReset( + "token", + "password", + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure + ); + }); + + asyncTest("requestEmailReverify with owned verified email - false status", function() { + storage.addSecondaryEmail(TEST_EMAIL, { verified: true }); + + var returnTo = "http://samplerp.org"; + lib.setReturnTo(returnTo); + lib.requestEmailReverify(TEST_EMAIL, function(status) { + testObjectValuesEqual(status, { + success: false, + reason: "verified_email" + }); + + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("requestEmailReverify with owned unverified email - false status", function() { + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); + + var returnTo = "http://samplerp.org"; + lib.setReturnTo(returnTo); + lib.requestEmailReverify(TEST_EMAIL, function(status) { + equal(status.success, true, "password reset for known user"); + equal(storage.getReturnTo(), returnTo, "RP URL is stored for verification"); + + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("requestEmailReverify with unowned email - false status, invalid_user", function() { + lib.requestEmailReverify(TEST_EMAIL, function(status) { + testObjectValuesEqual(status, { + success: false, + reason: "invalid_email" + }); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("requestEmailReverify owned email with throttle - false status, throttle", function() { + xhr.useResult("throttle"); + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); + + lib.requestEmailReverify(TEST_EMAIL, function(status) { + testObjectValuesEqual(status, { + success: false, + reason: "throttle" + }); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("requestEmailReverify with XHR failure", function() { + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); + failureCheck(lib.requestEmailReverify, TEST_EMAIL); + }); asyncTest("authenticate with valid credentials, also syncs email with server", function() { lib.authenticate(TEST_EMAIL, "testuser", function(authenticated) { @@ -793,12 +896,15 @@ var jwcrypto = require("./lib/jwcrypto"); asyncTest("verifyEmail with a good token - callback with email, returnTo, valid", function() { storage.setReturnTo(testOrigin); + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); lib.verifyEmail("token", "password", function onSuccess(info) { - - ok(info.valid, "token was valid"); - equal(info.email, TEST_EMAIL, "email part of info"); - equal(info.returnTo, testOrigin, "returnTo in info"); + testObjectValuesEqual(info, { + valid: true, + email: TEST_EMAIL, + returnTo: testOrigin + }); equal(storage.getReturnTo(), "", "initiating returnTo was removed"); + equal(storage.getEmail(TEST_EMAIL).verified, true, "email now marked as verified"); start(); }, testHelpers.unexpectedXHRFailure); @@ -945,12 +1051,16 @@ var jwcrypto = require("./lib/jwcrypto"); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("syncEmails with one to refresh", function() { - storage.addEmail(TEST_EMAIL, {pub: pubkey, cert: random_cert}); + asyncTest("syncEmails with one to update", function() { + // verified is set to false here, the mock for list_emails has verified + // set to true. If emails are being updated, verified will be set to true + // whenever syncEmails is complete. + storage.addEmail(TEST_EMAIL, {pub: pubkey, cert: random_cert, verified: false}); lib.syncEmails(function onSuccess() { var identities = lib.getStoredEmailKeypairs(); ok(TEST_EMAIL in identities, "refreshed key is synced"); + equal(identities[TEST_EMAIL].verified, true, "verified was correctly updated"); start(); }, testHelpers.unexpectedXHRFailure); }); @@ -1262,7 +1372,6 @@ var jwcrypto = require("./lib/jwcrypto"); start(); }, testHelpers.expectedXHRFailure); }, testHelpers.unexpectedXHRFailure); - xhr.useResult }, testHelpers.unexpectedXHRFailure); }); diff --git a/resources/static/test/cases/dialog/js/misc/state.js b/resources/static/test/cases/dialog/js/misc/state.js index d6535638fd817d6385d7950077102dfbffd5fb72..eabca0b5089db9cb4676578c79593e42fb702ec4 100644 --- a/resources/static/test/cases/dialog/js/misc/state.js +++ b/resources/static/test/cases/dialog/js/misc/state.js @@ -42,6 +42,36 @@ } } + function testVerifyStagedAddress(startMessage, verifyScreenAction) { + // start with a site name to ensure the site name is passed to the + // verifyScreenAction. + mediator.publish("start", { siteName: "Unit Test Site" }); + mediator.publish(startMessage, { + email: TEST_EMAIL + }); + + testActionStarted(verifyScreenAction, { + email: TEST_EMAIL, + siteName: "Unit Test Site" + }); + + // At this point the user should be displayed the "go confirm your address" + // screen. Simulate the user completing the verification transaction. + + mediator.subscribe("email_chosen", function(msg, info) { + equal(info.email, TEST_EMAIL, "email_chosen triggered with the correct email"); + start(); + }); + + // staged_address_confirmed means the user has confirmed their email and the dialog + // has received the "complete" message from the polling function, and all + // addresses are synced. Add the test email and make sure the email_chosen + // message is triggered. + storage.addSecondaryEmail(TEST_EMAIL, { unverified: true }); + mediator.publish("staged_address_confirmed"); + } + + function createMachine() { machine = bid.State.create(); actions = new ActionsMock(); @@ -61,7 +91,7 @@ }); } - module("resources/state", { + module("dialog/js/misc/state", { setup: function() { testHelpers.setup(); createMachine(); @@ -128,11 +158,11 @@ equal(actions.info.doStageEmail.email, TEST_EMAIL, "correct email sent to doStageEmail"); }); - test("password_set for reset password - call doResetPassword with correct email", function() { + test("password_set for reset password - call doStageResetPassword 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"); + equal(actions.info.doStageResetPassword.email, TEST_EMAIL, "correct email sent to doStageResetPassword"); }); test("start - RPInfo always started", function() { @@ -145,17 +175,8 @@ ok(actions.info.doRPInfo.privacyPolicy, "doRPInfo called with privacyPolicy set"); }); - test("user_staged - call doConfirmUser", function() { - mediator.publish("user_staged", { email: TEST_EMAIL }); - - equal(actions.info.doConfirmUser.email, TEST_EMAIL, "waiting for email confirmation for testuser@testuser.com"); - }); - - test("user_staged with required email - call doConfirmUser with required = true", function() { - mediator.publish("start", { requiredEmail: TEST_EMAIL }); - mediator.publish("user_staged", { email: TEST_EMAIL }); - - equal(actions.info.doConfirmUser.required, true, "doConfirmUser called with required flag"); + asyncTest("user_staged - call doConfirmUser", function() { + testVerifyStagedAddress("user_staged", "doConfirmUser"); }); test("user_confirmed - redirect to email_chosen", function() { @@ -175,17 +196,8 @@ } }); - test("email_staged - call doConfirmEmail", function() { - mediator.publish("email_staged", { email: TEST_EMAIL }); - - equal(actions.info.doConfirmEmail.required, false, "doConfirmEmail called without required flag"); - }); - - test("email_staged with required email - call doConfirmEmail with required = true", function() { - mediator.publish("start", { requiredEmail: TEST_EMAIL }); - mediator.publish("email_staged", { email: TEST_EMAIL }); - - equal(actions.info.doConfirmEmail.required, true, "doConfirmEmail called with required flag"); + asyncTest("email_staged - call doConfirmEmail", function() { + testVerifyStagedAddress("email_staged", "doConfirmEmail"); }); asyncTest("primary_user with already provisioned primary user - redirect to primary_user_ready", function() { @@ -272,51 +284,20 @@ mediator.publish("authenticated", { email: TEST_EMAIL }); }); - test("forgot_password - call doForgotPassword with correct options", function() { + test("forgot_password - call doResetPassword with correct options", function() { mediator.publish("start", { privacyPolicy: "priv.html", termsOfService: "tos.html" }); mediator.publish("forgot_password", { email: TEST_EMAIL, requiredEmail: true }); - testActionStarted("doForgotPassword", { email: TEST_EMAIL, requiredEmail: true }); + testActionStarted("doResetPassword", { email: TEST_EMAIL, requiredEmail: true }); }); - 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("password_reset", { - email: TEST_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. - - // user_confirmed means the user has confirmed their email and the dialog - // has received the "complete" message from /wsapi/user_creation_status. - try { - mediator.publish("user_confirmed"); - } catch(e) { - // Exception is expected because as part of the user confirmation - // process, before user_confirmed is called, email addresses are synced. - // Addresses are not synced in this test. - equal(e.toString(), "invalid email", "expected failure"); - } + asyncTest("reset_password_staged to staged_address_confirmed - call doConfirmResetPassword then doEmailConfirmed", function() { + testVerifyStagedAddress("reset_password_staged", "doConfirmResetPassword"); }); - 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 password_reset flow. - mediator.publish("authenticate"); - mediator.publish("forgot_password", undefined, { 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"); - }); - asyncTest("assertion_generated with null assertion - redirect to pick_email", function() { mediator.subscribe("pick_email", function() { ok(true, "redirect to pick_email"); @@ -433,15 +414,14 @@ testActionStarted("doAddEmail"); }); - asyncTest("email_chosen with secondary email, user must authenticate - call doAuthenticateWithRequiredEmail", function() { - var email = TEST_EMAIL; - storage.addEmail(email, { type: "secondary" }); + asyncTest("email_chosen with verified secondary email, user must authenticate - call doAuthenticateWithRequiredEmail", function() { + storage.addSecondaryEmail(TEST_EMAIL, { verified: true }); xhr.setContextInfo("auth_level", "assertion"); mediator.publish("start", { privacyPolicy: "priv.html", termsOfService: "tos.html" }); mediator.publish("email_chosen", { - email: email, + email: TEST_EMAIL, complete: function() { testActionStarted("doAuthenticateWithRequiredEmail", { siteTOSPP: false }); start(); @@ -449,8 +429,8 @@ }); }); - asyncTest("email_chosen with secondary email, user authenticated to secondary - redirect to email_valid_and_ready", function() { - storage.addEmail(TEST_EMAIL, { type: "secondary" }); + asyncTest("email_chosen with verified secondary email, user authenticated to secondary - redirect to email_valid_and_ready", function() { + storage.addSecondaryEmail(TEST_EMAIL, { verified: true }); xhr.setContextInfo("auth_level", "password"); mediator.subscribe("email_valid_and_ready", function(msg, info) { @@ -463,6 +443,28 @@ }); }); + function testReverifyEmailChosen(auth_level) { + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); + xhr.setContextInfo("auth_level", auth_level); + + mediator.subscribe("stage_reverify_email", function(msg, info) { + equal(info.email, TEST_EMAIL, "correctly redirected to stage_reverify_email with correct email"); + start(); + }); + + mediator.publish("email_chosen", { + email: TEST_EMAIL + }); + } + + asyncTest("email_chosen with unverified secondary email, user authenticated to secondary - redirect to stage_reverify_email", function() { + testReverifyEmailChosen("password"); + }); + + asyncTest("email_chosen with unverified secondary email, user authenticated to primary - redirect to stage_reverify_email", function() { + testReverifyEmailChosen("assertion"); + }); + test("email_chosen with primary email - call doProvisionPrimaryUser", function() { // If the email is a primary, throw the user down the primary flow. // Doing so will catch cases where the primary certificate is expired @@ -544,6 +546,15 @@ }); }); + test("stage_reverify_email - call doStageReverifyEmail", function() { + mediator.publish("stage_reverify_email", { email: TEST_EMAIL }); + testActionStarted("doStageReverifyEmail", { email: TEST_EMAIL }); + }); + + test("reverify_email_staged - call doConfirmReverifyEmail", function() { + testVerifyStagedAddress("reverify_email_staged", "doConfirmReverifyEmail"); + }); + asyncTest("window_unload - set the final KPIs", function() { mediator.subscribe("kpi_data", function(msg, data) { testHelpers.testKeysInObject(data, [ diff --git a/resources/static/test/cases/dialog/js/modules/actions.js b/resources/static/test/cases/dialog/js/modules/actions.js index 3f98fc81b8d968879c1c34738a00b4eab5500111..7c44e045bcdeaa795310336ccc2db0452eea215b 100644 --- a/resources/static/test/cases/dialog/js/modules/actions.js +++ b/resources/static/test/cases/dialog/js/modules/actions.js @@ -8,6 +8,7 @@ var bid = BrowserID, user = bid.User, + storage = bid.Storage, controller, el, testHelpers = bid.TestHelpers, @@ -34,7 +35,29 @@ }); } - module("controllers/actions", { + function testStageAddress(actionName, expectedMessage) { + createController({ + ready: function() { + var message, + email; + + testHelpers.register(expectedMessage, function(msg, info) { + message = msg; + email = info.email; + }); + + controller[actionName]({ email: TEST_EMAIL, password: "password", ready: function(status) { + equal(status, true, "correct status"); + equal(message, expectedMessage, "correct message triggered"); + equal(email, TEST_EMAIL, "address successfully staged"); + start(); + }}); + } + }); + } + + + module("dialog/js/modules/actions", { setup: function() { testHelpers.setup(); }, @@ -74,39 +97,50 @@ "primary_user_provisioned"); }); + asyncTest("doStageUser with successful creation - trigger user_staged", function() { + testStageAddress("doStageUser", "user_staged"); + }); + asyncTest("doConfirmUser - start the check_registration service", function() { testActionStartsModule("doConfirmUser", {email: TEST_EMAIL, siteName: "Unit Test Site"}, "check_registration"); }); + asyncTest("doStageEmail with successful staging - trigger email_staged", function() { + testStageAddress("doStageEmail", "email_staged"); + }); + asyncTest("doConfirmEmail - start the check_registration service", function() { testActionStartsModule("doConfirmEmail", {email: TEST_EMAIL, siteName: "Unit Test Site"}, "check_registration"); }); - asyncTest("doGenerateAssertion - start the generate_assertion service", function() { - testActionStartsModule('doGenerateAssertion', { email: TEST_EMAIL }, "generate_assertion"); + asyncTest("doResetPassword - call the set_password controller with reset_password true", function() { + testActionStartsModule('doResetPassword', { email: TEST_EMAIL }, "set_password"); }); - asyncTest("doStageUser with successful creation - trigger user_staged", function() { - createController({ - ready: function() { - var email; - testHelpers.register("user_staged", function(msg, info) { - email = info.email; - }); + asyncTest("doStageResetPassword - trigger reset_password_staged", function() { + testStageAddress("doStageResetPassword", "reset_password_staged"); + }); - controller.doStageUser({ email: TEST_EMAIL, password: "password", ready: function(status) { - equal(status, true, "correct status"); - equal(email, TEST_EMAIL, "user successfully staged"); - start(); - }}); - } - }); + asyncTest("doConfirmResetPassword - start the check_registration service", function() { + testActionStartsModule("doConfirmResetPassword", {email: TEST_EMAIL, siteName: "Unit Test Site"}, + "check_registration"); + }); + + asyncTest("doStageReverifyEmail - trigger reverify_email_staged", function() { + + storage.addSecondaryEmail(TEST_EMAIL, { verified: false }); + testStageAddress("doStageReverifyEmail", "reverify_email_staged"); }); - asyncTest("doForgotPassword - call the set_password controller with reset_password true", function() { - testActionStartsModule('doForgotPassword', { email: TEST_EMAIL }, "set_password"); + asyncTest("doConfirmReverifyEmail - start the check_registration service", function() { + testActionStartsModule("doConfirmReverifyEmail", {email: TEST_EMAIL, siteName: "Unit Test Site"}, + "check_registration"); + }); + + asyncTest("doGenerateAssertion - start the generate_assertion service", function() { + testActionStartsModule('doGenerateAssertion', { email: TEST_EMAIL }, "generate_assertion"); }); asyncTest("doRPInfo - start the rp_info service", function() { diff --git a/resources/static/test/cases/pages/js/verify_secondary_address.js b/resources/static/test/cases/pages/js/verify_secondary_address.js index f15227bc0934937982552abb941804c3fe69463f..fcbbe88392b2d1f792bc76d9e1f1a3ac5d7b74d1 100644 --- a/resources/static/test/cases/pages/js/verify_secondary_address.js +++ b/resources/static/test/cases/pages/js/verify_secondary_address.js @@ -25,7 +25,7 @@ module("pages/verify_secondary_address", { setup: function() { testHelpers.setup(); - bid.Renderer.render("#page_head", "site/add_email_address", {}); + bid.Renderer.render("#page_head", "site/confirm", {}); $(".siteinfo,.password_entry").hide(); }, teardown: function() { diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js index 95f6b1d192ce8d0bb9ddd7063a796daa51b0af63..99116c22ffabfc7240808018866584c1226a2b7e 100644 --- a/resources/static/test/mocks/xhr.js +++ b/resources/static/test/mocks/xhr.js @@ -53,15 +53,45 @@ BrowserID.Mocks.xhr = (function() { "post /wsapi/cert_key valid": random_cert, "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 badPassword": 401, - "post /wsapi/complete_email_addition invalid": { success: false }, - "post /wsapi/complete_email_addition ajaxError": undefined, + "post /wsapi/complete_email_confirmation valid": { success: true }, + "post /wsapi/complete_email_confirmation badPassword": 401, + "post /wsapi/complete_email_confirmation invalid": { success: false }, + "post /wsapi/complete_email_confirmation ajaxError": undefined, "post /wsapi/stage_user unknown_secondary": { success: true }, "post /wsapi/stage_user valid": { success: true }, "post /wsapi/stage_user invalid": { success: false }, "post /wsapi/stage_user throttle": 429, "post /wsapi/stage_user ajaxError": undefined, + + "post /wsapi/stage_reset unknown_secondary": { success: true }, + "post /wsapi/stage_reset valid": { success: true }, + "post /wsapi/stage_reset invalid": { success: false }, + "post /wsapi/stage_reset throttle": 429, + "post /wsapi/stage_reset ajaxError": undefined, + + "post /wsapi/complete_reset valid": { success: true }, + "post /wsapi/complete_reset badPassword": 401, + "post /wsapi/complete_reset invalid": { success: false }, + "post /wsapi/complete_reset ajaxError": undefined, + + "get /wsapi/password_reset_status?email=registered%40testuser.com pending": { status: "pending" }, + "get /wsapi/password_reset_status?email=registered%40testuser.com complete": { status: "complete", userid: 4 }, + "get /wsapi/password_reset_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" }, + "get /wsapi/password_reset_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" }, + "get /wsapi/password_reset_status?email=registered%40testuser.com ajaxError": undefined, + + "post /wsapi/stage_reverify unknown_secondary": { success: true }, + "post /wsapi/stage_reverify valid": { success: true }, + "post /wsapi/stage_reverify invalid": { success: false }, + "post /wsapi/stage_reverify throttle": 429, + "post /wsapi/stage_reverify ajaxError": undefined, + + "get /wsapi/email_reverify_status?email=registered%40testuser.com pending": { status: "pending" }, + "get /wsapi/email_reverify_status?email=registered%40testuser.com complete": { status: "complete", userid: 4 }, + "get /wsapi/email_reverify_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" }, + "get /wsapi/email_reverify_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" }, + "get /wsapi/email_reverify_status?email=registered%40testuser.com ajaxError": undefined, + "get /wsapi/user_creation_status?email=registered%40testuser.com pending": { status: "pending" }, "get /wsapi/user_creation_status?email=registered%40testuser.com complete": { status: "complete", userid: 4 }, "get /wsapi/user_creation_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" }, @@ -77,6 +107,9 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/have_email?email=registered%40testuser.com valid": { email_known: true }, "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=testuser%40testuser.com valid": { email_known: true }, + "get /wsapi/have_email?email=testuser%40testuser.com throttle": { email_known: true }, + "get /wsapi/have_email?email=testuser%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 }, @@ -98,7 +131,8 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/email_addition_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" }, "get /wsapi/email_addition_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" }, "get /wsapi/email_addition_status?email=registered%40testuser.com ajaxError": undefined, - "get /wsapi/list_emails valid": {"testuser@testuser.com":{ type: "secondary" }}, + "get /wsapi/list_emails valid": {"testuser@testuser.com":{ type: "secondary", verified: true }}, + "get /wsapi/list_emails unverified": {"testuser@testuser.com":{ type: "secondary", verified: false }}, //"get /wsapi/list_emails known_secondary": {"registered@testuser.com":{ type: "secondary" }}, "get /wsapi/list_emails primary": {"testuser@testuser.com": { type: "primary" }}, "get /wsapi/list_emails multiple": {"testuser@testuser.com":{}, "testuser2@testuser.com":{}}, diff --git a/resources/views/add_email_address.ejs b/resources/views/confirm.ejs similarity index 100% rename from resources/views/add_email_address.ejs rename to resources/views/confirm.ejs diff --git a/tests/cache-header-tests.js b/tests/cache-header-tests.js index 9649791ba48c1ae17cb9902af3e1822dd77dd40e..da97f16712a38a3f8288bf5ce373fb13daccf17d 100755 --- a/tests/cache-header-tests.js +++ b/tests/cache-header-tests.js @@ -132,6 +132,7 @@ suite.addBatch({ '/privacy': hasProperCacheHeaders('/privacy'), '/verify_email_address': hasProperCacheHeaders('/verify_email_address'), '/add_email_address': hasProperCacheHeaders('/add_email_address'), + '/confirm': hasProperCacheHeaders('/confirm'), // '/pk': hasProperCacheHeaders('/pk'), // '/.well-known/browserid': hasProperCacheHeaders('/.well-known/browserid') }); diff --git a/tests/db-test.js b/tests/db-test.js index 6c8ec1ea54b157fb05f29e74918049d7fd4cead2..811f5cfb6ec27313c02335f02b684fd2fd0d8203 100755 --- a/tests/db-test.js +++ b/tests/db-test.js @@ -125,7 +125,7 @@ suite.addBatch({ suite.addBatch({ "upon receipt of a secret": { topic: function() { - db.gotVerificationSecret(secret, this.callback); + db.completeCreateUser(secret, this.callback); }, "gotVerificationSecret completes without error": function (err, r) { assert.isNull(err); @@ -200,7 +200,10 @@ suite.addBatch({ }, "then staging an email": { topic: function(err, uid) { - db.stageEmail(uid, 'lloyd@somewhe.re', 'biglonghashofapassword', this.callback); + // do not supply a password here. Email addition only supplies a password + // in the case it's the addition of a secondary address to an account with + // only primaries. + db.stageEmail(uid, 'lloyd@somewhe.re', undefined, this.callback); }, "yields a valid secret": function(err, secret) { assert.isNull(err); @@ -215,7 +218,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, this.callback); + db.completeConfirmEmail(secret, this.callback); }, "successfully": function(err, r) { assert.isNull(err); @@ -233,6 +236,17 @@ suite.addBatch({ assert.isNull(err); assert.isFalse(r); } + }, + "and user's password": { + topic: function() { + var self = this; + db.emailToUID('lloyd@nowhe.re', function(err, uid) { + db.checkAuth(uid, self.callback); + }); + }, + "is still populated": function(err, hash) { + assert.strictEqual(hash, "biglonghashofapassword"); + } } } } diff --git a/tests/email-throttling-test.js b/tests/email-throttling-test.js index 9b7e4560cfe8149e06d57272d946c9e042d3ee5d..5b7611572f19b74c96b2dee964f79bc6980c01e2 100755 --- a/tests/email-throttling-test.js +++ b/tests/email-throttling-test.js @@ -112,7 +112,7 @@ suite.addBatch({ suite.addBatch({ "and when we attempt to finish adding the email address": { topic: function() { - wsapi.post('/wsapi/complete_email_addition', { token: token }).call(this); + wsapi.post('/wsapi/complete_email_confirmation', { token: token }).call(this); }, "it works swimmingly": function(err, r) { assert.equal(r.code, 200); diff --git a/tests/forgotten-email-test.js b/tests/forgotten-email-test.js deleted file mode 100755 index c2e6aa3823be90f8c8093e013cc5697f09d6e67e..0000000000000000000000000000000000000000 --- a/tests/forgotten-email-test.js +++ /dev/null @@ -1,256 +0,0 @@ -#!/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'), -email = require('../lib/email.js'); - -var suite = vows.describe('forgotten-email'); - -start_stop.addStartupBatches(suite); - -// every time a new token is sent out, let's update the global -// var 'token' -var token = undefined; - -// create a new account via the api with (first address) -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) { - assert.strictEqual(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; - } - } -}); - -suite.addBatch({ - "create first account": { - topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); - }, - "account created": function(err, r) { - assert.equal(r.code, 200); - assert.strictEqual(true, JSON.parse(r.body).success); - token = undefined; - } - } -}); - -suite.addBatch({ - "email created": { - topic: wsapi.get('/wsapi/user_creation_status', { email: 'first@fakeemail.com' } ), - "should exist": function(err, r) { - assert.strictEqual(r.code, 200); - assert.strictEqual(JSON.parse(r.body).status, "complete"); - } - } -}); - -// add a new email address to the account (second address) -suite.addBatch({ - "add a new email address to our account": { - topic: wsapi.post('/wsapi/stage_email', { - email: 'second@fakeemail.com', - site:'https://fakesite.foobar.bizbaz.uk' - }), - "works": function(err, r) { - assert.strictEqual(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; - } - } -}); - -// confirm second email email address to the account -suite.addBatch({ - "create second account": { - topic: function() { - wsapi.post('/wsapi/complete_email_addition', { token: token }).call(this); - }, - "account created": function(err, r) { - assert.equal(r.code, 200); - assert.strictEqual(JSON.parse(r.body).success, true); - token = undefined; - } - } -}); - -// verify now both email addresses are known -suite.addBatch({ - "first email exists": { - topic: wsapi.get('/wsapi/have_email', { email: 'first@fakeemail.com' }), - "should exist": function(err, r) { - assert.strictEqual(JSON.parse(r.body).email_known, true); - } - }, - "second email exists": { - topic: wsapi.get('/wsapi/have_email', { email: 'second@fakeemail.com' }), - "should exist": function(err, r) { - assert.strictEqual(JSON.parse(r.body).email_known, true); - } - }, - "a random email doesn't exist": { - topic: wsapi.get('/wsapi/have_email', { email: 'third@fakeemail.com' }), - "shouldn't exist": function(err, r) { - assert.strictEqual(JSON.parse(r.body).email_known, false); - } - } -}); - -// Run the "forgot_email" flow with first address. This is really -// just re-registering the user. -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) { - assert.strictEqual(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; - } - } -}); - -// verify that the old email address + password combinations are still -// valid (this is so *until* someone clicks through) -suite.addBatch({ - "first email works": { - topic: wsapi.post('/wsapi/authenticate_user', { - email: 'first@fakeemail.com', - pass: 'firstfakepass', - ephemeral: false - }), - "should work": function(err, r) { - assert.strictEqual(JSON.parse(r.body).success, true); - } - }, - "second email works": { - topic: wsapi.post('/wsapi/authenticate_user', { - email: 'second@fakeemail.com', - pass: 'firstfakepass', - ephemeral: false - }), - "should work": function(err, r) { - assert.strictEqual(JSON.parse(r.body).success, true); - } - } -}); - -// now let's complete the re-registration of first email address -suite.addBatch({ - "re-create first email address": { - topic: function() { - wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); - }, - "account created": function(err, r) { - assert.equal(r.code, 200); - assert.strictEqual(JSON.parse(r.body).success, true); - } - } -}); - -// 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": { - topic: wsapi.post('/wsapi/authenticate_user', { - email: 'first@fakeemail.com', - pass: 'firstfakepass', - ephemeral: false - }), - "shouldn't work": function(err, r) { - assert.strictEqual(JSON.parse(r.body).success, false); - } - }, - "first email, second pass good": { - topic: wsapi.post('/wsapi/authenticate_user', { - email: 'first@fakeemail.com', - pass: 'secondfakepass', - ephemeral: false - }), - "should work": function(err, r) { - assert.strictEqual(JSON.parse(r.body).success, true); - } - }, - "logout": { - topic: wsapi.post('/wsapi/logout', {}), - "should work": function(err, r) { - assert.strictEqual(JSON.parse(r.body).success, true); - } - }, - "second email, first pass good": { - topic: wsapi.post('/wsapi/authenticate_user', { - email: 'second@fakeemail.com', - pass: 'firstfakepass', - ephemeral: false - }), - "should work": function(err, r) { - assert.strictEqual(JSON.parse(r.body).success, true); - } - }, - "second email, second pass bad": { - topic: wsapi.post('/wsapi/authenticate_user', { - email: 'second@fakeemail.com', - pass: 'secondfakepass', - ephemeral: false - }), - "shouldn' work": function(err, r) { - assert.strictEqual(JSON.parse(r.body).success, false); - } - }, -}); - -start_stop.addShutdownBatches(suite); - -// run or export the suite. -if (process.argv[1] === __filename) suite.run(); -else suite.export(module); diff --git a/tests/forgotten-pass-test.js b/tests/forgotten-pass-test.js new file mode 100755 index 0000000000000000000000000000000000000000..7e9aae487b38946332a3096323207edc3ad5e329 --- /dev/null +++ b/tests/forgotten-pass-test.js @@ -0,0 +1,465 @@ +#!/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'), +email = require('../lib/email.js'), +jwcrypto = require('jwcrypto'); + +var suite = vows.describe('forgotten-email'); + +// algs +require("jwcrypto/lib/algs/ds"); +require("jwcrypto/lib/algs/rs"); + +start_stop.addStartupBatches(suite); + +// every time a new token is sent out, let's update the global +// var 'token' +var token = undefined; + +// create a new account via the api with (first address) +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) { + assert.strictEqual(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; + } + } +}); + +suite.addBatch({ + "create first account": { + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); + }, + "account created": function(err, r) { + assert.equal(r.code, 200); + assert.strictEqual(true, JSON.parse(r.body).success); + token = undefined; + } + } +}); + +suite.addBatch({ + "email created": { + topic: wsapi.get('/wsapi/user_creation_status', { email: 'first@fakeemail.com' } ), + "should exist": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).status, "complete"); + } + } +}); + +// add a new email address to the account (second address) +suite.addBatch({ + "add a new email address to our account": { + topic: wsapi.post('/wsapi/stage_email', { + email: 'second@fakeemail.com', + site:'https://fakesite.foobar.bizbaz.uk' + }), + "works": function(err, r) { + assert.strictEqual(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; + } + } +}); + +// confirm second email email address to the account +suite.addBatch({ + "create second account": { + topic: function() { + wsapi.post('/wsapi/complete_email_confirmation', { token: token }).call(this); + }, + "account created": function(err, r) { + assert.equal(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, true); + token = undefined; + } + } +}); + +// verify now both email addresses are known +suite.addBatch({ + "first email exists": { + topic: wsapi.get('/wsapi/have_email', { email: 'first@fakeemail.com' }), + "should exist": function(err, r) { + assert.strictEqual(JSON.parse(r.body).email_known, true); + } + }, + "second email exists": { + topic: wsapi.get('/wsapi/have_email', { email: 'second@fakeemail.com' }), + "should exist": function(err, r) { + assert.strictEqual(JSON.parse(r.body).email_known, true); + } + }, + "a random email doesn't exist": { + topic: wsapi.get('/wsapi/have_email', { email: 'third@fakeemail.com' }), + "shouldn't exist": function(err, r) { + assert.strictEqual(JSON.parse(r.body).email_known, false); + } + } +}); + +suite.addBatch({ + "reset status": { + topic: wsapi.get('/wsapi/password_reset_status', { email: 'first@fakeemail.com' } ), + "returns 'complete' before calling reset": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).status, "complete"); + } + } +}); + +// Run the "forgot_email" flow with first address. +suite.addBatch({ + "reset password on first account": { + topic: wsapi.post('/wsapi/stage_reset', { + email: 'first@fakeemail.com', + pass: 'secondfakepass', + site:'https://otherfakesite.com' + }), + "works": function(err, r) { + assert.strictEqual(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; + } + } +}); + +suite.addBatch({ + "given a token, getting an email": { + topic: function() { + wsapi.get('/wsapi/email_for_token', { token: token }).call(this); + }, + "account created": function(err, r) { + assert.equal(r.code, 200); + var body = JSON.parse(r.body); + assert.strictEqual(body.success, true); + } + } +}); + +// verify that the old email address + password combinations are still +// valid (this is so *until* someone clicks through) +suite.addBatch({ + "first email works": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), + "should work": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + } + }, + "second email works": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'second@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), + "should work": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + } + }, + "reset status": { + topic: wsapi.get('/wsapi/password_reset_status', { email: 'first@fakeemail.com' } ), + "returns 'pending' after calling reset": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).status, "pending"); + } + } +}); + +// now let's complete the re-registration of first email address +suite.addBatch({ + "complete password reset": { + topic: function() { + wsapi.post('/wsapi/complete_reset', { token: token }).call(this); + }, + "account created": function(err, r) { + assert.equal(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, true); + } + } +}); + +suite.addBatch({ + "reset status": { + topic: wsapi.get('/wsapi/password_reset_status', { email: 'first@fakeemail.com' } ), + "returns 'complete' after completing reset": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).status, "complete"); + } + } +}); + +// now we should be able to sign in using any email address +suite.addBatch({ + "first email, first pass bad": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), + "shouldn't work": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, false); + } + }, + "first email, second pass good": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'secondfakepass', + ephemeral: false + }), + "should work": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + } + }, + "second email, first pass bad": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'second@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), + "should work": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, false); + } + }, + "second email, second pass bad": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'second@fakeemail.com', + pass: 'secondfakepass', + ephemeral: false + }), + "shouldn' work": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + } + }, +}); + +// test list emails +suite.addBatch({ + "list emails API": { + topic: wsapi.get('/wsapi/list_emails', {}), + "succeeds with HTTP 200" : function(err, r) { + assert.strictEqual(r.code, 200); + }, + "returns an object with proper bits set": function(err, r) { + r = JSON.parse(r.body); + assert.strictEqual(r['second@fakeemail.com'].verified, false); + assert.strictEqual(r['first@fakeemail.com'].verified, true); + } + } +}); + +// test that certification fails for unverified email addresses + +// generate a keypair, we'll use this to sign assertions, as if +// this keypair is stored in the browser localStorage +var kp; + +suite.addBatch({ + "generate a keypair": { + topic: function() { + jwcrypto.generateKeypair({algorithm: "RS", keysize: 64}, this.callback); + }, + "works": function(err, keypair) { + assert.isNull(err); + assert.isObject(keypair); + kp = keypair; + }, + "and cert a key for a verified email address": { + topic: function() { + wsapi.post('/wsapi/cert_key', { + email: 'first@fakeemail.com', + pubkey: kp.publicKey.serialize(), + ephemeral: false + }).call(this); + }, + "returns a success response" : function(err, r) { + assert.strictEqual(r.code, 200); + } + }, + "and cert a key for an unverified email address": { + topic: function() { + wsapi.post('/wsapi/cert_key', { + email: 'second@fakeemail.com', + pubkey: kp.publicKey.serialize(), + ephemeral: false + }).call(this); + }, + "is forbidden" : function(err, r) { + assert.strictEqual(r.code, 403); + } + } + } +}); + +// Now we have an account with an unverified email. Let's attempt to reverify our other email +// address +// Run the "forgot_email" flow with first address. +suite.addBatch({ + "reverify a non-existent email": { + topic: wsapi.post('/wsapi/stage_reverify', { + email: 'dne@fakeemail.com', + site:'https://otherfakesite.com' + }), + "fails": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, false); + } + }, + "reverify a verified email": { + topic: wsapi.post('/wsapi/stage_reverify', { + email: 'first@fakeemail.com', + site:'https://otherfakesite.com' + }), + "fails": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, false); + } + }, + "reverify an unverified email": { + topic: wsapi.post('/wsapi/stage_reverify', { + email: 'second@fakeemail.com', + site:'https://otherfakesite.com' + }), + "works": function(err, r) { + assert.strictEqual(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, true); + } + } +}); + +suite.addBatch({ + "a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "is obtained": function (t) { + assert.strictEqual(typeof t, 'string'); + token = t; + } + } +}); + +suite.addBatch({ + "given a token, getting an email": { + topic: function() { + wsapi.get('/wsapi/email_for_token', { token: token }).call(this); + }, + "works dandy": function(err, r) { + assert.equal(r.code, 200); + var body = JSON.parse(r.body); + assert.strictEqual(body.success, true); + assert.strictEqual(body.email, 'second@fakeemail.com'); + assert.strictEqual(body.must_auth, false); + } + } +}); + +suite.addBatch({ + "reverify status": { + topic: function() { + wsapi.get('/wsapi/email_reverify_status', { email: "second@fakeemail.com" }).call(this); + }, + "is pending": function(err, r) { + assert.equal(r.code, 200); + var body = JSON.parse(r.body); + assert.strictEqual(body.status, 'pending'); + } + } +}); + +suite.addBatch({ + "complete reverify": { + topic: function() { + wsapi.post('/wsapi/complete_email_confirmation', { token: token }).call(this); + }, + "works": function(err, r) { + assert.equal(r.code, 200); + var body = JSON.parse(r.body); + } + } +}); + +suite.addBatch({ + "after reverification": { + topic: function() { + jwcrypto.generateKeypair({algorithm: "RS", keysize: 64}, this.callback); + }, + "we can generate a keypair": function(err, keypair) { + assert.isNull(err); + assert.isObject(keypair); + kp = keypair; + }, + "we can certify a key for the email address": { + topic: function() { + wsapi.post('/wsapi/cert_key', { + email: 'second@fakeemail.com', + pubkey: kp.publicKey.serialize(), + ephemeral: false + }).call(this); + }, + "returns a success response" : function(err, r) { + assert.strictEqual(r.code, 200); + } + } + } +}); + + +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/list-emails-wsapi-test.js b/tests/list-emails-wsapi-test.js index 037bed0b812225b6618a231c75f94ec25e12bf33..0eb3449b7461b485b3ddb31f1cd1e5a7898d272b 100755 --- a/tests/list-emails-wsapi-test.js +++ b/tests/list-emails-wsapi-test.js @@ -85,6 +85,7 @@ suite.addBatch({ var emails = Object.keys(respObj); assert.equal(emails[0], "syncer@somehost.com"); assert.equal(respObj[emails[0]].type, "secondary"); + assert.equal(respObj[emails[0]].verified, true); assert.equal(emails.length, 1); } } diff --git a/tests/page-requests-test.js b/tests/page-requests-test.js index 57f9b081300a171366a24b1c72a4bf43405d4605..a6fd57988775c0fe5c8cfb04a0811a40fc3695d2 100755 --- a/tests/page-requests-test.js +++ b/tests/page-requests-test.js @@ -63,6 +63,9 @@ suite.addBatch({ 'GET /privacy': respondsWith(200), 'GET /verify_email_address': respondsWith(200), 'GET /add_email_address': respondsWith(200), + 'GET /confirm': respondsWith(200), + 'GET /reset_password': respondsWith(200), + 'GET /confirm': respondsWith(200), 'GET /idp_auth_complete': respondsWith(200), 'GET /pk': respondsWith(200), 'GET /.well-known/browserid': respondsWith(200), diff --git a/tests/primary-then-secondary-test.js b/tests/primary-then-secondary-test.js index 2112b3e2a674b6f5f16992d55996c2a30b7e36fa..1eb6aaeda15e3cc62ba4954a317ac910291b5989 100755 --- a/tests/primary-then-secondary-test.js +++ b/tests/primary-then-secondary-test.js @@ -122,7 +122,7 @@ suite.addBatch({ }, "which then": { topic: function() { - wsapi.post('/wsapi/complete_email_addition', { + wsapi.post('/wsapi/complete_email_confirmation', { token: this._token }).call(this); }, @@ -200,7 +200,7 @@ suite.addBatch({ }, "with a token": { topic: function() { - wsapi.post('/wsapi/complete_email_addition', { + wsapi.post('/wsapi/complete_email_confirmation', { token: this._token }).call(this); }, diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js index 5bf27ce9716093340a626e60f44b48c0bb902c5d..5337ad2950c1cc19285fec08f8547ecc73e9a548 100755 --- a/tests/stalled-mysql-test.js +++ b/tests/stalled-mysql-test.js @@ -119,8 +119,8 @@ suite.addBatch({ assert.strictEqual(r.code, 503); } }, - "complete_email_addition": { - topic: wsapi.post('/wsapi/complete_email_addition', { + "complete_email_confirmation": { + topic: wsapi.post('/wsapi/complete_email_confirmation', { token: 'bogus' }), "fails with 503": function(err, r) { diff --git a/tests/verify-in-different-browser-test.js b/tests/verify-in-different-browser-test.js index b5a5a0fbb5f5cf6463088a36bba301a3b29e67f1..9cd451b6c4122de385924a9c1d9b1fb847d08f87 100755 --- a/tests/verify-in-different-browser-test.js +++ b/tests/verify-in-different-browser-test.js @@ -100,7 +100,7 @@ suite.addBatch({ "then clearing cookies and completing": { topic: function() { wsapi.clearCookies(); - wsapi.post('/wsapi/complete_email_addition', { + wsapi.post('/wsapi/complete_email_confirmation', { token: this._token }).call(this); }, @@ -109,7 +109,7 @@ suite.addBatch({ }, "but succeeds": { topic: function() { - wsapi.post('/wsapi/complete_email_addition', { + wsapi.post('/wsapi/complete_email_confirmation', { token: this._token, pass: TEST_PASS }).call(this); @@ -171,7 +171,7 @@ suite.addBatch({ "then clearing cookies and completing": { topic: function() { wsapi.clearCookies(); - wsapi.post('/wsapi/complete_email_addition', { + wsapi.post('/wsapi/complete_email_confirmation', { token: this._token }).call(this); }, @@ -180,7 +180,7 @@ suite.addBatch({ }, "but succeeds": { topic: function() { - wsapi.post('/wsapi/complete_email_addition', { + wsapi.post('/wsapi/complete_email_confirmation', { token: this._token, pass: TEST_PASS }).call(this); @@ -254,7 +254,7 @@ suite.addBatch({ }, "but succeeds": { topic: function() { - wsapi.post('/wsapi/complete_email_addition', { + wsapi.post('/wsapi/complete_user_creation', { token: this._token, pass: TEST_PASS }).call(this);