From 8c5e67acedbec932b751eb894067df3ba2936994 Mon Sep 17 00:00:00 2001
From: Lloyd Hilaiel <lloyd@hilaiel.com>
Date: Thu, 10 Nov 2011 09:49:29 -0700
Subject: [PATCH] reorganize browserid process - break out view serving and
 wsapi handling.  preparation for dbwriter split.  issue #460

---
 bin/browserid                           | 282 +++-----------
 bin/dbwriter                            |   3 +
 lib/browserid/fake_verification.js      |   2 +-
 lib/browserid/views.js                  | 124 +++++++
 lib/browserid/wsapi.js                  | 464 ------------------------
 lib/configuration.js                    |  11 +-
 lib/db.js                               |  24 +-
 lib/{browserid => }/email.js            |   7 +-
 lib/secrets.js                          |   3 +-
 lib/wsapi.js                            | 230 ++++++++++++
 lib/wsapi/account_cancel.js             |  18 +
 lib/wsapi/authenticate_user.js          |  45 +++
 lib/wsapi/cert_key.js                   |  27 ++
 lib/wsapi/complete_email_addition.js    |  20 +
 lib/wsapi/complete_user_creation.js     |  50 +++
 lib/wsapi/email_addition_status.js      |  40 ++
 lib/wsapi/email_for_token.js            |  19 +
 lib/wsapi/have_email.js                 |  16 +
 lib/wsapi/list_emails.js                |  22 ++
 lib/wsapi/logout.js                     |  11 +
 lib/wsapi/remove_email.js               |  21 ++
 lib/wsapi/session_context.js            |  53 +++
 lib/wsapi/stage_email.js                |  44 +++
 lib/wsapi/stage_user.js                 |  52 +++
 lib/wsapi/user_creation_status.js       |  35 ++
 tests/ca-test.js                        |   2 +-
 tests/cert-emails-test.js               |   2 +-
 tests/cookie-session-security-test.js   |   2 +-
 tests/forgotten-email-test.js           |   2 +-
 tests/password-length-test.js           |   2 +-
 tests/registration-status-wsapi-test.js |   2 +-
 31 files changed, 918 insertions(+), 717 deletions(-)
 create mode 100755 bin/dbwriter
 create mode 100644 lib/browserid/views.js
 delete mode 100644 lib/browserid/wsapi.js
 rename lib/{browserid => }/email.js (95%)
 create mode 100644 lib/wsapi.js
 create mode 100644 lib/wsapi/account_cancel.js
 create mode 100644 lib/wsapi/authenticate_user.js
 create mode 100644 lib/wsapi/cert_key.js
 create mode 100644 lib/wsapi/complete_email_addition.js
 create mode 100644 lib/wsapi/complete_user_creation.js
 create mode 100644 lib/wsapi/email_addition_status.js
 create mode 100644 lib/wsapi/email_for_token.js
 create mode 100644 lib/wsapi/have_email.js
 create mode 100644 lib/wsapi/list_emails.js
 create mode 100644 lib/wsapi/logout.js
 create mode 100644 lib/wsapi/remove_email.js
 create mode 100644 lib/wsapi/session_context.js
 create mode 100644 lib/wsapi/stage_email.js
 create mode 100644 lib/wsapi/stage_user.js
 create mode 100644 lib/wsapi/user_creation_status.js

diff --git a/bin/browserid b/bin/browserid
index 92c84f079..2eb922c05 100755
--- a/bin/browserid
+++ b/bin/browserid
@@ -40,21 +40,21 @@ fs = require('fs'),
 path = require('path'),
 url = require('url'),
 http = require('http');
-sessions = require('connect-cookie-session'),
 urlparse = require('urlparse'),
 express = require('express');
 
 const
-wsapi = require('../lib/browserid/wsapi.js'),
+wsapi = require('../lib/wsapi.js'),
 httputils = require('../lib/httputils.js'),
 secrets = require('../lib/secrets.js'),
 db = require('../lib/db.js'),
 config = require('../lib/configuration.js'),
 heartbeat = require('../lib/heartbeat.js'),
 metrics = require('../lib/metrics.js'),
-logger = require('../lib/logging.js').logger
+logger = require('../lib/logging.js').logger,
 forward = require('../lib/browserid/http_forward'),
-shutdown = require('../lib/shutdown');
+shutdown = require('../lib/shutdown'),
+views = require('../lib/browserid/views.js');
 
 var app = undefined;
 
@@ -62,142 +62,14 @@ app = express.createServer();
 
 logger.info("browserid server starting up");
 
-const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path'));
-const COOKIE_KEY = 'browserid_state';
-
 // verify that we have a keysigner configured
 if (!config.get('keysigner_url')) {
   logger.error('missing required configuration - url for the keysigner (KEYSIGNER_URL in env)');
   process.exit(1);
 }
 
