Permalink
Browse files

require authentication on complete_* wsapis to reduce risk now that p…

…assword is provided before email verification. issue #290 and a pre-requisite for issue #1000
  • Loading branch information...
1 parent 1ab8126 commit 8316bb978fd95c3fb571736628551dda0a251e7e @lloyd lloyd committed with shane-tomlinson May 2, 2012
View
@@ -73,20 +73,21 @@ exports.onReady = function(f) {
// these are read only database calls
[
+ 'authForVerificationSecret',
+ 'checkAuth',
+ 'emailForVerificationSecret',
'emailKnown',
- 'userKnown',
- 'isStaged',
+ 'emailToUID',
+ 'emailType',
'emailsBelongToSameAccount',
- 'emailForVerificationSecret',
'haveVerificationSecret',
- 'verificationSecretForEmail',
- 'checkAuth',
- 'listEmails',
+ 'isStaged',
'lastStaged',
+ 'listEmails',
'ping',
- 'emailType',
+ 'userKnown',
'userOwnsEmail',
- 'emailToUID'
+ 'verificationSecretForEmail'
].forEach(function(fn) {
exports[fn] = function() {
checkReady();
View
@@ -236,15 +236,26 @@ exports.emailForVerificationSecret = function(secret, cb) {
process.nextTick(function() {
sync();
if (!db.staged[secret]) return cb("no such secret");
+ cb(null, db.staged[secret].email, db.staged[secret].existing_user);
+ });
+};
+
+exports.authForVerificationSecret = function(secret, cb) {
+ process.nextTick(function() {
+ sync();
+ if (!db.staged[secret]) return cb("no such secret");
+
+ if (db.staged[secret].passwd) {
+ return cb(null, db.staged[secret].passwd, db.staged[secret].existing_user);
+ }
+
exports.checkAuth(db.staged[secret].existing_user, function (err, hash) {
- cb(err, {
- email: db.staged[secret].email,
- needs_password: !hash
- });
+ cb(err, hash, db.staged[secret].existing_user);
});
});
};
+
exports.verificationSecretForEmail = function(email, cb) {
setTimeout(function() {
sync();
View
@@ -265,7 +265,20 @@ exports.haveVerificationSecret = function(secret, cb) {
exports.emailForVerificationSecret = function(secret, cb) {
client.query(
- "SELECT * FROM staged WHERE secret = ?", [ secret ],
+ "SELECT email, existing_user FROM staged WHERE secret = ?", [ secret ],
+ function(err, rows) {
+ if (err) return cb("database unavailable");
+
+ // if the record was not found, fail out
+ if (!rows || rows.length != 1) return cb("no such secret");
+
+ cb(null, rows[0].email, rows[0].existing_user);
+ });
+};
+
+exports.authForVerificationSecret = function(secret, cb) {
+ client.query(
+ "SELECT existing_user, passwd FROM staged WHERE secret = ?", [ secret ],
function(err, rows) {
if (err) return cb("database unavailable");
@@ -274,34 +287,15 @@ exports.emailForVerificationSecret = function(secret, cb) {
var o = rows[0];
- // if the record was found and this is for a new_acct, return the email
- if (o.new_acct) return cb(null, { email: o.email, needs_password: false });
-
- // we need a userid. the old schema had an 'existing' field which was an email
- // address. the new schema has an 'existing_user' field which is a userid.
- // this is transitional code so outstanding verification links continue working
- // and can be removed in feb 2012 some time. maybe for valentines day?
- if (typeof o.existing_user === 'number') doCheckAuth(o.existing_user);
- else if (typeof o.existing === 'string') {
- exports.emailToUID(o.existing, function(err, uid) {
- if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist');
- doCheckAuth(uid);
- });
- }
+ // if there is a hashed passwd in the result, we're done
+ if (o.passwd) return cb(null, o.passwd, o.existing_user);
- function doCheckAuth(uid) {
- // if the account is being added to an existing account, let's find
- // out if the account has a password set (if only primary email addresses
- // are associated with the acct at the moment, then there will not be a
- // password set and the user will need to set one with the addition of
- // this addresss)
- exports.checkAuth(uid, function(err, hash) {
- cb(err, {
- email: o.email,
- needs_password: !hash
- });
- });
- }
+ // otherwise, let's get the passwd from the user record
+ if (!o.existing_user) cb("no password for user");
+
+ exports.checkAuth(o.existing_user, function(err, hash) {
+ cb(err, hash, o.existing_user);
+ });
});
};
View
@@ -28,6 +28,10 @@ exports.serviceUnavailable = function(resp, reason) {
sendResponse(resp, "Service Unavailable", reason, 503);
};
+exports.authRequired = function(resp, reason) {
+ sendResponse(resp, "Authentication Required", reason, 401);
+};
+
exports.badRequest = function(resp, reason) {
sendResponse(resp, "Bad Request", reason, 400);
};
@@ -5,31 +5,56 @@
const
db = require('../db.js'),
logger = require('../logging.js').logger,
-wsapi = require('../wsapi.js');
+wsapi = require('../wsapi.js'),
+brycpt = require('../bcrypt.js');
exports.method = 'post';
exports.writes_db = true;
-// XXX: see issue #290 - we want to require authentication here and update frontend code
exports.authed = false;
// NOTE: this API also takes a 'pass' parameter which is required
// when a user has a null password (only primaries on their acct)
exports.args = ['token'];
exports.i18n = false;
exports.process = function(req, res) {
- db.emailForVerificationSecret(req.body.token, function(err, r) {
- if (err === 'database unavailable') {
+ // in order to complete an email addition, one of the following must be true:
+ //
+ // 1. you must already be authenticated as the user who initiated the verification
+ // 2. you must provide the password of the initiator.
+ //
+ db.authForVerificationSecret(req.body.token, function(err, initiator_hash, initiator_uid) {
+ if (err) {
+ logger.info("unknown verification secret: " + err);
return wsapi.databaseDown(res, err);
}
- db.gotVerificationSecret(req.body.token, function(e, email, uid) {
- if (e) {
- logger.warn("couldn't complete email verification: " + e);
- wsapi.databaseDown(res, e);
- } else {
- wsapi.authenticateSession(req.session, uid, 'password');
- res.json({ success: true });
- }
- });
+ if (req.session.userid === initiator_uid) {
+ postAuthentication();
+ } else if (typeof req.body.pass === 'string') {
+ bcrypt.compare(req.body.pass, initiator_hash, function (err, success) {
+ if (err) {
+ logger.warn("max load hit, failing on auth request with 503: " + err);
+ return httputils.serviceUnavailable(res, "server is too busy");
+ } else if (!success) {
+ return httputils.authRequired(res, "password mismatch");
+ } else {
+ postAuthentication();
+ }
+ });
+ } else {
+ return httputils.authRequired(res, "password required");
+ }
+
+ function postAuthentication() {
+ db.gotVerificationSecret(req.body.token, function(e, email, uid) {
+ if (e) {
+ logger.warn("couldn't complete email verification: " + e);
+ wsapi.databaseDown(res, e);
+ } else {
+ wsapi.authenticateSession(req.session, uid, 'password');
+ res.json({ success: true });
+ }
+ });
+ };
});
};
@@ -6,7 +6,8 @@ const
db = require('../db.js'),
wsapi = require('../wsapi.js'),
httputils = require('../httputils'),
-logger = require('../logging.js').logger;
+logger = require('../logging.js').logger,
+bcrypt = require('../bcrypt');
exports.method = 'post';
exports.writes_db = true;
@@ -15,27 +16,68 @@ exports.args = ['token'];
exports.i18n = false;
exports.process = function(req, res) {
- // at the time the email verification is performed, we'll clear the pendingCreation
- // data on the session.
- delete req.session.pendingCreation;
+ // in order to complete a user creation, one of the following must be true:
+ //
+ // 1. you are using the same browser to complete the email verification as you
+ // used to start it
+ // 2. you have provided the password chosen by the initiator of the verification
+ // request
+ //
+ // These protections guard against the case where an attacker can send out a bunch
+ // of verification emails, wait until a distracted internet user clicks on one,
+ // and then control a browserid account that they can use to prove they own
+ // the email address of the attacked.
- db.haveVerificationSecret(req.body.token, function(err, known) {
- if (err) return wsapi.databaseDown(res, err);
-
- if (!known) return res.json({ success: false} );
-
- db.gotVerificationSecret(req.body.token, function(err, email, uid) {
+ // is this the same browser?
+ if (typeof req.session.pendingCreation === 'string' &&
+ req.body.token === req.session.pendingCreation) {
+ postAuthentication();
+ }
+ // is a password provided?
+ else if (typeof req.body.pass === 'string') {
+ return db.authForVerificationSecret(req.body.token, function(err, hash) {
if (err) {
- logger.warn("couldn't complete email verification: " + err);
- wsapi.databaseDown(res, err);
- } else {
- // FIXME: not sure if we want to do this (ba)
- // at this point the user has set a password associated with an email address
- // that they've verified. We create an authenticated session.
- wsapi.authenticateSession(req.session, uid, 'password',
- config.get('ephemeral_session_duration_ms'));
- res.json({ success: true });
+ 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 {
+ postAuthentication();
+ }
+ });
+ });
+ } else {
+ return httputils.authRequired(res, 'Provide your password');
+ }
+
+ function postAuthentication() {
+ // the time the email verification is performed, we'll clear the pendingCreation
+ // data on the session.
+ delete req.session.pendingCreation;
+
+ db.haveVerificationSecret(req.body.token, function(err, known) {
+ if (err) return wsapi.databaseDown(res, err);
+
+ if (!known) return res.json({ success: false} );
+
+ db.gotVerificationSecret(req.body.token, function(err, email, uid) {
+ if (err) {
+ logger.warn("couldn't complete email verification: " + err);
+ wsapi.databaseDown(res, err);
+ } else {
+ // FIXME: not sure if we want to do this (ba)
+ // at this point the user has set a password associated with an email address
+ // that they've verified. We create an authenticated session.
+ wsapi.authenticateSession(req.session, uid, 'password',
+ config.get('ephemeral_session_duration_ms'));
+ res.json({ success: true });
+ }
+ });
});
- });
+ }
};
@@ -19,7 +19,7 @@ exports.args = ['token'];
exports.i18n = false;
exports.process = function(req, res) {
- db.emailForVerificationSecret(req.query.token, function(err, r) {
+ db.emailForVerificationSecret(req.query.token, function(err, email, uid) {
if (err) {
if (err === 'database unavailable') {
httputils.serviceUnavailable(res, err);
@@ -30,10 +30,23 @@ exports.process = function(req, res) {
});
}
} else {
+ // must the user authenticate? This is true if they are not authenticated
+ // as the uid who initiated the verification, and they are not on the same
+ // browser as the initiator
+ var must_auth = true;
+
+ if (uid && req.session.userid === uid) {
+ must_auth = false;
+ }
+ else if (!uid && typeof req.session.pendingCreation === 'string' &&
+ req.query.token === req.session.pendingCreation) {
+ must_auth = false;
+ }
+
res.json({
success: true,
- email: r.email,
- needs_password: r.needs_password
+ email: email,
+ must_auth: must_auth
});
}
});
View
@@ -25,9 +25,6 @@ exports.i18n = true;
exports.process = function(req, resp) {
var langContext = wsapi.langContext(req);
- // staging a user logs you out.
- wsapi.clearAuthenticatedUser(req.session);
-
// validate
try {
sanitize(req.body.email).isEmail();
@@ -54,6 +51,9 @@ exports.process = function(req, resp) {
return httputils.throttled(resp, "Too many emails sent to that address, try again later.");
}
+ // staging a user logs you out.
+ wsapi.clearAuthenticatedUser(req.session);
+
// now bcrypt the password
wsapi.bcryptPassword(req.body.pass, function (err, hash) {
if (err) {
View
@@ -85,8 +85,8 @@ suite.addBatch({
topic: function(err, secret) {
db.emailForVerificationSecret(secret, this.callback);
},
- "matches expected email": function(err, r) {
- assert.strictEqual(r.email, 'lloyd@nowhe.re');
+ "matches expected email": function(err, email, uid) {
+ assert.strictEqual(email, 'lloyd@nowhe.re');
}
},
"fetch secret for email": {
@@ -129,7 +129,8 @@ suite.addBatch({
},
"complete_user_creation": {
topic: wsapi.post('/wsapi/complete_user_creation', {
- token: 'bogus'
+ token: 'bogus',
+ pass: 'alsobogus'
}),
"fails with 503": function(err, r) {
assert.strictEqual(r.code, 503);

0 comments on commit 8316bb9

Please sign in to comment.