Newer
Older
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla BrowserID.
*
* The Initial Developer of the Original Code is Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
// a module which implements the authorities web server api.
// it used to be that we stuffed every function in exports.
// now we're using proper express function registration to deal
// with HTTP methods and the like, apply middleware, etc.
db = require('db.js'),
httputils = require('httputils.js'),
Lloyd Hilaiel
committed
bcrypt = require('bcrypt'),
Lloyd Hilaiel
committed
crypto = require('crypto'),
logger = require('logging.js').logger,
Lloyd Hilaiel
committed
config = require('configuration.js'),
validate = require('validate');
Lloyd Hilaiel
committed
// log a user out, clearing everything from their session except the csrf token
function clearAuthenticatedUser(session) {
Object.keys(session).forEach(function(k) {
if (k !== 'csrf') delete session[k];
});
}
function setAuthenticatedUser(session, email) {
session.authenticatedUser = email;
session.authenticatedAt = new Date();
}
function isAuthed(req) {
Lloyd Hilaiel
committed
var who;
try {
if (req.session.authenticatedUser) {
if (!Date.parse(req.session.authenticatedAt) > 0) throw "bad timestamp";
if (new Date() - new Date(req.session.authenticatedAt) >
Lloyd Hilaiel
committed
config.get('authentication_duration_ms'))
Lloyd Hilaiel
committed
{
throw "expired";
}
who = req.session.authenticatedUser;
}
} catch(e) {
logger.debug("Session authentication has expired:", e);
clearAuthenticatedUser(req.session);
}
return who;
}
Lloyd Hilaiel
committed
// turned this into a proper middleware
function checkAuthed(req, resp, next) {
return httputils.badRequest(resp, "requires authentication");
}
function setup(app) {
Lloyd Hilaiel
committed
// return the CSRF token, authentication status, and current server time (for assertion signing)
Lloyd Hilaiel
committed
// IMPORTANT: this is safe because it's only readable by same-origin code
app.get('/wsapi/session_context', function(req, res) {
Lloyd Hilaiel
committed
if (typeof req.session == 'undefined') {
req.session = {};
}
if (typeof req.session.csrf == 'undefined') {
// FIXME: using express-csrf's approach for generating randomness
// not awesome, but probably sufficient for now.
req.session.csrf = crypto.createHash('md5').update('' + new Date().getTime()).digest('hex');
logger.debug("NEW csrf token created: " + req.session.csrf);
}
Lloyd Hilaiel
committed
var auth_status = false;
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
function sendResponse() {
Lloyd Hilaiel
committed
res.json({
Lloyd Hilaiel
committed
csrf_token: req.session.csrf,
server_time: (new Date()).getTime(),
authenticated: auth_status
Lloyd Hilaiel
committed
});
Lloyd Hilaiel
committed
};
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// if they're authenticated for an email address that we don't know about,
// then we should purge the stored cookie
if (!isAuthed(req)) {
logger.debug("user is not authenticated");
sendResponse();
} else {
db.emailKnown(req.session.authenticatedUser, function (known) {
if (!known) {
logger.debug("user is authenticated with an email that doesn't exist in the database");
clearAuthenticatedUser(req.session);
} else {
logger.debug("user is authenticated");
auth_status = true;
}
sendResponse();
});
}
Lloyd Hilaiel
committed
});
/* checks to see if an email address is known to the server
* takes 'email' as a GET argument */
app.get('/wsapi/have_email', function(req, resp) {
// get inputs from get data!
var email = url.parse(req.url, true).query['email'];
db.emailKnown(email, function(known) {
Lloyd Hilaiel
committed
resp.json({ email_known: known });
/* 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
*/
Lloyd Hilaiel
committed
app.post('/wsapi/stage_user', validate([ "email", "site" ]), function(req, resp) {
Lloyd Hilaiel
committed
// staging a user logs you out.
clearAuthenticatedUser(req.session);
Lloyd Hilaiel
committed
db.lastStaged(req.body.email, function (last) {
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.forbidden(resp, "throttling. try again later.");
}
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
try {
// upon success, stage_user returns a secret (that'll get baked into a url
// and given to the user), on failure it throws
db.stageUser(req.body.email, function(secret) {
// store the email being registered in the session data
if (!req.session) req.session = {};
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// store the secret we're sending via email in the users session, as checking
// that it still exists in the database is the surest way to determine the
// status of the email verification.
req.session.pendingCreation = secret;
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
resp.json({ success: true });
// let's now kick out a verification email!
email.sendNewUserEmail(req.body.email, req.body.site, secret);
});
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
}
});
Lloyd Hilaiel
committed
});
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
app.get('/wsapi/user_creation_status', function(req, resp) {
var email = req.query.email;
if (typeof email !== 'string') {
logger.warn("user_creation_status called without 'email' parameter");
httputils.badRequest(resp, "no 'email' parameter");
return;
}
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// if the user is authenticated as the user in question, we're done
if (isAuthed(req) && req.session.authenticatedUser === email) {
Lloyd Hilaiel
committed
return resp.json({ status: 'complete' });
Lloyd Hilaiel
committed
}
// if the user isn't authenticated and there's no pendingCreation token,
// then they must authenticate
else if (!req.session.pendingCreation) {
Lloyd Hilaiel
committed
return resp.json({ status: 'mustAuth' });
Lloyd Hilaiel
committed
}
// if the secret is still in the database, it hasn't yet been verified and
// verification is still pending
db.emailForVerificationSecret(req.session.pendingCreation, function (email) {
Lloyd Hilaiel
committed
if (email) return resp.json({ status: 'pending' });
Lloyd Hilaiel
committed
// if the secret isn't known, and we're not authenticated, then the user must authenticate
// (maybe they verified the URL on a different browser, or maybe they canceled the account
// creation)
else {
delete req.session.pendingCreation;
Lloyd Hilaiel
committed
resp.json({ status: 'mustAuth' });
Lloyd Hilaiel
committed
}
});
});
Lloyd Hilaiel
committed
function bcrypt_password(password, cb) {
Lloyd Hilaiel
committed
var bcryptWorkFactor = config.get('bcrypt_work_factor');
bcrypt.gen_salt(bcryptWorkFactor, function (err, salt) {
if (err) {
var msg = "error generating salt with bcrypt: " + err;
logger.error(msg);
return cb(msg);
}
bcrypt.encrypt(password, salt, function(err, hash) {
if (err) {
var msg = "error generating password hash with bcrypt: " + err;
logger.error(msg);
return cb(msg);
}
return cb(undefined, hash);
});
});
};
Lloyd Hilaiel
committed
app.post('/wsapi/complete_user_creation', validate(["token", "pass"]), function(req, resp) {
Lloyd Hilaiel
committed
// issue #155, valid password length is between 8 and 80 chars.
Lloyd Hilaiel
committed
if (req.body.pass.length < 8 || req.body.pass.length > 80) {
Lloyd Hilaiel
committed
httputils.badRequest(resp, "valid passwords are between 8 and 80 chars");
return;
}
Lloyd Hilaiel
committed
// at the time the email verification is performed, we'll clear the pendingCreation
// data on the session.
delete req.session.pendingCreation;
Lloyd Hilaiel
committed
// We should check to see if the verification secret is valid *before*
// bcrypting the password (which is expensive), to prevent a possible
// DoS attack.
db.emailForVerificationSecret(req.body.token, function(email) {
Lloyd Hilaiel
committed
if (!email) return resp.json({ success: false} );
Lloyd Hilaiel
committed
// now bcrypt the password
bcrypt_password(req.body.pass, function (err, hash) {
Lloyd Hilaiel
committed
if (err) {
logger.error("can't bcrypt: " + err);
Lloyd Hilaiel
committed
return resp.json({ success: false });
Lloyd Hilaiel
committed
}
db.gotVerificationSecret(req.body.token, hash, function(err, email) {
Lloyd Hilaiel
committed
if (err) {
Lloyd Hilaiel
committed
logger.warn("couldn't complete email verification: " + err);
resp.json({ success: false });
} 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.
setAuthenticatedUser(req.session, email);
resp.json({ success: true });
Lloyd Hilaiel
committed
}
});
Lloyd Hilaiel
committed
});
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
app.post('/wsapi/stage_email', checkAuthed, validate(["email", "site"]), function (req, resp) {
Lloyd Hilaiel
committed
db.lastStaged(req.body.email, function (last) {
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.forbidden(resp, "throttling. try again later.");
}
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
try {
// on failure stageEmail may throw
db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// store the email being added in session data
req.session.pendingAddition = secret;
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
resp.json({ success: true });
// let's now kick out a verification email!
email.sendAddAddressEmail(req.body.email, req.body.site, secret);
});
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
}
});
Lloyd Hilaiel
committed
});
Lloyd Hilaiel
committed
app.get('/wsapi/email_for_token', validate(["token"]), function(req,resp) {
db.emailForVerificationSecret(req.query.token, function(email) {
Lloyd Hilaiel
committed
resp.json({ email: email });
Lloyd Hilaiel
committed
app.get('/wsapi/email_addition_status', function(req, resp) {
var email = req.query.email;
if (typeof email !== 'string')
Lloyd Hilaiel
committed
logger.warn("email_addition_status called without an 'email' parameter");
httputils.badRequest(resp, "missing 'email' parameter");
Lloyd Hilaiel
committed
// this is a pending email addition, it requires authentication
if (!isAuthed(req, resp)) {
delete req.session.pendingAddition;
return httputils.badRequest(resp, "requires authentication");
}
Lloyd Hilaiel
committed
// check if the currently authenticated user has the email stored under pendingAddition
// in their acct.
db.emailsBelongToSameAccount(
email,
req.session.authenticatedUser,
function(registered) {
if (registered) {
delete req.session.pendingAddition;
Lloyd Hilaiel
committed
resp.json({ status: 'complete' });
Lloyd Hilaiel
committed
} else if (!req.session.pendingAddition) {
resp.json('failed');
db.emailForVerificationSecret(req.session.pendingAddition, function (email) {
if (email) {
Lloyd Hilaiel
committed
return resp.json({ status: 'pending' });
Lloyd Hilaiel
committed
} else {
delete req.session.pendingAddition;
Lloyd Hilaiel
committed
resp.json({ status: 'failed' });
Lloyd Hilaiel
committed
}
});
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
app.post('/wsapi/complete_email_addition', validate(["token"]), function(req, resp) {
Lloyd Hilaiel
committed
db.gotVerificationSecret(req.body.token, undefined, function(e) {
if (e) {
Lloyd Hilaiel
committed
logger.warn("couldn't complete email verification: " + e);
Lloyd Hilaiel
committed
resp.json({ success: false });
Lloyd Hilaiel
committed
} else {
Lloyd Hilaiel
committed
resp.json({ success: true });
Lloyd Hilaiel
committed
}
});
});
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
app.post('/wsapi/authenticate_user', validate(["email", "pass"]), function(req, resp) {
db.checkAuth(req.body.email, function(hash) {
Lloyd Hilaiel
committed
if (typeof hash !== 'string' ||
typeof req.body.pass !== 'string')
{
Lloyd Hilaiel
committed
return resp.json({ success: false });
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
bcrypt.compare(req.body.pass, hash, function (err, success) {
if (err) {
Lloyd Hilaiel
committed
logger.warn("error comparing passwords with bcrypt: " + err);
Lloyd Hilaiel
committed
success = false;
}
if (success) {
if (!req.session) req.session = {};
Lloyd Hilaiel
committed
setAuthenticatedUser(req.session, req.body.email);
Lloyd Hilaiel
committed
// if the work factor has changed, update the hash here. issue #204
// NOTE: this runs asynchronously and will not delay the response
Lloyd Hilaiel
committed
if (config.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
Lloyd Hilaiel
committed
logger.info("updating bcrypted password for email " + req.body.email);
bcrypt_password(req.body.pass, function(err, hash) {
db.updatePassword(req.body.email, hash, function(err) {
if (err) {
logger.error("error updating bcrypted password for email " + req.body.email, err);
}
});
});
}
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
resp.json({ success: success });
Lloyd Hilaiel
committed
});
Lloyd Hilaiel
committed
});
Lloyd Hilaiel
committed
app.post('/wsapi/remove_email', checkAuthed, validate(["email"]), function(req, resp) {
db.removeEmail(req.session.authenticatedUser, email, function(error) {
if (error) {
Lloyd Hilaiel
committed
logger.error("error removing email " + email);
httputils.badRequest(resp, error.toString());
} else {
Lloyd Hilaiel
committed
resp.json({ success: true });
app.post('/wsapi/account_cancel', checkAuthed, function(req, resp) {
db.cancelAccount(req.session.authenticatedUser, function(error) {
if (error) {
Lloyd Hilaiel
committed
logger.error("error cancelling account : " + error.toString());
httputils.badRequest(resp, error.toString());
} else {
Lloyd Hilaiel
committed
resp.json({ success: true });
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
app.post('/wsapi/cert_key', checkAuthed, validate(["email", "pubkey"]), function(req, resp) {
db.emailsBelongToSameAccount(req.session.authenticatedUser, req.body.email, function(sameAccount) {
// not same account? big fat error
if (!sameAccount) return httputils.badRequest(resp, "that email does not belong to you");
// parse the pubkey
var pk = ca.parsePublicKey(req.body.pubkey);
// we certify it for a day for now
var expiration = new Date();
Lloyd Hilaiel
committed
expiration.setTime(new Date().valueOf() + config.get('certificate_validity_ms'));
var cert = ca.certify(req.body.email, pk, expiration);
Lloyd Hilaiel
committed
resp.writeHead(200, {'Content-Type': 'text/plain'});
resp.write(cert);
resp.end();
app.post('/wsapi/logout', function(req, resp) {
clearAuthenticatedUser(req.session);
Lloyd Hilaiel
committed
resp.json({ success: true });
// in the cert world, syncing is not necessary,
// just get a list of emails.
// returns:
// {
// "foo@foo.com" : {..properties..}
// ...
// }
app.get('/wsapi/list_emails', checkAuthed, function(req, resp) {
logger.debug('listing emails for ' + req.session.authenticatedUser);
db.listEmails(req.session.authenticatedUser, function(err, emails) {
if (err) httputils.serverError(resp, err);
else resp.json(emails);
});
});
Lloyd Hilaiel
committed
// if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include
// fake_verification.js. This is used during testing only and should
// never be included in a production deployment
if (process.env['BROWSERID_FAKE_VERIFICATION']) {
require('./fake_verification.js').addVerificationWSAPI(app);
}
Lloyd Hilaiel
committed
}
exports.setup = setup;