-// NOTE: ordering is important in this file.  Pay attention
-
-function router(app) {
-  app.set("views", path.join(__dirname, "..", "resources", "views"));
-
-  app.set('view options', {
-    production: config.get('use_minified_resources')
-  });
-
-  // this should probably be an internal redirect
-  // as soon as relative paths are figured out.
-  app.get('/sign_in', function(req, res, next ) {
-    metrics.userEntry(req);
-    res.render('dialog.ejs', {
-      title: 'A Better Way to Sign In',
-      layout: 'dialog_layout.ejs',
-      useJavascript: true,
-      production: config.get('use_minified_resources')
-    });
-  });
-
-  app.get('/communication_iframe', function(req, res, next ) {
-    res.removeHeader('x-frame-options');
-    res.render('communication_iframe.ejs', {
-      layout: false,
-      production: config.get('use_minified_resources')
-    });
-  });
-
-  app.get("/unsupported_dialog", function(req,res) {
-    res.render('unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
-  });
-
-  // Used for a relay page for communication.
-  app.get("/relay", function(req,res, next) {
-    // Allow the relay to be run within a frame
-    res.removeHeader('x-frame-options');
-    res.render('relay.ejs', {
-      layout: false,
-      production: config.get('use_minified_resources')
-    });
-  });
-
-  app.get('/', function(req,res) {
-    res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
-  });
-
-  app.get("/signup", function(req, res) {
-    res.render('signup.ejs', {title: 'Sign Up', fullpage: false});
-  });
-
-  app.get("/forgot", function(req, res) {
-    res.render('forgot.ejs', {title: 'Forgot Password', fullpage: false, email: req.query.email});
-  });
-
-  app.get("/signin", function(req, res) {
-    res.render('signin.ejs', {title: 'Sign In', fullpage: false});
-  });
-
-  app.get("/about", function(req, res) {
-    res.render('about.ejs', {title: 'About', fullpage: false});
-  });
-
-  app.get("/tos", function(req, res) {
-    res.render('tos.ejs', {title: 'Terms of Service', fullpage: false});
-  });
-
-  app.get("/privacy", function(req, res) {
-    res.render('privacy.ejs', {title: 'Privacy Policy', fullpage: false});
-  });
-
-  app.get("/verify_email_address", function(req, res) {
-    res.render('verifyuser.ejs', {title: 'Complete Registration', fullpage: true, token: req.query.token});
-  });
-
-  app.get("/add_email_address", function(req,res) {
-    res.render('verifyemail.ejs', {title: 'Verify Email Address', fullpage: false});
-  });
-
-  // REDIRECTS
-  REDIRECTS = {
-    "/manage": "/",
-    "/users": "/",
-    "/users/": "/",
-    "/primaries" : "/developers",
-    "/primaries/" : "/developers",
-    "/developers" : "https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site"
-  };
-
-  // set up all the redirects
-  // oh my watch out for scope issues on var url - closure time
-  for (var url in REDIRECTS) {
-    (function(from,to) {
-      app.get(from, function(req, res) {
-        res.redirect(to);
-      });
-    })(url, REDIRECTS[url]);
-  }
-
-  // register all the WSAPI handlers
-  wsapi.setup(app);
-
-  // the public key
-  app.get("/pk", function(req, res) {
-    res.json(config.get('public_key').toSimpleObject());
-  });
-
-  // vep bundle of JavaScript
-  app.get("/vepbundle", function(req, res) {
-    fs.readFile(__dirname + "/../node_modules/jwcrypto/vepbundle.js", function(error, content) {
-      if (error) {
-        res.writeHead(500);
-        res.end("oops");
-        console.log(error);
-      } else {
-        res.writeHead(200, {'Content-Type': 'text/javascript'});
-        res.write(content);
-        res.end();
-      }
-    });
-  });
-
-  shutdown.installUpdateHandler(app, function(readyForShutdown) {
-    logger.debug("closing database connection");
-    db.close(readyForShutdown)
-  });
-};
+// NOTE: ordering of middleware registration is important in this file, it is the
+// order in which middleware will be invoked as requests are processed.
 
 // #1 - Setup health check / heartbeat middleware.
 // This is in front of logging on purpose.  see issue #537
@@ -206,7 +78,7 @@ heartbeat.setup(app, function(cb) {
   heartbeat.check(config.get('keysigner_url'), cb);
 });
 
-// request to logger, dev formatted which omits personal data in the requests
+// #2 - logging!  all requests other than __heartbeat__ are logged
 app.use(express.logger({
   format: config.get('express_log_format'),
   stream: {
@@ -216,8 +88,24 @@ app.use(express.logger({
   }
 }));
 
-// if these are verify requests, we'll redirect them off
-// to the verifier
+// #3 - Add Strict-Transport-Security headers if we're serving over SSL
+if (config.get('scheme') == 'https') {
+  app.use(function(req, resp, next) {
+    // expires in 30 days, include subdomains like www
+    resp.setHeader("Strict-Transport-Security", "max-age=2592000; includeSubdomains");
+    next();
+    });
+}
+
+// #4 - prevent framing of everything.  content underneath that needs to be
+// framed must explicitly remove the x-frame-options
+app.use(function(req, resp, next) {
+  resp.setHeader('x-frame-options', 'DENY');
+  next();
+});
+
+// #5 - redirection!  redirect requests to the "verifier" or to the "dbwriter"
+// processes
 if (config.get('verifier_url')) {
   app.use(function(req, res, next) {
     if (/^\/verify$/.test(req.url)) {
@@ -234,51 +122,7 @@ if (config.get('verifier_url')) {
   });
 }
 
-// over SSL?
-var overSSL = (config.get('scheme') == 'https');
-
-app.use(express.cookieParser());
-
-var cookieSessionMiddleware = sessions({
-  secret: COOKIE_SECRET,
-  key: COOKIE_KEY,
-  cookie: {
-    path: '/wsapi',
-    httpOnly: true,
-    // IMPORTANT: we allow users to go 1 weeks on the same device
-    // without entering their password again
-    maxAge: config.get('authentication_duration_ms'),
-    secure: overSSL
-  }
-});
-
-// cookie sessions && cache control
-app.use(function(req, resp, next) {
-  // cookie sessions are only applied to calls to /wsapi
-  // as all other resources can be aggressively cached
-  // by layers higher up based on cache control headers.
-  // the fallout is that all code that interacts with sessions
-  // should be under /wsapi
-  if (/^\/wsapi/.test(req.url)) {
-    // explicitly disallow caching on all /wsapi calls (issue #294)
-    resp.setHeader('Cache-Control', 'no-cache, max-age=0');
-
-    // we set this parameter so the connect-cookie-session
-    // sends the cookie even though the local connection is HTTP
-    // (the load balancer does SSL)
-    if (overSSL)
-      req.connection.proxySecure = true;
-
-    return cookieSessionMiddleware(req, resp, next);
-
-  } else {
-    return next();
-  }
-});
-
-config.performSubstitution(app);
-
-// verify all JSON responses are objects - prevents regression on issue #217
+// #6 - verify all JSON responses are objects - prevents regression on issue #217
 app.use(function(req, resp, next) {
   var realRespJSON = resp.json;
   resp.json = function(obj) {
@@ -291,67 +135,37 @@ app.use(function(req, resp, next) {
   return next();
 });
 
-app.use(express.bodyParser());
-
-// Check CSRF token early.  POST requests are only allowed to
-// /wsapi and they always must have a valid csrf token
-app.use(function(req, resp, next) {
-  // only on POSTs
-  if (req.method == "POST") {
-    var denied = false;
-    if (!/^\/wsapi/.test(req.url)) { // post requests only allowed to /wsapi
-      denied = true;
-      logger.warn("CSRF validation failure: POST only allowed to /wsapi urls.  not '" + req.url + "'");
-    }
-
-    else if (req.session === undefined) { // there must be a session
-      denied = true;
-      logger.warn("CSRF validation failure: POST calls to /wsapi require an active session");
-    }
-
-    // the session must have a csrf token
-    else if (typeof req.session.csrf !== 'string') {
-      denied = true;
-      logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set");
-    }
-
-    // and the token must match what is sent in the post body
-    else if (req.body.csrf != req.session.csrf) {
-      denied = true;
-      // if any of these things are false, then we'll block the request
-      logger.warn("CSRF validation failure, token mismatch. got:" + req.body.csrf + " want:" + req.session.csrf);
-    }
+// #7 - perform response substitution to support local/dev/beta environments
+// (specifically, this replaces URLs in responses, e.g. https://browserid.org
+//  with https://diresworb.org)
+config.performSubstitution(app);
 
-    if (denied) return httputils.badRequest(resp, "CSRF violation");
+// #8 - handle /wsapi requests
+wsapi.setup(app);
 
-  }
-  return next();
-});
+// #9 - handle views for dynamicish content
+views.setup(app);
 
-// Strict Transport Security
-app.use(function(req, resp, next) {
-  if (overSSL) {
-    // expires in 30 days, include subdomains like www
-    resp.setHeader("Strict-Transport-Security", "max-age=2592000; includeSubdomains");
-  }
-  next();
-});
+// #10 - if nothing else has caught this request, serve static files
+app.use(express.static(path.join(__dirname, "..", "resources", "static")));
 
-// prevent framing
-app.use(function(req, resp, next) {
-  resp.setHeader('x-frame-options', 'DENY');
-  next();
+// #11 - calls to /code_update from localhost will restart the daemon,
+// this feature is not externally accessible and is only used by
+// the update logic
+shutdown.installUpdateHandler(app, function(readyForShutdown) {
+  logger.debug("closing database connection");
+  db.close(readyForShutdown)
 });
 
-// add the actual URL handlers other than static
-router(app);
-
-// use the express 'static' middleware for serving of static files (cache headers, HTTP range, etc)
-app.use(express.static(path.join(__dirname, "..", "resources", "static")));
+// #12 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('../lib/fake_verification.js').addVerificationWSAPI(app);
+}
 
 // open the databse
 db.open(config.get('database'), function (error) {
-
   if (error) {
     logger.error("can't open database: " + error);
     // let async logging flush, then exit 1
diff --git a/bin/dbwriter b/bin/dbwriter
new file mode 100755
index 000000000..79bfbf63b
--- /dev/null
+++ b/bin/dbwriter
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+require('./browserid');
diff --git a/lib/browserid/fake_verification.js b/lib/browserid/fake_verification.js
index 8fcafc904..7212ac81e 100644
--- a/lib/browserid/fake_verification.js
+++ b/lib/browserid/fake_verification.js
@@ -41,7 +41,7 @@
  */
 
 const
-email = require('./email.js'),
+email = require('../email.js'),
 configuration = require('../../libs/configuration.js'),
 url = require('url');
 
diff --git a/lib/browserid/views.js b/lib/browserid/views.js
new file mode 100644
index 000000000..c39f16791
--- /dev/null
+++ b/lib/browserid/views.js
@@ -0,0 +1,124 @@
+const metrics = require('../metrics.js');
+
+// all templated content, redirects, and renames are handled here.
+// anything that is not an api, and not static
+
+const
+path = require('path');
+
+exports.setup = function(app) {
+  app.set("views", path.join(__dirname, "..", "..", "resources", "views"));
+
+  app.set('view options', {
+    production: config.get('use_minified_resources')
+  });
+
+  // this should probably be an internal redirect
+  // as soon as relative paths are figured out.
+  app.get('/sign_in', function(req, res, next ) {
+    metrics.userEntry(req);
+    res.render('dialog.ejs', {
+      title: 'A Better Way to Sign In',
+      layout: 'dialog_layout.ejs',
+      useJavascript: true,
+      production: config.get('use_minified_resources')
+    });
+  });
+
+  app.get('/communication_iframe', function(req, res, next ) {
+    res.removeHeader('x-frame-options');
+    res.render('communication_iframe.ejs', {
+      layout: false,
+      production: config.get('use_minified_resources')
+    });
+  });
+
+  app.get("/unsupported_dialog", function(req,res) {
+    res.render('unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
+  });
+
+  // Used for a relay page for communication.
+  app.get("/relay", function(req,res, next) {
+    // Allow the relay to be run within a frame
+    res.removeHeader('x-frame-options');
+    res.render('relay.ejs', {
+      layout: false,
+      production: config.get('use_minified_resources')
+    });
+  });
+
+  app.get('/', function(req,res) {
+    res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
+  });
+
+  app.get("/signup", function(req, res) {
+    res.render('signup.ejs', {title: 'Sign Up', fullpage: false});
+  });
+
+  app.get("/forgot", function(req, res) {
+    res.render('forgot.ejs', {title: 'Forgot Password', fullpage: false, email: req.query.email});
+  });
+
+  app.get("/signin", function(req, res) {
+    res.render('signin.ejs', {title: 'Sign In', fullpage: false});
+  });
+
+  app.get("/about", function(req, res) {
+    res.render('about.ejs', {title: 'About', fullpage: false});
+  });
+
+  app.get("/tos", function(req, res) {
+    res.render('tos.ejs', {title: 'Terms of Service', fullpage: false});
+  });
+
+  app.get("/privacy", function(req, res) {
+    res.render('privacy.ejs', {title: 'Privacy Policy', fullpage: false});
+  });
+
+  app.get("/verify_email_address", function(req, res) {
+    res.render('verifyuser.ejs', {title: 'Complete Registration', fullpage: true, token: req.query.token});
+  });
+
+  app.get("/add_email_address", function(req,res) {
+    res.render('verifyemail.ejs', {title: 'Verify Email Address', fullpage: false});
+  });
+
+  // REDIRECTS
+  REDIRECTS = {
+    "/manage": "/",
+    "/users": "/",
+    "/users/": "/",
+    "/primaries" : "/developers",
+    "/primaries/" : "/developers",
+    "/developers" : "https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site"
+  };
+
+  // set up all the redirects
+  // oh my watch out for scope issues on var url - closure time
+  for (var url in REDIRECTS) {
+    (function(from,to) {
+      app.get(from, function(req, res) {
+        res.redirect(to);
+      });
+    })(url, REDIRECTS[url]);
+  }
+
+  // the public key
+  app.get("/pk", function(req, res) {
+    res.json(config.get('public_key').toSimpleObject());
+  });
+
+  // vep bundle of JavaScript
+  try {
+    const vepBundleFilePath = path.join(__dirname, "..", "..", "node_modules", "jwcrypto", "vepbundle.js");
+    var vepBundleFile = fs.readFileSync(vepBundleFilePath);
+  } catch(e) {
+    logger.error("can't read vepbundle file (" + vepBundleFilePath +"): " + e);
+  }
+
+  app.get("/vepbundle", function(req, res) {
+    res.writeHead(200, {'Content-Type': 'text/javascript'});
+    res.write(vepBundleFile);
+    res.end();
+  });
+};
diff --git a/lib/browserid/wsapi.js b/lib/browserid/wsapi.js
deleted file mode 100644
index a76778e8a..000000000
--- a/lib/browserid/wsapi.js
+++ /dev/null
@@ -1,464 +0,0 @@
-/* ***** 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.
-
-const
-db = require('../db.js'),
-url = require('url'),
-httputils = require('../httputils.js'),
-email = require('./email.js'),
-bcrypt = require('bcrypt'),
-crypto = require('crypto'),
-logger = require('../logging.js').logger,
-config = require('../configuration.js'),
-validate = require('../validate'),
-forward = require('./http_forward.js');
-
-// 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) {
-  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) >
-          config.get('authentication_duration_ms'))
-      {
-        throw "expired";
-      }
-      who = req.session.authenticatedUser;
-    }
-  } catch(e) {
-    logger.debug("Session authentication has expired:", e);
-    clearAuthenticatedUser(req.session);
-  }
-
-  return who;
-}
-
-// turned this into a proper middleware
-function checkAuthed(req, resp, next) {
-  if (!isAuthed(req)) {
-    return httputils.badRequest(resp, "requires authentication");
-  }
-
-  next();
-}
-
-function setup(app) {
-  // return the CSRF token, authentication status, and current server time (for assertion signing)
-  // IMPORTANT: this is safe because it's only readable by same-origin code
-  app.get('/wsapi/session_context', function(req, res) {
-    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);
-    }
-
-    var auth_status = false;
-
-    function sendResponse() {
-      res.json({
-        csrf_token: req.session.csrf,
-        server_time: (new Date()).getTime(),
-        authenticated: auth_status
-      });
-    };
-
-    // 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();
-      });
-    }
-  });
-
-  /* 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) {
-      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
-   */
-  app.post('/wsapi/stage_user', validate([ "email", "site" ]), function(req, resp) {
-    // staging a user logs you out.
-    clearAuthenticatedUser(req.session);
-
-    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.");
-      }
-
-      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 = {};
-
-          // 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;
-
-          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());
-      }
-    });
-  });
-
-  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;
-    }
-
-    // if the user is authenticated as the user in question, we're done
-    if (isAuthed(req) && req.session.authenticatedUser === email) {
-      return resp.json({ status: 'complete' });
-    }
-    // if the user isn't authenticated and there's no pendingCreation token,
-    // then they must authenticate
-    else if (!req.session.pendingCreation) {
-      return resp.json({ status: 'mustAuth' });
-    }
-
-    // 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) {
-      if (email) return resp.json({ status: 'pending' });
-      // 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;
-        resp.json({ status: 'mustAuth' });
-      }
-    });
-  });
-
-  function bcrypt_password(password, cb) {
-    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);
-      });
-    });
-  };
-
-  app.post('/wsapi/complete_user_creation', validate(["token", "pass"]), function(req, resp) {
-    // issue #155, valid password length is between 8 and 80 chars.
-    if (req.body.pass.length < 8 || req.body.pass.length > 80) {
-      httputils.badRequest(resp, "valid passwords are between 8 and 80 chars");
-      return;
-    }
-
-    // at the time the email verification is performed, we'll clear the pendingCreation
-    // data on the session.
-    delete req.session.pendingCreation;
-
-    // We should check to see if the verification secret is valid *before*
-    // bcrypting the password (which is expensive), to prevent a possible
-    // DoS attack.
-    db.emailForVerificationSecret(req.body.token, function(email) {
-      if (!email) return resp.json({ success: false} );
-
-      // now bcrypt the password
-      bcrypt_password(req.body.pass, function (err, hash) {
-        if (err) {
-          logger.error("can't bcrypt: " + err);
-          return resp.json({ success: false });
-        }
-
-        db.gotVerificationSecret(req.body.token, hash, function(err, email) {
-          if (err) {
-            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 });
-          }
-        });
-      });
-    });
-  });
-
-  app.post('/wsapi/stage_email', checkAuthed, validate(["email", "site"]), function (req, resp) {
-    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.");
-      }
-
-      try {
-        // on failure stageEmail may throw
-        db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
-
-          // store the email being added in session data
-          req.session.pendingAddition = secret;
-
-          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());
-      }
-    });
-  });
-
-  app.get('/wsapi/email_for_token', validate(["token"]), function(req,resp) {
-    db.emailForVerificationSecret(req.query.token, function(email) {
-      resp.json({ email: email });
-    });
-  });
-
-  app.get('/wsapi/email_addition_status', function(req, resp) {
-
-    var email = req.query.email;
-    if (typeof email !== 'string')
-    {
-      logger.warn("email_addition_status called without an 'email' parameter");
-      httputils.badRequest(resp, "missing 'email' parameter");
-      return;
-    }
-
-    // this is a pending email addition, it requires authentication
-    if (!isAuthed(req, resp)) {
-      delete req.session.pendingAddition;
-      return httputils.badRequest(resp, "requires authentication");
-    }
-
-    // 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;
-          resp.json({ status: 'complete' });
-        } else if (!req.session.pendingAddition) {
-          resp.json('failed');
-        } else {
-          db.emailForVerificationSecret(req.session.pendingAddition, function (email) {
-            if (email) {
-              return resp.json({ status: 'pending' });
-            } else {
-              delete req.session.pendingAddition;
-              resp.json({ status: 'failed' });
-            }
-          });
-        }
-      });
-  });
-
-  app.post('/wsapi/complete_email_addition', validate(["token"]), function(req, resp) {
-    db.gotVerificationSecret(req.body.token, undefined, function(e) {
-      if (e) {
-        logger.warn("couldn't complete email verification: " + e);
-        resp.json({ success: false });
-      } else {
-        resp.json({ success: true });
-      }
-    });
-  });
-
-  app.post('/wsapi/authenticate_user', validate(["email", "pass"]), function(req, resp) {
-    db.checkAuth(req.body.email, function(hash) {
-      if (typeof hash !== 'string' ||
-          typeof req.body.pass !== 'string')
-      {
-        return resp.json({ success: false });
-      }
-
-      bcrypt.compare(req.body.pass, hash, function (err, success) {
-        if (err) {
-          logger.warn("error comparing passwords with bcrypt: " + err);
-          success = false;
-        }
-        if (success) {
-          if (!req.session) req.session = {};
-          setAuthenticatedUser(req.session, req.body.email);
-
-          // if the work factor has changed, update the hash here.  issue #204
-          // NOTE: this runs asynchronously and will not delay the response
-          if (config.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
-            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);
-                }
-              });
-            });
-          }
-        }
-        resp.json({ success: success });
-      });
-    });
-  });
-
-  app.post('/wsapi/remove_email', checkAuthed, validate(["email"]), function(req, resp) {
-    var email = req.body.email;
-
-    db.removeEmail(req.session.authenticatedUser, email, function(error) {
-      if (error) {
-        logger.error("error removing email " + email);
-        httputils.badRequest(resp, error.toString());
-      } else {
-        resp.json({ success: true });
-      }});
-  });
-
-  app.post('/wsapi/account_cancel', checkAuthed, function(req, resp) {
-    db.cancelAccount(req.session.authenticatedUser, function(error) {
-      if (error) {
-        logger.error("error cancelling account : " + error.toString());
-        httputils.badRequest(resp, error.toString());
-      } else {
-        resp.json({ success: true });
-      }});
-  });
-
-  app.post('/wsapi/cert_key', checkAuthed, validate(["email", "pubkey"]), function(req, res) {
-    db.emailsBelongToSameAccount(req.session.authenticatedUser, req.body.email, function(sameAccount) {
-      // not same account? big fat error
-      if (!sameAccount) return httputils.badRequest(res, "that email does not belong to you");
-
-      // forward to the keysigner!
-      var keysigner = config.get('keysigner_url');
-      keysigner.path = '/wsapi/cert_key';
-      forward(keysigner, req, res, function(err) {
-        if (err) {
-          logger.error("error forwarding request:", err);
-        }
-      });
-    });
-  });
-
-  app.post('/wsapi/logout', function(req, resp) {
-    clearAuthenticatedUser(req.session);
-    resp.json({ success: true });
-  });
-
-  // returns a list of emails owned by the user
-  // 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);
-    });
-  });
-
-  // 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);
-  }
-}
-
-exports.setup = setup;
diff --git a/lib/configuration.js b/lib/configuration.js
index 4b150976d..87230eea6 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -80,7 +80,8 @@ g_configs.production = {
   database: {
     driver: "mysql",
     user: 'browserid',
-    create_schema: true
+    create_schema: true,
+    may_write: false
   },
   bcrypt_work_factor: 12,
   authentication_duration_ms: (2 * 7 * 24 * 60 * 60 * 1000),
@@ -95,7 +96,10 @@ g_configs.local =  {
   email_to_console: true, // don't send email, just dump verification URLs to console.
   use_minified_resources: false,
   var_path: path.join(__dirname, "..", "var"),
-  database: { driver: "json" },
+  database: {
+    driver: "json",
+    may_write: false
+  },
   bcrypt_work_factor: g_configs.production.bcrypt_work_factor,
   authentication_duration_ms: g_configs.production.authentication_duration_ms,
   certificate_validity_ms: g_configs.production.certificate_validity_ms,
@@ -211,6 +215,9 @@ g_config['process_type'] = path.basename(process.argv[1], ".js");
 
 g_config['public_key'] = secrets.loadPublicKey('root', exports.get('var_path'));
 
+// only allow the dbwriter process to write to the database
+g_config.database.may_write = true; //(g_config.process_type === 'dbwriter');
+
 // log the process_type
 setTimeout(function() {
   require("./logging.js").logger.info("process type is " + g_config["process_type"]);
diff --git a/lib/db.js b/lib/db.js
index e3a409203..ce154bd02 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -34,6 +34,7 @@
  * ***** END LICENSE BLOCK ***** */
 
 var logger = require('./logging.js').logger;
+const config = require('./configuration.js');
 
 var driver;
 
@@ -92,22 +93,37 @@ exports.onReady = function(f) {
   }, 0);
 };
 
+// these are read only database calls
 [
   'emailKnown',
   'isStaged',
   'emailsBelongToSameAccount',
-  'stageUser',
-  'stageEmail',
-  'gotVerificationSecret',
   'emailForVerificationSecret',
   'checkAuth',
   'listEmails',
+  'lastStaged'
+].forEach(function(fn) {
+  exports[fn] = function() {
+    checkReady();
+    driver[fn].apply(undefined, arguments);
+  };
+});
+
+// These are database calls that write.  Database
+// writing must be enabled on the process for them
+// to work.
+[
+  'stageUser',
+  'stageEmail',
+  'gotVerificationSecret',
   'removeEmail',
   'cancelAccount',
   'updatePassword',
-  'lastStaged'
 ].forEach(function(fn) {
   exports[fn] = function() {
+    if (!config.get('database').may_write) {
+      throw "this process may not write the database"
+    }
     checkReady();
     driver[fn].apply(undefined, arguments);
   };
diff --git a/lib/browserid/email.js b/lib/email.js
similarity index 95%
rename from lib/browserid/email.js
rename to lib/email.js
index b0b873d70..5bee98ed5 100644
--- a/lib/browserid/email.js
+++ b/lib/email.js
@@ -34,13 +34,12 @@
  * ***** END LICENSE BLOCK ***** */
 
 const
-db = require('../db.js'),
 emailer = require('nodemailer'),
 fs = require('fs'),
 path = require('path'),
 mustache = require('mustache'),
-config = require('../configuration.js'),
-logger = require('../logging.js').logger;
+config = require('./configuration.js'),
+logger = require('./logging.js').logger;
 
 /* if smtp parameters are configured, use them */
 var smtp_params = config.get('smtp');
@@ -56,7 +55,7 @@ if (smtp_params && smtp_params.host) {
   }
 }
 
-const template = fs.readFileSync(path.join(__dirname, "prove_template.txt")).toString();
+const template = fs.readFileSync(path.join(__dirname, "browserid", "prove_template.txt")).toString();
 
 var interceptor = undefined;
 
diff --git a/lib/secrets.js b/lib/secrets.js
index 6382d5e42..41d53d024 100644
--- a/lib/secrets.js
+++ b/lib/secrets.js
@@ -36,8 +36,7 @@
 const
 path = require('path'),
 fs = require('fs'),
-jwk = require('jwcrypto/jwk'),
-configuration = require("./configuration");
+jwk = require('jwcrypto/jwk');
 
 exports.generate = function(chars) {
   var str = "";
diff --git a/lib/wsapi.js b/lib/wsapi.js
new file mode 100644
index 000000000..3c14d7d53
--- /dev/null
+++ b/lib/wsapi.js
@@ -0,0 +1,230 @@
+// an abstraction that implements all of the cookie handling, CSRF protection,
+// etc of the wsapi.  This module also routes request to the approriate handlers
+// underneath wsapi/
+//
+// each handler under wsapi/ supports the following exports:
+//   exports.process - function(req, res) - process a request
+//   exports.writes_db - must be true if the processing causes a database write
+//   exports.method - either 'get' or 'post'
+//   exports.authed - whether the wsapi requires authentication
+//   exports.args - an array of arguments that should be verified
+
+
+const
+sessions = require('connect-cookie-session'),
+express = require('express');
+secrets = require('./secrets'),
+config = require('./configuration'),
+logger = require('./logging.js').logger,
+httputils = require('./httputils.js'),
+url = require('url'),
+fs = require('fs'),
+path = require('path'),
+validate = require('./validate'),
+bcrypt = require('bcrypt');
+
+const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path'));
+const COOKIE_KEY = 'browserid_state';
+
+function clearAuthenticatedUser(session) {
+  Object.keys(session).forEach(function(k) {
+    if (k !== 'csrf') delete session[k];
+  });
+}
+
+
+function isAuthed(req) {
+  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) >
+          config.get('authentication_duration_ms'))
+      {
+        throw "expired";
+      }
+      who = req.session.authenticatedUser;
+    }
+  } catch(e) {
+    logger.debug("Session authentication has expired:", e);
+    clearAuthenticatedUser(req.session);
+  }
+
+  return who;
+}
+
+function bcryptPassword(password, cb) {
+  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);
+    });
+  });
+};
+
+function setAuthenticatedUser(session, email) {
+  session.authenticatedUser = email;
+  session.authenticatedAt = new Date();
+}
+
+// common functions exported, for use by different api calls
+exports.clearAuthenticatedUser = clearAuthenticatedUser;
+exports.isAuthed = isAuthed;
+exports.bcryptPassword = bcryptPassword;
+exports.setAuthenticatedUser = setAuthenticatedUser;
+
+exports.setup = function(app) {
+
+  // XXX: we can and should make all of the logic below only take effect for POST requests
+  // to /wsapi to reduce code run for other requests (cookie parsing, etc)
+
+  // If externally we're serving content over SSL we can enable things
+  // like strict transport security and change the way cookies are set
+  const overSSL = (config.get('scheme') == 'https');
+
+  app.use(express.cookieParser());
+
+  var cookieSessionMiddleware = sessions({
+    secret: COOKIE_SECRET,
+    key: COOKIE_KEY,
+    cookie: {
+      path: '/wsapi',
+      httpOnly: true,
+      // IMPORTANT: we allow users to go 1 weeks on the same device
+      // without entering their password again
+      maxAge: config.get('authentication_duration_ms'),
+      secure: overSSL
+    }
+  });
+
+  // cookie sessions && cache control
+  app.use(function(req, resp, next) {
+    // cookie sessions are only applied to calls to /wsapi
+    // as all other resources can be aggressively cached
+    // by layers higher up based on cache control headers.
+    // the fallout is that all code that interacts with sessions
+    // should be under /wsapi
+    if (/^\/wsapi/.test(req.url)) {
+      // explicitly disallow caching on all /wsapi calls (issue #294)
+      resp.setHeader('Cache-Control', 'no-cache, max-age=0');
+
+      // we set this parameter so the connect-cookie-session
+      // sends the cookie even though the local connection is HTTP
+      // (the load balancer does SSL)
+      if (overSSL)
+        req.connection.proxySecure = true;
+
+      return cookieSessionMiddleware(req, resp, next);
+
+    } else {
+      return next();
+    }
+  });
+
+  app.use(express.bodyParser());
+
+  // Check CSRF token early.  POST requests are only allowed to
+  // /wsapi and they always must have a valid csrf token
+  app.use(function(req, resp, next) {
+    // only on POSTs
+    if (req.method == "POST") {
+      var denied = false;
+      if (!/^\/wsapi/.test(req.url)) { // post requests only allowed to /wsapi
+        denied = true;
+        logger.warn("CSRF validation failure: POST only allowed to /wsapi urls.  not '" + req.url + "'");
+      }
+
+      else if (req.session === undefined) { // there must be a session
+        denied = true;
+        logger.warn("CSRF validation failure: POST calls to /wsapi require an active session");
+      }
+
+      // the session must have a csrf token
+      else if (typeof req.session.csrf !== 'string') {
+        denied = true;
+        logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set");
+      }
+
+      // and the token must match what is sent in the post body
+      else if (req.body.csrf != req.session.csrf) {
+        denied = true;
+        // if any of these things are false, then we'll block the request
+        logger.warn("CSRF validation failure, token mismatch. got:" + req.body.csrf + " want:" + req.session.csrf);
+      }
+
+      if (denied) return httputils.badRequest(resp, "CSRF violation");
+
+    }
+    return next();
+  });
+
+  const WSAPI_PREFIX = '/wsapi/';
+
+  // load all of the APIs supported by this process
+  var wsapis = { };
+
+  logger.debug("registering WSAPIs:");
+  fs.readdirSync(path.join(__dirname, 'wsapi')).forEach(function (f) {
+    // skip files that don't have a .js suffix or start with a dot
+    if (f.length <= 3 || f.substr(-3) !== '.js' || f.substr(0,1) === '.') return;
+    var operation = f.substr(0, f.length - 3);
+
+    try {
+      var api = require(path.join(__dirname, 'wsapi', f));
+      wsapis[operation] = api;
+
+      // set up the argument validator
+      if (api.args) {
+        if (!Array.isArray(api.args)) throw "exports.args must be an array of strings";
+        wsapis[operation].validate = validate(api.args);
+      } else {
+        wsapis[operation].validate = function(req,res,next) { next(); };
+      }
+
+    } catch(e) {
+      var msg = "error registering " + operation + " api: " + e;
+      logger.error(msg);
+      throw msg;
+    }
+
+    logger.debug('  ' + operation);
+
+  });
+
+  app.use(function(req, resp, next) {
+    var purl = url.parse(req.url);
+
+    if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) {
+      const operation = purl.pathname.substr(WSAPI_PREFIX.length);
+      if (wsapis.hasOwnProperty(operation) &&
+          wsapis[operation].method.toLowerCase() === req.method.toLowerCase()) {
+
+        // does the request require authentication?
+        if (wsapis[operation].authed && !isAuthed(req)) {
+          return httputils.badRequest(resp, "requires authentication");
+        }
+
+        // validate the arguments of the request
+        wsapis[operation].validate(req, resp, function() {
+          wsapis[operation].process(req, resp);
+        });
+      } else {
+        return httputils.badRequest(resp, "no such api");
+      }
+
+    } else {
+      next();
+    }
+  });
+};
diff --git a/lib/wsapi/account_cancel.js b/lib/wsapi/account_cancel.js
new file mode 100644
index 000000000..4850bafc5
--- /dev/null
+++ b/lib/wsapi/account_cancel.js
@@ -0,0 +1,18 @@
+const
+db = require('../db.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger;
+
+exports.method = 'post';
+exports.writes_db = true;
+exports.authed = true;
+
+exports.process = function(req, res) {
+  db.cancelAccount(req.session.authenticatedUser, function(error) {
+    if (error) {
+      logger.error("error cancelling account : " + error.toString());
+      httputils.badRequest(res, error.toString());
+    } else {
+      res.json({ success: true });
+    }});
+};
diff --git a/lib/wsapi/authenticate_user.js b/lib/wsapi/authenticate_user.js
new file mode 100644
index 000000000..80d6e0dbd
--- /dev/null
+++ b/lib/wsapi/authenticate_user.js
@@ -0,0 +1,45 @@
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger,
+bcrypt = require('bcrypt');
+
+exports.method = 'post';
+exports.writes_db = true;
+exports.authed = false;
+exports.args = ['email','pass'];
+
+exports.process = function(req, res) {
+  db.checkAuth(req.body.email, function(hash) {
+    if (typeof hash !== 'string' || typeof req.body.pass !== 'string')
+    {
+      return res.json({ success: false });
+    }
+
+    bcrypt.compare(req.body.pass, hash, function (err, success) {
+      if (err) {
+        logger.warn("error comparing passwords with bcrypt: " + err);
+        success = false;
+      }
+      if (success) {
+        if (!req.session) req.session = {};
+        wsapi.setAuthenticatedUser(req.session, req.body.email);
+
+        // if the work factor has changed, update the hash here.  issue #204
+        // NOTE: this runs asynchronously and will not delay the response
+        if (config.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
+          logger.info("updating bcrypted password for email " + req.body.email);
+          wsapi.bcryptPassword(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);
+              }
+            });
+          });
+        }
+      }
+      res.json({ success: success });
+    });
+  });
+};
diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js
new file mode 100644
index 000000000..57d821281
--- /dev/null
+++ b/lib/wsapi/cert_key.js
@@ -0,0 +1,27 @@
+const
+db = require('../db.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger,
+forward = require('../browserid/http_forward.js'),
+config = require('../configuration.js');
+
+exports.method = 'post';
+exports.writes_db = false;
+exports.authed = true;
+exports.args = ['email','pubkey'];
+
+exports.process = function(req, res) {
+  db.emailsBelongToSameAccount(req.session.authenticatedUser, req.body.email, function(sameAccount) {
+    // not same account? big fat error
+    if (!sameAccount) return httputils.badRequest(res, "that email does not belong to you");
+
+    // forward to the keysigner!
+    var keysigner = config.get('keysigner_url');
+    keysigner.path = '/wsapi/cert_key';
+    forward(keysigner, req, res, function(err) {
+      if (err) {
+        logger.error("error forwarding request:", err);
+      }
+    });
+  });
+};
diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js
new file mode 100644
index 000000000..d0a1cb091
--- /dev/null
+++ b/lib/wsapi/complete_email_addition.js
@@ -0,0 +1,20 @@
+const
+db = require('../db.js'),
+logger = require('../logging.js').logger;
+
+exports.method = 'post';
+exports.writes_db = false;
+// XXX: see issue #290 - we want to require authentication here and update frontend code
+exports.authed = false;
+exports.args = ['token'];
+
+exports.process = function(req, res) {
+  db.gotVerificationSecret(req.body.token, undefined, function(e) {
+    if (e) {
+      logger.warn("couldn't complete email verification: " + e);
+      res.json({ success: false });
+    } else {
+      res.json({ success: true });
+    }
+  });
+};
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
new file mode 100644
index 000000000..1e586c145
--- /dev/null
+++ b/lib/wsapi/complete_user_creation.js
@@ -0,0 +1,50 @@
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger;
+
+exports.method = 'post';
+exports.writes_db = true;
+exports.authed = false;
+exports.args = ['token','pass'];
+
+exports.process = function(req, res) {
+  // issue #155, valid password length is between 8 and 80 chars.
+  if (req.body.pass.length < 8 || req.body.pass.length > 80) {
+    httputils.badRequest(res, "valid passwords are between 8 and 80 chars");
+    return;
+  }
+
+  // at the time the email verification is performed, we'll clear the pendingCreation
+  // data on the session.
+  delete req.session.pendingCreation;
+
+  // We should check to see if the verification secret is valid *before*
+  // bcrypting the password (which is expensive), to prevent a possible
+  // DoS attack.
+  db.emailForVerificationSecret(req.body.token, function(email) {
+    if (!email) return res.json({ success: false} );
+
+    // now bcrypt the password
+    wsapi.bcryptPassword(req.body.pass, function (err, hash) {
+      if (err) {
+        logger.error("can't bcrypt: " + err);
+        return res.json({ success: false });
+      }
+
+      db.gotVerificationSecret(req.body.token, hash, function(err, email) {
+        if (err) {
+          logger.warn("couldn't complete email verification: " + err);
+          res.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.
+          wsapi.setAuthenticatedUser(req.session, email);
+          res.json({ success: true });
+        }
+      });
+    });
+  });
+};
diff --git a/lib/wsapi/email_addition_status.js b/lib/wsapi/email_addition_status.js
new file mode 100644
index 000000000..dc7ef61bb
--- /dev/null
+++ b/lib/wsapi/email_addition_status.js
@@ -0,0 +1,40 @@
+const
+db = require('../db.js');
+
+/* 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 = 'get';
+exports.writes_db = false;
+exports.authed = true;
+exports.args = ['email'];
+
+exports.process = function(req, res) {
+  var email = req.query.email;
+
+  // 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;
+        res.json({ status: 'complete' });
+      } else if (!req.session.pendingAddition) {
+        res.json('failed');
+      } else {
+        db.emailForVerificationSecret(req.session.pendingAddition, function (email) {
+          if (email) {
+            return res.json({ status: 'pending' });
+          } else {
+            delete req.session.pendingAddition;
+            res.json({ status: 'failed' });
+          }
+        });
+      }
+    });
+};
diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js
new file mode 100644
index 000000000..3a6f70748
--- /dev/null
+++ b/lib/wsapi/email_for_token.js
@@ -0,0 +1,19 @@
+const
+db = require('../db.js');
+
+/* 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 = 'get';
+exports.writes_db = false;
+exports.authed = false;
+exports.args = ['token'];
+
+exports.process = function(req, res) {
+  db.emailForVerificationSecret(req.query.token, function(email) {
+    res.json({ email: email });
+  });
+};
diff --git a/lib/wsapi/have_email.js b/lib/wsapi/have_email.js
new file mode 100644
index 000000000..e399bb86b
--- /dev/null
+++ b/lib/wsapi/have_email.js
@@ -0,0 +1,16 @@
+const
+db = require('../db.js');
+
+// return if an email is known to browserid
+
+exports.method = 'get';
+exports.writes_db = false;
+exports.authed = false;
+exports.args = ['email'];
+
+exports.process = function(req, resp) {
+  var email = url.parse(req.url, true).query['email'];
+  db.emailKnown(email, function(known) {
+    resp.json({ email_known: known });
+  });
+};
diff --git a/lib/wsapi/list_emails.js b/lib/wsapi/list_emails.js
new file mode 100644
index 000000000..5263c2265
--- /dev/null
+++ b/lib/wsapi/list_emails.js
@@ -0,0 +1,22 @@
+const
+db = require('../db.js'),
+logger = require('../logging.js').logger;
+
+// returns a list of emails owned by the user:
+//
+// {
+//   "foo@foo.com" : {..properties..}
+//   ...
+// }
+
+exports.method = 'get';
+exports.writes_db = false;
+exports.authed = true;
+
+exports.process = 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);
+  });
+};
diff --git a/lib/wsapi/logout.js b/lib/wsapi/logout.js
new file mode 100644
index 000000000..ff48f014b
--- /dev/null
+++ b/lib/wsapi/logout.js
@@ -0,0 +1,11 @@
+const
+wsapi = require('../wsapi.js');
+
+exports.method = 'post';
+exports.writes_db = false;
+exports.authed = true;
+
+exports.process = function(req, res) {
+  wsapi.clearAuthenticatedUser(req.session);
+  res.json({ success: true });
+};
diff --git a/lib/wsapi/remove_email.js b/lib/wsapi/remove_email.js
new file mode 100644
index 000000000..404ee9ac6
--- /dev/null
+++ b/lib/wsapi/remove_email.js
@@ -0,0 +1,21 @@
+const
+db = require('../db.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger;
+
+exports.method = 'post';
+exports.writes_db = true;
+exports.authed = true;
+exports.args = ['email'];
+
+exports.process = function(req, res) {
+  var email = req.body.email;
+
+  db.removeEmail(req.session.authenticatedUser, email, function(error) {
+    if (error) {
+      logger.error("error removing email " + email);
+      httputils.badRequest(res, error.toString());
+    } else {
+      res.json({ success: true });
+    }});
+};
diff --git a/lib/wsapi/session_context.js b/lib/wsapi/session_context.js
new file mode 100644
index 000000000..3a73014ef
--- /dev/null
+++ b/lib/wsapi/session_context.js
@@ -0,0 +1,53 @@
+const
+db = require('../db.js'),
+logger = require('../logging.js').logger,
+crypto = require('crypto'),
+wsapi = require('../wsapi.js');
+
+// return the CSRF token, authentication status, and current server time (for assertion signing)
+// IMPORTANT: this is safe because it's only readable by same-origin code
+
+exports.method = 'get';
+exports.writes_db = false;
+exports.authed = false;
+
+exports.process = function(req, res) {
+  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);
+  }
+
+  var auth_status = false;
+
+  function sendResponse() {
+    res.json({
+      csrf_token: req.session.csrf,
+      server_time: (new Date()).getTime(),
+      authenticated: auth_status
+    });
+  };
+
+  // if they're authenticated for an email address that we don't know about,
+  // then we should purge the stored cookie
+  if (!wsapi.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");
+        wsapi.clearAuthenticatedUser(req.session);
+      } else {
+        logger.debug("user is authenticated");
+        auth_status = true;
+      }
+      sendResponse();
+    });
+  }
+};
diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js
new file mode 100644
index 000000000..df2cbe07f
--- /dev/null
+++ b/lib/wsapi/stage_email.js
@@ -0,0 +1,44 @@
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger,
+email = require('../email.js');
+
+/* 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 = true;
+exports.args = ['email','site'];
+
+exports.process = function(req, res) {
+  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(res, "throttling.  try again later.");
+    }
+
+    try {
+      // on failure stageEmail may throw
+      db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
+
+        // store the email being added in session data
+        req.session.pendingAddition = secret;
+
+        res.json({ success: true });
+
+        // let's now kick out a verification email!
+        email.sendAddAddressEmail(req.body.email, req.body.site, secret);
+      });
+    } catch(e) {
+      // we should differentiate tween' 400 and 500 here.
+      httputils.badRequest(res, e.toString());
+    }
+  });
+};
diff --git a/lib/wsapi/stage_user.js b/lib/wsapi/stage_user.js
new file mode 100644
index 000000000..7830bb840
--- /dev/null
+++ b/lib/wsapi/stage_user.js
@@ -0,0 +1,52 @@
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js'),
+httputils = require('../httputils'),
+logger = require('../logging.js').logger,
+email = require('../email.js');
+
+/* 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'];
+
+exports.process = function(req, resp) {
+  // staging a user logs you out.
+  wsapi.clearAuthenticatedUser(req.session);
+
+  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.");
+    }
+
+    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 = {};
+
+        // 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;
+
+        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());
+    }
+  });
+};
diff --git a/lib/wsapi/user_creation_status.js b/lib/wsapi/user_creation_status.js
new file mode 100644
index 000000000..ea80db258
--- /dev/null
+++ b/lib/wsapi/user_creation_status.js
@@ -0,0 +1,35 @@
+const
+db = require('../db.js'),
+wsapi = require('../wsapi.js');
+
+exports.method = 'get';
+exports.writes_db = false;
+exports.authed = false;
+exports.args = ['email'];
+
+exports.process = function(req, res) {
+  var email = req.query.email;
+
+  // if the user is authenticated as the user in question, we're done
+  if (wsapi.isAuthed(req) && req.session.authenticatedUser === email) {
+    return res.json({ status: 'complete' });
+  }
+  // if the user isn't authenticated and there's no pendingCreation token,
+  // then they must authenticate
+  else if (!req.session.pendingCreation) {
+    return res.json({ status: 'mustAuth' });
+  }
+
+  // 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) {
+    if (email) return res.json({ status: 'pending' });
+    // 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;
+      res.json({ status: 'mustAuth' });
+    }
+  });
+};
diff --git a/tests/ca-test.js b/tests/ca-test.js
index 7336c88a0..028a182d1 100755
--- a/tests/ca-test.js
+++ b/tests/ca-test.js
@@ -41,7 +41,7 @@ const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/browserid/email.js'),
+email = require('../lib/email.js'),
 ca = require('../lib/keysigner/ca.js'),
 jwcert = require('jwcrypto/jwcert'),
 jwk = require('jwcrypto/jwk'),
diff --git a/tests/cert-emails-test.js b/tests/cert-emails-test.js
index e8c313a25..531f77b7f 100755
--- a/tests/cert-emails-test.js
+++ b/tests/cert-emails-test.js
@@ -41,7 +41,7 @@ const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/browserid/email.js'),
+email = require('../lib/email.js'),
 ca = require('../lib/keysigner/ca.js'),
 jwcert = require('jwcrypto/jwcert'),
 jwk = require('jwcrypto/jwk'),
diff --git a/tests/cookie-session-security-test.js b/tests/cookie-session-security-test.js
index 17705ed9d..1393ecbf4 100755
--- a/tests/cookie-session-security-test.js
+++ b/tests/cookie-session-security-test.js
@@ -42,7 +42,7 @@ vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
 wcli = require('../lib/wsapi_client.js');
-email = require('../lib/browserid/email.js'),
+email = require('../lib/email.js'),
 jwcert = require('jwcrypto/jwcert'),
 jwk = require('jwcrypto/jwk'),
 jws = require('jwcrypto/jws');
diff --git a/tests/forgotten-email-test.js b/tests/forgotten-email-test.js
index 3f30279d7..14b2c2729 100755
--- a/tests/forgotten-email-test.js
+++ b/tests/forgotten-email-test.js
@@ -41,7 +41,7 @@ const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/browserid/email.js');
+email = require('../lib/email.js');
 
 var suite = vows.describe('forgotten-email');
 
diff --git a/tests/password-length-test.js b/tests/password-length-test.js
index 0a4b7e2dc..e8d47283b 100755
--- a/tests/password-length-test.js
+++ b/tests/password-length-test.js
@@ -42,7 +42,7 @@ require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/browserid/email.js');
+email = require('../lib/email.js');
 
 var suite = vows.describe('password-length');
 
diff --git a/tests/registration-status-wsapi-test.js b/tests/registration-status-wsapi-test.js
index 171974e91..dfb962282 100755
--- a/tests/registration-status-wsapi-test.js
+++ b/tests/registration-status-wsapi-test.js
@@ -117,7 +117,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 400);
     },
     "returns an error string": function (r, err) {
-      assert.strictEqual(r.body, "Bad Request: no 'email' parameter");
+      assert.strictEqual(r.body, "Bad Request: missing 'email' argument");
     }
   }
 });
-- 
GitLab