Skip to content
Browse files

integrating train 2011.08.18

  • Loading branch information...
2 parents a867ecb + c3356e3 commit de6afc97e12fee59b1613c69313448340726d2aa @shane-tomlinson shane-tomlinson committed
Showing with 4,379 additions and 2,076 deletions.
  1. +2 −1 .gitignore
  2. +24 −0 ChangeLog
  3. +10 −2 DEPLOYMENT.md
  4. +5 −0 LICENSE
  5. +0 −1 README.md
  6. +127 −58 browserid/app.js
  7. +91 −369 browserid/lib/db.js
  8. +381 −0 browserid/lib/db_json.js
  9. +471 −0 browserid/lib/db_mysql.js
  10. +72 −12 browserid/lib/email.js
  11. +35 −0 browserid/lib/httputils.js
  12. +38 −3 browserid/lib/secrets.js
  13. +35 −0 browserid/lib/webfinger.js
  14. +112 −34 browserid/lib/wsapi.js
  15. +37 −5 browserid/run.js
  16. +7 −1 browserid/static/css/style.css
  17. +83 −0 browserid/static/dialog/controllers/addemail_controller.js
  18. +86 −0 browserid/static/dialog/controllers/authenticate_controller.js
  19. +117 −0 browserid/static/dialog/controllers/checkregistration_controller.js
  20. +71 −0 browserid/static/dialog/controllers/chooseemail_controller.js
  21. +171 −0 browserid/static/dialog/controllers/createaccount_controller.js
  22. +165 −464 browserid/static/dialog/controllers/dialog_controller.js
  23. +116 −0 browserid/static/dialog/controllers/forgotpassword_controller.js
  24. +135 −0 browserid/static/dialog/controllers/page_controller.js
  25. +48 −2 browserid/static/dialog/dialog.js
  26. +35 −3 browserid/static/dialog/register_iframe.js
  27. +87 −0 browserid/static/dialog/resources/browserid-errors.js
  28. +58 −0 browserid/static/dialog/resources/browserid-extensions.js
  29. +225 −0 browserid/static/dialog/resources/browserid-network.js
  30. +64 −0 browserid/static/dialog/resources/browserid-wait.js
  31. +35 −0 browserid/static/dialog/resources/channel.js
  32. +35 −0 browserid/static/dialog/resources/crypto-api.js
  33. +0 −781 browserid/static/dialog/resources/main.js
  34. +34 −0 browserid/static/dialog/resources/storage.js
  35. +14 −10 browserid/static/dialog/style.css
  36. +2 −2 browserid/static/dialog/views/addemail.ejs
  37. +1 −1 browserid/static/dialog/views/authenticate.ejs
  38. +11 −4 browserid/static/dialog/views/body.ejs
  39. +1 −1 browserid/static/dialog/views/bottom-addemail.ejs
  40. +1 −1 browserid/static/dialog/views/bottom-confirmemail.ejs
  41. +2 −2 browserid/static/dialog/views/bottom-continue.ejs
  42. +1 −1 browserid/static/dialog/views/bottom-pickemail.ejs
  43. +1 −1 browserid/static/dialog/views/bottom-signin.ejs
  44. +1 −1 browserid/static/dialog/views/bottom.ejs
  45. +4 −3 browserid/static/dialog/views/create.ejs
  46. +4 −3 browserid/static/dialog/views/forgotpassword.ejs
  47. +5 −7 browserid/static/dialog/views/signin.ejs
  48. +41 −6 browserid/static/include.js
  49. +63 −10 browserid/static/js/browserid.js
  50. +88 −47 browserid/tests/db-test.js
  51. +48 −5 browserid/tests/forgotten-email-test.js
  52. +0 −16 browserid/tests/lib/email-interceptor.js
  53. +46 −0 browserid/tests/lib/start-stop.js
  54. +47 −0 browserid/tests/lib/test_env.js
  55. +110 −71 browserid/tests/lib/wsapi.js
  56. +107 −0 browserid/tests/password-length-test.js
  57. +44 −6 browserid/tests/registration-status-wsapi-test.js
  58. +0 −3 browserid/views/manage.ejs
  59. +0 −3 browserid/views/prove.ejs
  60. +95 −30 libs/configuration.js
  61. +72 −43 libs/logging.js
  62. +117 −0 libs/metrics.js
  63. +35 −0 libs/substitute.js
  64. +4 −3 package.json
  65. +42 −16 run.js
  66. +4 −0 scripts/branch_train.sh
  67. +22 −0 scripts/merge_train.sh
  68. +16 −0 scripts/test_db_connectivity.js
  69. +23 −0 test.sh
  70. +0 −1 verifier/.gitignore
  71. +64 −23 verifier/app.js
  72. +35 −0 verifier/lib/httputils.js
  73. +55 −17 verifier/lib/idassertion.js
  74. +35 −0 verifier/lib/jwt.js
  75. +35 −0 verifier/lib/make_assertion.js
  76. +36 −3 verifier/run.js
  77. +35 −0 verifier/tests/run.js
View
3 .gitignore
@@ -1,4 +1,5 @@
*~
\#*\#
.\#*
-node_modules
+/node_modules
+/var
View
24 ChangeLog
@@ -1,3 +1,27 @@
+train-2011.08.18:
+ * upon clickthrough of the email link, don't have the browser window close itself: #162
+ * passwords must be between 8 and 80 chars: #155
+ * improved handling of emailing & verification urls during local development & testing: #88
+ * language changes in dialog: #150
+ * many improvements to unit tests: #171
+ * forgotten password flow was broken with port to mysql, fixed: #170
+ * improved metrics reporting abstraction: #168
+ * moved all server logging into a single file: #169
+ * all files created at execution time are now in one location: #172
+ * developer ergonomics - improved colorized logging with terse webserver output to console
+ * always require a user to authenticate if they don't have an active session: #74
+ * improved CSRF protection to fix race conditions in previous train: #173
+
+train-2011.08.12:
+ * massive zero-user-visibile refactoring of dialog javascript.
+ * fix cancel button in "waiting for verification state" (issue #147)
+ * all browserid source is now tri-licensed (MPL1.1/GPL/LGPL). (issue #141)
+ * fixes for mobile firefox (fennec). (issue #140)
+ * mysql support implemented for browserid (default persistence production) (issue #71)
+ * json persistence support added - a standalone dead simple persistence layer which is the default for local development and requires no external software.
+ * email secrets are now persisted in the database, so upon server restart outstanding verification links are no longer invalidated (issue #91)
+ * (website) styling changes - like fix issues where links on dev page were being displayed white on white.
+
train-2011.08.04:
* when user closes dialog without clicking "cancel", properly return 'null' to the webpage (via getVerifiedEmail callback) - issue #107
* improve checks to warn developer that prerequisite software is missing. issue #110
View
12 DEPLOYMENT.md
@@ -85,9 +85,17 @@ Subsequent steps use different software which you might need to install.
* **curl** - used to iniate http requests from the cmd line (to kick the browserid server)
* **java** - used to minify css
- * **libsqlite3-dev** - database libraries
+ * **mysql 5.1+** - the preferred persistence backend
-### 4. Set up post-update hook
+### 4. Set up mysql
+
+ 0. ensure you can connect via TCP - localhost:3306 (like, make sure skip-networking is off in my.cnf)
+ 1. connect to the database as user root
+ 2. `CREATE USER 'browserid'@'localhost' IDENTIFIED BY 'browserid';`
+ 3. `CREATE DATABASE browserid;`
+ 4. `GRANT CREATE, DELETE, INDEX, INSERT, LOCK TABLES, SELECT, UPDATE ON browserid.* TO 'browserid'@'localhost';`
+
+### 5. Set up post-update hook
*This step is optional* - if you want to manually update code you
probably skipped step #1, you can skip this one as well. All you need
View
5 LICENSE
@@ -0,0 +1,5 @@
+This software is available under your choice of the following licenses:
+
+ * MPL 1.1 or later: http://www.mozilla.org/MPL/
+ * GPL 2.0 or later: http://www.gnu.org/licenses/gpl.html
+ * LGPL 2.1 or later: http://www.gnu.org/licenses/lgpl.html
View
1 README.md
@@ -18,7 +18,6 @@ Here's the software you'll need installed:
* node.js (>= 0.4.5): http://nodejs.org/
* npm: http://npmjs.org/
-* sqlite (3) development libraries: http://www.sqlite.org/
* Several node.js 3rd party libraries - see `package.json` for details
## Getting started:
View
185 browserid/app.js
@@ -1,28 +1,60 @@
-const
-fs = require('fs'),
-path = require('path');
-
-// create the var directory if it doesn't exist
-var VAR_DIR = path.join(__dirname, "var");
-try { fs.mkdirSync(VAR_DIR, 0755); } catch(e) { };
+/* ***** 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 ***** */
const
+fs = require('fs'),
+path = require('path'),
url = require('url'),
-crypto = require('crypto'),
wsapi = require('./lib/wsapi.js'),
httputils = require('./lib/httputils.js'),
webfinger = require('./lib/webfinger.js'),
-sessions = require('cookie-sessions'),
+sessions = require('connect-cookie-session'),
express = require('express'),
secrets = require('./lib/secrets.js'),
db = require('./lib/db.js'),
configuration = require('../libs/configuration.js'),
substitution = require('../libs/substitute.js');
-logging = require("../libs/logging.js");
+metrics = require("../libs/metrics.js"),
+logger = require("../libs/logging.js").logger;
+
+logger.info("browserid server starting up");
-// looks unused, see run.js
-// const STATIC_DIR = path.join(path.dirname(__dirname), "static");
-const COOKIE_SECRET = secrets.hydrateSecret('cookie_secret', VAR_DIR);
+// open the databse
+db.open(configuration.get('database'));
+
+const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', configuration.get('var_path'));
const COOKIE_KEY = 'browserid_state';
function internal_redirector(new_url) {
@@ -33,7 +65,7 @@ function internal_redirector(new_url) {
}
function router(app) {
- app.set("views", __dirname + '/views');
+ app.set("views", __dirname + '/views');
app.set('view options', {
production: configuration.get('use_minified_resources')
@@ -42,7 +74,7 @@ function router(app) {
// this should probably be an internal redirect
// as soon as relative paths are figured out.
app.get('/sign_in', function(req, res, next ) {
- logging.userEntry('browserid', req);
+ metrics.userEntry(req);
res.render('dialog.ejs', {
title: 'A Better Way to Sign In',
layout: false,
@@ -53,15 +85,6 @@ function router(app) {
// simple redirects (internal for now)
app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html'));
- // return the CSRF token
- // IMPORTANT: this should be safe because it's only readable by same-origin code
- // but we must be careful that this is never a JSON structure that could be hijacked
- // by a third party
- app.get('/csrf', function(req, res) {
- res.write(req.session.csrf);
- res.end();
- });
-
app.get('/', function(req,res) {
res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
});
@@ -83,7 +106,7 @@ function router(app) {
});
app.get(/^\/manage(\.html)?$/, function(req,res) {
- res.render('manage.ejs', {title: 'My Account', fullpage: false, csrf: req.session.csrf});
+ res.render('manage.ejs', {title: 'My Account', fullpage: false});
});
app.get(/^\/tos(\.html)?$/, function(req, res) {
@@ -108,46 +131,96 @@ function router(app) {
});
app.get('/code_update', function(req, resp, next) {
- console.log("code updated. shutting down.");
+ logger.warn("code updated. shutting down.");
process.exit();
});
};
-exports.varDir = VAR_DIR;
-
exports.setup = function(server) {
+ // request to logger, dev formatted which omits personal data in the requests
+ server.use(express.logger({
+ format: 'dev',
+ stream: {
+ write: function(x) {
+ logger.info(typeof x === 'string' ? x.trim() : x);
+ }
+ }
+ }));
+
+ // over SSL?
+ var overSSL = (configuration.get('scheme') == 'https');
+
server.use(express.cookieParser());
var cookieSessionMiddleware = sessions({
secret: COOKIE_SECRET,
- session_key: COOKIE_KEY,
- path: '/'
+ 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: (7 * 24 * 60 * 60 * 1000),
+ secure: overSSL
+ }
});
+ // cookie sessions
server.use(function(req, resp, next) {
- try {
- cookieSessionMiddleware(req, resp, next);
- } catch(e) {
- console.log("invalid cookie found: ignoring");
- delete req.cookies[COOKIE_KEY];
- cookieSessionMiddleware(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)) {
+ // 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();
}
});
server.use(express.bodyParser());
- // we make sure that everyone has a session, otherwise we can't do CSRF properly
+ // Check CSRF token early. POST requests are only allowed to
+ // /wsapi and they always must have a valid csrf token
server.use(function(req, resp, next) {
- if (typeof req.session == 'undefined')
- req.session = {};
+ // 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 + "'");
+ }
- 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');
- }
+ 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
+ if (typeof req.session.csrf !== 'string') {
+ denied = true;
+ logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set");
+ }
- next();
+ // and the token must match what is sent in the post body
+ 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();
});
// a tweak to get the content type of host-meta correct
@@ -158,22 +231,18 @@ exports.setup = function(server) {
next();
});
- // prevent framing
- server.use(function(req, resp, next) {
- resp.setHeader('x-frame-options', 'DENY');
- next();
- });
-
- // check CSRF token
+ // Strict Transport Security
server.use(function(req, resp, next) {
- // only on POSTs
- if (req.method == "POST") {
- if (req.body.csrf != req.session.csrf) {
- // error, problem with CSRF
- throw new Error("CSRF violation - " + req.body.csrf + '/' + req.session.csrf);
+ if (overSSL) {
+ // expires in 30 days, include subdomains like www
+ resp.setHeader("Strict-Transport-Security", "max-age=2592000; includeSubdomains");
}
- }
+ next();
+ });
+ // prevent framing
+ server.use(function(req, resp, next) {
+ resp.setHeader('x-frame-options', 'DENY');
next();
});
View
460 browserid/lib/db.js
@@ -1,391 +1,113 @@
-const
-sqlite = require('sqlite'),
-path = require('path');
-
-var VAR_DIR = path.join(path.dirname(__dirname), "var");
-
-var db = new sqlite.Database();
-
-// a configurable parameter if set immediately after require() of db.js
-exports.dbPath = path.join(VAR_DIR, "authdb.sqlite");
+/* ***** 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 ***** */
+
+var logger = require('../../libs/logging.js').logger;
+
+var driver;
var ready = false;
var waiting = [];
+function checkReady() {
+ if (!ready) throw "database not ready. did you call open()?";
+}
+
// async break allow database path to be configured by calling code
// a touch tricky cause client must set dbPath before releasing
// control of the runloop
-setTimeout(function() {
- db.open(exports.dbPath, function (error) {
- if (error) {
- console.log("Couldn't open database: " + error);
- throw error;
- }
- db.executeScript(
- "CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, password TEXT );" +
- "CREATE TABLE IF NOT EXISTS emails ( id INTEGER PRIMARY KEY, user INTEGER, address TEXT UNIQUE );" +
- "CREATE TABLE IF NOT EXISTS keys ( id INTEGER PRIMARY KEY, email INTEGER, key TEXT, expires INTEGER )",
- function (error) {
- if (error) {
- throw error;
- }
- ready = true;
- waiting.forEach(function(f) { f() });
- waiting = [];
- });
- });
-}, 0);
-
-// accepts a function that will be invoked once the database is ready for transactions.
-// this hook is important to pause the rest of application startup until async database
-// connection establishment is complete.
-exports.onReady = function(f) {
- setTimeout(function() {
- if (ready) f();
- else waiting.push(f);
- }, 0);
-};
-
-// XXX: g_staged and g_stagedEmails should be moved into persistent/fast storage.
-
-// half created user accounts (pending email verification)
-// OR
-// half added emails (pending verification)
-var g_staged = {
-};
-
-// an email to secret map for efficient fulfillment of isStaged queries
-var g_stagedEmails = {
-};
-
-function executeTransaction(statements, cb) {
- function executeTransaction2(statements, cb) {
- if (statements.length == 0) cb();
- else {
- var s = statements.shift();
- db.execute(s[0], s[1], function(err, rows) {
- if (err) cb(err);
- else executeTransaction2(statements, cb);
- });
- }
+exports.open = function(cfg, cb) {
+ var driverName = "json";
+ if (cfg && cfg.driver) driverName = cfg.driver;
+ try {
+ driver = require('./db_' + driverName + '.js');
+ } catch(e) {
+ var msg = "FATAL: couldn't find database driver: " + driverName;
+ console.log(msg);
+ throw msg + ": " + e.toString();
}
- db.execute('BEGIN', function(err, rows) {
- executeTransaction2(statements, function(err) {
- if (err) cb(err);
- else db.execute('COMMIT', function(err, rows) {
- cb(err);
- });
- });
- });
-}
-
-function emailToUserID(email, cb) {
- db.execute(
- 'SELECT users.id FROM emails, users WHERE emails.address = ? AND users.id == emails.user',
- [ email ],
- function (err, rows) {
- if (rows && rows.length == 1) {
- cb(rows[0].id);
- } else {
- if (err) console.log("database error: " + err);
- cb(undefined);
+ driver.open(cfg, function(error) {
+ if (error) {
+ if (cb) cb(error);
+ else {
+ logger.error(error);
+ process.exit(1);
}
- });
-}
-
-exports.emailKnown = function(email, cb) {
- db.execute(
- "SELECT id FROM emails WHERE address = ?",
- [ email ],
- function(error, rows) {
- cb(rows.length > 0);
- });
-};
-
-// XXX: should be moved to async.
-exports.isStaged = function(email) {
- return g_stagedEmails.hasOwnProperty(email);
-};
-
-function generateSecret() {
- var str = "";
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- for (var i=0; i < 48; i++) {
- str += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
- }
- return str;
-}
-
-function addEmailToAccount(existing_email, email, pubkey, cb) {
- emailToUserID(existing_email, function(userID) {
- if (userID == undefined) {
- cb("no such email: " + existing_email, undefined);
} else {
- executeTransaction([
- [ "INSERT INTO emails (user, address) VALUES(?,?)", [ userID, email ] ],
- [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)",
- [ pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ]
- ]
- ], function (error) {
- if (error) cb(error);
- else cb();
- });
+ ready = true;
+ waiting.forEach(function(f) { f() });
+ waiting = [];
+ if (cb) cb();
}
});
-}
-
-exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
- emailToUserID(lhs, function(lhs_uid) {
- emailToUserID(rhs, function(rhs_uid) {
- cb(lhs_uid === rhs_uid);
- }, function (error) {
- cb(false);
- });
- }, function (error) {
- cb(false);
- });
};
-exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
- emailToUserID(existing_email, function(userID) {
- if (userID == undefined) {
- cb("no such email: " + existing_email, undefined);
- return;
- }
- db.execute("SELECT emails.id FROM emails,users WHERE users.id = ? AND emails.address = ? AND emails.user = users.id",
- [ userID, email ],
- function(err, rows) {
- if (err || rows.length != 1) {
- cb(err);
- return;
- }
- executeTransaction([
- [ "INSERT INTO keys (email, key, expires) VALUES(?,?,?)",
- [ rows[0].id, pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ]
- ]
- ], function (error) {
- if (error) cb(error);
- else cb();
- });
- });
+exports.close = function(cb) {
+ driver.close(function(err) {
+ ready = false;
+ cb(err);
});
-}
-
-/* takes an argument object including email, password hash, and pubkey. */
-exports.stageUser = function(obj) {
- var secret = generateSecret();
-
- // overwrite previously staged users
- g_staged[secret] = {
- type: "add_account",
- email: obj.email,
- pubkey: obj.pubkey,
- pass: obj.hash
- };
-
- g_stagedEmails[obj.email] = secret;
- return secret;
};
-/* takes an argument object including email, pass, and pubkey. */
-// XXX: change to async
-exports.stageEmail = function(existing_email, new_email, pubkey) {
- var secret = generateSecret();
- // overwrite previously staged users
- g_staged[secret] = {
- type: "add_email",
- existing_email: existing_email,
- email: new_email,
- pubkey: pubkey
- };
- g_stagedEmails[new_email] = secret;
- return secret;
-};
-
-/* invoked when a user clicks on a verification URL in their email */
-exports.gotVerificationSecret = function(secret, cb) {
- if (!g_staged.hasOwnProperty(secret)) return cb("unknown secret");
-
- // simply move from staged over to the emails "database"
- var o = g_staged[secret];
- delete g_staged[secret];
- delete g_stagedEmails[o.email];
- if (o.type === 'add_account') {
- exports.emailKnown(o.email, function(known) {
- function createAccount() {
- executeTransaction([
- [ "INSERT INTO users (password) VALUES(?)", [ o.pass ] ] ,
- [ "INSERT INTO emails (user, address) VALUES(last_insert_rowid(),?)", [ o.email ] ],
- [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)",
- [ o.pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ]
- ]
- ], function (error) {
- if (error) cb(error);
- else cb();
- });
- }
-
- // if this email address is known and a user has completed a re-verification of this email
- // address, remove the email from the old account that it was associated with, and then
- // create a brand new account with only this email.
- // NOTE: this might be sub-optimal, but it's a dead simple approach that mitigates many attacks
- // and gives us reasonable behavior (without explicitly supporting) in the face of shared email
- // addresses.
- if (known) {
- exports.removeEmail(o.email, o.email, function (err) {
- if (err) cb(err);
- else createAccount();
- });
- } else {
- createAccount();
- }
- });
- } else if (o.type === 'add_email') {
- exports.emailKnown(o.email, function(known) {
- function addIt() {
- addEmailToAccount(o.existing_email, o.email, o.pubkey, cb);
- }
- if (known) {
- exports.removeEmail(o.email, o.email, function (err) {
- if (err) cb(err);
- else addIt();
- });
- } else {
- addIt();
- }
- });
- } else {
- cb("internal error");
- }
-};
-
-// check authentication credentials for a given email address. This will invoke the
-// users callback with the authentication (password/hash/whatever - the database layer
-// doesn't care). callback will be passed undefined if email cannot be found
-exports.checkAuth = function(email, cb) {
- db.execute("SELECT users.password FROM emails, users WHERE users.id = emails.user AND emails.address = ?",
- [ email ],
- function (error, rows) {
- cb(rows.length !== 1 ? undefined : rows[0].password);
- });
+// accepts a function that will be invoked once the database is ready for transactions.
+// this hook is important to pause the rest of application startup until async database
+// connection establishment is complete.
+exports.onReady = function(f) {
+ setTimeout(function() {
+ if (ready) f();
+ else waiting.push(f);
+ }, 0);
};
-function emailHasPubkey(email, pubkey, cb) {
- db.execute(
- 'SELECT keys.key FROM keys, emails WHERE emails.address = ? AND keys.email = emails.id AND keys.key = ?',
- [ email, pubkey ],
- function(err, rows) {
- cb(rows.length === 1);
- });
-}
-
-/* a high level operation that attempts to sync a client's view with that of the
- * server. email is the identity of the authenticated channel with the user,
- * identities is a map of email -> pubkey.
- * We'll return an object that expresses three different types of information:
- * there are several things we need to express:
- * 1. emails that the client knows about but we do not
- * 2. emails that we know about and the client does not
- * 3. emails that we both know about but who need to be re-keyed
- * NOTE: it's not neccesary to differentiate between #2 and #3, as the client action
- * is the same (regen keypair and tell us about it).
- */
-exports.getSyncResponse = function(email, identities, cb) {
- var respBody = {
- unknown_emails: [ ],
- key_refresh: [ ]
+[
+ 'emailKnown',
+ 'isStaged',
+ 'emailsBelongToSameAccount',
+ 'addKeyToEmail',
+ 'stageUser',
+ 'stageEmail',
+ 'gotVerificationSecret',
+ 'checkAuth',
+ 'getSyncResponse',
+ 'pubkeysForEmail',
+ 'removeEmail',
+ 'cancelAccount'
+].forEach(function(fn) {
+ exports[fn] = function() {
+ checkReady();
+ driver[fn].apply(undefined, arguments);
};
-
- // get the user id associated with this account
- emailToUserID(email, function(userID) {
- if (userID === undefined) {
- cb("no such email: " + email);
- return;
- }
- db.execute(
- 'SELECT address FROM emails WHERE ? = user',
- [ userID ],
- function (err, rows) {
- if (err) cb(err);
- else {
- var emails = [ ];
- var keysToCheck = [ ];
- for (var i = 0; i < rows.length; i++) emails.push(rows[i].address);
-
- // #1
- for (var e in identities) {
- if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e);
- else keysToCheck.push(e);
- }
-
- // #2
- for (var e in emails) {
- e = emails[e];
- if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e);
-
- }
-
- // #3 -- yes, this is sub-optimal in terms of performance. when we
- // move away from public keys this will be unnec.
- if (keysToCheck.length) {
- var checked = 0;
- keysToCheck.forEach(function(e) {
- emailHasPubkey(e, identities[e], function(v) {
- checked++;
- if (!v) respBody.key_refresh.push(e);
- if (checked === keysToCheck.length) {
- cb(undefined, respBody);
- }
- });
- });
- } else {
- cb(undefined, respBody);
- }
- }
- });
- });
-};
-
-// get all public keys associated with an email address
-exports.pubkeysForEmail = function(identity, cb) {
- db.execute(
- 'SELECT keys.key FROM keys, emails WHERE emails.address = ? AND keys.email = emails.id',
- [ identity ],
- function(err, rows) {
- var keys = undefined;
- if (!err && rows && rows.length) {
- keys = [ ];
- for (var i = 0; i < rows.length; i++) keys.push(rows[i].key);
- }
- cb(keys);
- });
-};
-
-exports.removeEmail = function(authenticated_email, email, cb) {
- // figure out the user, and remove Email only from addressed
- // linked to the authenticated email address
- emailToUserID(authenticated_email, function(user_id) {
- executeTransaction([
- [ "delete from emails where emails.address = ? and user = ?", [ email,user_id ] ] ,
- [ "delete from keys where email in (select address from emails where emails.address = ? and user = ?)", [ email,user_id ] ],
- ], function (error) {
- if (error) cb(error);
- else cb();
- });
- });
-};
-
-exports.cancelAccount = function(authenticated_email, cb) {
- emailToUserID(authenticated_email, function(user_id) {
- executeTransaction([
- [ "delete from emails where user = ?", [ user_id ] ] ,
- [ "delete from keys where email in (select address from emails where user = ?)", [ user_id ] ],
- [ "delete from users where id = ?", [ user_id ] ],
- ], function (error) {
- if (error) cb(error);
- else cb();
- });
- });
-};
+});
View
381 browserid/lib/db_json.js
@@ -0,0 +1,381 @@
+/* ***** 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 ***** */
+
+/* db_json is a json database driver. It is designed for use in
+ * local development, is intended to be extremely easy to maintain,
+ * have minimal dependencies on 3rd party libraries, and we could
+ * care less if it performs well with more than 10 or so users.
+ */
+const
+path = require('path'),
+fs = require('fs'),
+secrets = require('./secrets'),
+jsel = require('JSONSelect'),
+logger = require('../../libs/logging.js').logger,
+configuration = require('../../libs/configuration.js'),
+temp = require('temp');
+
+// a little alias for stringify
+const ESC = JSON.stringify;
+
+var dbPath = path.join(configuration.get('var_path'), "authdb.json");
+
+/* The JSON database. The structure is thus:
+ * [
+ * {
+ * password: "somepass",
+ * emails: [
+ * {
+ * address: "lloyd@hilaiel.com",
+ * keys: [
+ * {
+ * key: "SOMESTRINGOFTEXT",
+ * expires: 1231541615125
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * ]
+ */
+
+var db = [];
+var stagedEmails = { };
+var staged = { };
+
+function flush() {
+ try {
+ var e = fs.writeFileSync(dbPath, JSON.stringify(db));
+ } catch (e) {
+ logger.error("Cannot save database to " + dbPath);
+ }
+}
+
+// when should a key created right now expire?
+function getExpiryTime() {
+ return ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000));
+}
+
+// when unit_test is set in configuration, database should be
+// ephemeral. which simply means we use a temp file and delete
+// on close;
+var delete_on_close = false;
+
+exports.open = function(cfg, cb) {
+ delete_on_close = false;
+
+ if (cfg) {
+ if (cfg.unit_test) {
+ dbPath = temp.path({suffix: '.db'});
+ delete_on_close = true;
+ } else if (cfg.path) {
+ dbPath = cfg.path;
+ }
+ }
+
+ try {
+ db = JSON.parse(fs.readFileSync(dbPath));
+ } catch(e) {
+ }
+
+ setTimeout(cb, 0);
+};
+
+exports.close = function(cb) {
+ flush();
+ setTimeout(cb, 0);
+ if (delete_on_close) {
+ delete_on_close = false;
+ fs.unlink(dbPath, function(err) { });
+ };
+};
+
+exports.emailKnown = function(email, cb) {
+ var m = jsel.match(".address:val(" + ESC(email) + ")", db);
+ setTimeout(function() { cb(m.length > 0) }, 0);
+};
+
+exports.isStaged = function(email, cb) {
+ if (cb) {
+ setTimeout(function() {
+ cb(stagedEmails.hasOwnProperty(email));
+ }, 0);
+ }
+};
+
+exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
+ emailToUserID(lhs, function(lhs_uid) {
+ emailToUserID(rhs, function(rhs_uid) {
+ cb(lhs_uid === rhs_uid);
+ }, function (error) {
+ cb(false);
+ });
+ }, function (error) {
+ cb(false);
+ });
+};
+
+function addEmailToAccount(existing_email, email, pubkey, cb) {
+ emailToUserID(existing_email, function(userID) {
+ if (userID == undefined) {
+ cb("no such email: " + existing_email, undefined);
+ } else {
+ db[userID].emails.push({
+ address: email,
+ keys: [
+ {
+ key: pubkey,
+ expires: getExpiryTime()
+ }
+ ]
+ });
+ flush();
+ cb();
+ }
+ });
+}
+
+exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
+ emailToUserID(existing_email, function(userID) {
+ if (userID == undefined) {
+ cb("no such email: " + existing_email, undefined);
+ return;
+ }
+
+ if (!(db[userID].emails)) {
+ db[userID].emails = [ ];
+ }
+
+ var m = jsel.match("object:has(.address:val(" + ESC(email) + ")) > .keys", db[userID].emails);
+
+ var kobj = {
+ key: pubkey,
+ expires: getExpiryTime()
+ };
+
+ if (m.length) {
+ m[0].push(kobj);
+ } else {
+ db[userID].emails.push({
+ address: email,
+ keys: [ kobj ]
+ });
+ }
+
+ flush();
+ if (cb) setTimeout(function() { cb(); }, 0);
+ });
+}
+
+exports.stageUser = function(obj, cb) {
+ var secret = secrets.generate(48);
+
+ // overwrite previously staged users
+ staged[secret] = {
+ type: "add_account",
+ email: obj.email,
+ pubkey: obj.pubkey,
+ pass: obj.hash
+ };
+
+ stagedEmails[obj.email] = secret;
+ setTimeout(function() { cb(secret); }, 0);
+};
+
+exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
+ var secret = secrets.generate(48);
+ // overwrite previously staged users
+ staged[secret] = {
+ type: "add_email",
+ existing_email: existing_email,
+ email: new_email,
+ pubkey: pubkey
+ };
+ stagedEmails[new_email] = secret;
+ setTimeout(function() { cb(secret); }, 0);
+};
+
+exports.gotVerificationSecret = function(secret, cb) {
+ if (!staged.hasOwnProperty(secret)) return cb("unknown secret");
+
+ // simply move from staged over to the emails "database"
+ var o = staged[secret];
+ delete staged[secret];
+ delete stagedEmails[o.email];
+ if (o.type === 'add_account') {
+ exports.emailKnown(o.email, function(known) {
+ function createAccount() {
+ db.push({
+ password: o.pass,
+ emails: [
+ {
+ address: o.email,
+ keys: [ {
+ key: o.pubkey,
+ expires: getExpiryTime(),
+ } ]
+ }
+ ]
+ });
+ flush();
+ cb();
+ }
+
+ // if this email address is known and a user has completed a re-verification of this email
+ // address, remove the email from the old account that it was associated with, and then
+ // create a brand new account with only this email.
+ // NOTE: this might be sub-optimal, but it's a dead simple approach that mitigates many attacks
+ // and gives us reasonable behavior (without explicitly supporting) in the face of shared email
+ // addresses.
+
+ if (known) {
+ exports.removeEmail(o.email, o.email, function (err) {
+ if (err) cb(err);
+ else createAccount();
+ });
+ } else {
+ createAccount();
+ }
+ });
+ } else if (o.type === 'add_email') {
+ exports.emailKnown(o.email, function(known) {
+ function addIt() {
+ addEmailToAccount(o.existing_email, o.email, o.pubkey, cb);
+ }
+ if (known) {
+ exports.removeEmail(o.email, o.email, function (err) {
+ if (err) cb(err);
+ else addIt();
+ });
+ } else {
+ addIt();
+ }
+ });
+ } else {
+ cb("internal error");
+ }
+};
+
+exports.checkAuth = function(email, cb) {
+ var m = jsel.match(":root > object:has(.address:val(" + ESC(email) + ")) > .password", db);
+ if (m.length === 0) m = undefined;
+ else m = m[0];
+ setTimeout(function() { cb(m) }, 0);
+};
+
+function emailToUserID(email, cb) {
+ var id = undefined;
+
+ for (var i = 0; i < db.length; i++) {
+ if (jsel.match(".address:val(" + JSON.stringify(email) + ")", db[i]).length) {
+ id = i;
+ break;
+ }
+ if (id !== undefined) break;
+ }
+
+ setTimeout(function() { cb(id); }, 0);
+}
+
+exports.getSyncResponse = function(email, identities, cb) {
+ var respBody = {
+ unknown_emails: [ ],
+ key_refresh: [ ]
+ };
+
+ // get the user id associated with this account
+ emailToUserID(email, function(userID) {
+ if (userID === undefined) {
+ cb("no such email: " + email);
+ return;
+ }
+ var emails = jsel.match(".address", db[userID]);
+ var keysToCheck = [ ];
+
+ // #1 emails that the client knows about but we do not
+ for (var e in identities) {
+ if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e);
+ else keysToCheck.push(e);
+ }
+
+ // #2 emails that we know about and the client does not
+ for (var e in emails) {
+ e = emails[e];
+ if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e);
+ }
+
+ // #3 emails that we both know about but who need to be re-keyed
+ if (keysToCheck.length) {
+ var checked = 0;
+ keysToCheck.forEach(function(e) {
+ if (!jsel.match(".key:val(" + ESC(identities[e]) + ")", db[userID]).length)
+ respBody.key_refresh.push(e);
+ checked++;
+ if (checked === keysToCheck.length) cb(undefined, respBody);
+ });
+ } else {
+ cb(undefined, respBody);
+ }
+ });
+};
+
+exports.pubkeysForEmail = function(identity, cb) {
+ var m = jsel.match(".emails object:has(.address:val(" + ESC(identity)+ ")) .key", db);
+ setTimeout(function() { cb(m); }, 0);
+};
+
+exports.removeEmail = function(authenticated_email, email, cb) {
+ var m = jsel.match(":root > object:has(.address:val("+ESC(authenticated_email)+")):has(.address:val("+ESC(email)+")) .emails", db);
+
+ if (m.length) {
+ var emails = m[0];
+ for (var i = 0; i < emails.length; i++) {
+ if (emails[i].address === email) {
+ emails.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ setTimeout(function() { cb(); }, 0);
+};
+
+exports.cancelAccount = function(authenticated_email, cb) {
+ emailToUserID(authenticated_email, function(user_id) {
+ db.splice(user_id, 1);
+ flush();
+ cb();
+ });
+};
View
471 browserid/lib/db_mysql.js
@@ -0,0 +1,471 @@
+/* ***** 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 ***** */
+
+/* This is a mysql driver for the browserid server. It maps the data
+ * storage requirements of browserid onto a relational schema. This
+ * driver is intended to be fast and scalable.
+ */
+
+/*
+ * The Schema:
+ *
+ * +--- user ------+ +--- email ----+ +--- pubkey -----+
+ * |*int id | <-\ |*int id | <-\ |*int id |
+ * | string passwd | \- |*int user | \-- |*int email |
+ * +---------------+ |*string address | string pubkey |
+ * +--------------+ | int expires |
+ * +----------------+
+ *
+ *
+ * +------ staged ----------+
+ * |*string secret |
+ * | bool new_acct |
+ * | string existing |
+ * |*string email |
+ * | string pubkey |
+ * | string passwd |
+ * | timestamp ts |
+ * +------------------------+
+ */
+
+const
+mysql = require('mysql'),
+secrets = require('./secrets'),
+logger = require('../../libs/logging.js').logger;
+
+var client = undefined;
+
+// may get defined at open() time causing a database to be dropped upon connection closing.
+var drop_on_close = undefined;
+
+const schemas = [
+ "CREATE TABLE IF NOT EXISTS user ( id INTEGER AUTO_INCREMENT PRIMARY KEY, passwd VARCHAR(64) );",
+ "CREATE TABLE IF NOT EXISTS email ( id INTEGER AUTO_INCREMENT PRIMARY KEY, user INTEGER, address VARCHAR(255) UNIQUE, INDEX(address) );",
+ "CREATE TABLE IF NOT EXISTS pubkey ( id INTEGER AUTO_INCREMENT PRIMARY KEY, email INTEGER, content TEXT, expiry DATETIME );",
+ "CREATE TABLE IF NOT EXISTS staged ( secret VARCHAR(48) PRIMARY KEY, new_acct BOOL, existing VARCHAR(255), email VARCHAR(255) UNIQUE, INDEX(email), pubkey TEXT, passwd VARCHAR(64), ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
+];
+
+// log an unexpected database error
+function logUnexpectedError(detail) {
+ // first, get line number of callee
+ var where;
+ try { dne; } catch (e) { where = e.stack.split('\n')[2].trim(); };
+ // now log it!
+ logger.error("unexpected database failure: " + detail + " -- " + where);
+}
+
+// open & create the mysql database
+exports.open = function(cfg, cb) {
+ if (client) throw "database is already open!";
+ // mysql config requires
+ var options = {
+ host: '127.0.0.1',
+ port: "3306",
+ user: undefined,
+ password: undefined,
+ unit_test: false
+ };
+
+ Object.keys(options).forEach(function(param) {
+ options[param] = (cfg[param] !== undefined ? cfg[param] : options[param]);
+ if (options[param] === undefined) delete options[param];
+ });
+
+ // create the client
+ client = mysql.createClient(options);
+
+ // let's figure out the database name
+ var database = cfg.database;
+ if (!database) database = "browserid";
+ if (cfg.unit_test) {
+ database += "_" + secrets.generate(8);
+ drop_on_close = database;
+ }
+
+ // now create the databse
+ client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) {
+ if (err) {
+ logUnexpectedError(err);
+ cb(err);
+ return;
+ }
+ client.useDatabase(database, function(err) {
+ if (err) {
+ logUnexpectedError(err);
+ cb(err);
+ return;
+ }
+
+ // now create tables
+ function createNextTable(i) {
+ if (i < schemas.length) {
+ client.query(schemas[i], function(err) {
+ if (err) {
+ logUnexpectedError(err);
+ cb(err);
+ } else {
+ createNextTable(i+1);
+ }
+ });
+ } else {
+ cb();
+ }
+ }
+ createNextTable(0);
+ });
+ });
+};
+
+exports.close = function(cb) {
+ function endConn() {
+ client.end(function(err) {
+ client = undefined;
+ if (err) logUnexpectedError(err);
+ if (cb) cb(err);
+ });
+ }
+ // when unit_test is specified at open time, we use a temporary database,
+ // and clean it up upon close.
+ if (drop_on_close) {
+ client.query("DROP DATABASE " + drop_on_close, function() {
+ endConn();
+ });
+ } else {
+ endConn();
+ }
+};
+
+exports.emailKnown = function(email, cb) {
+ client.query(
+ "SELECT COUNT(*) as N FROM email WHERE address = ?", [ email ],
+ function(err, rows) {
+ if (err) logUnexpectedError(err);
+ cb(rows && rows.length > 0 && rows[0].N > 0);
+ }
+ );
+}
+
+exports.isStaged = function(email, cb) {
+ client.query(
+ "SELECT COUNT(*) as N FROM staged WHERE email = ?", [ email ],
+ function(err, rows) {
+ if (err) logUnexpectedError(err);
+ cb(rows && rows.length > 0 && rows[0].N > 0);
+ }
+ );
+}
+
+exports.stageUser = function(obj, cb) {
+ var secret = secrets.generate(48);
+ // overwrite previously staged users
+ client.query('INSERT INTO staged (secret, new_acct, email, pubkey, passwd) VALUES(?,TRUE,?,?,?) ' +
+ 'ON DUPLICATE KEY UPDATE secret=?, existing="", new_acct=TRUE, pubkey=?, passwd=?',
+ [ secret, obj.email, obj.pubkey, obj.hash, secret, obj.pubkey, obj.hash],
+ function(err) {
+ if (err) {
+ logUnexpectedError(err);
+ cb(undefined, err);
+ } else cb(secret);
+ });
+}
+
+exports.gotVerificationSecret = function(secret, cb) {
+ client.query(
+ "SELECT * FROM staged WHERE secret = ?", [ secret ],
+ function(err, rows) {
+ if (err) {
+ logUnexpectedError(err);
+ cb(err);
+ } else if (rows.length === 0) cb("unknown secret");
+ else {
+ var o = rows[0];
+
+ function addEmailAndPubkey(userID) {
+ // issue #170 - delete any old records with the same
+ // email address. this is necessary because
+ // gotVerificationSecret is invoked both for
+ // forgotten password flows and for new user signups.
+ // We could add an `ON DUPLICATE KEY` clause, however
+ // We actually want to invalidate all old public keys.
+ //
+ // XXX: periodic database cleanup should remove old expired
+ // keys, but this is moot once we move to certs as the
+ // server won't know about old keys
+ client.query(
+ "DELETE FROM email WHERE address = ?",
+ [ o.email ],
+ function(err, info) {
+ if (err) { logUnexpectedError(err); cb(err); return; }
+ else {
+ client.query(
+ "INSERT INTO email(user, address) VALUES(?, ?)",
+ [ userID, o.email ],
+ function(err, info) {
+ if (err) { logUnexpectedError(err); cb(err); return; }
+ addKeyToEmailRecord(info.insertId, o.pubkey, cb);
+ });
+ }
+ });
+ }
+
+ // delete the record
+ client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]);
+
+ if (o.new_acct) {
+ // we're creating a new account, add appropriate entries into user, email, and pubkey.
+ client.query(
+ "INSERT INTO user(passwd) VALUES(?)",
+ [ o.passwd ],
+ function(err, info) {
+ if (err) { logUnexpectedError(err); cb(err); return; }
+ addEmailAndPubkey(info.insertId);
+ });
+ } else {
+ // we're adding an email address to an existing user account. add appropriate entries into email and
+ // pubkey
+ client.query(
+ "SELECT user FROM email WHERE address = ?", [ o.existing ],
+ function(err, rows) {
+ if (err) { logUnexpectedError(err); cb(err); }
+ else if (rows.length === 0) cb("cannot find email address: " + o.existing);
+ else {
+ addEmailAndPubkey(rows[0].user);
+ }
+ });
+ }
+ }
+ }
+ );
+}
+
+exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
+ client.query(
+ 'SELECT COUNT(*) AS n FROM email WHERE address = ? AND user = ( SELECT user FROM email WHERE address = ? );',
+ [ lhs, rhs ],
+ function (err, rows) {
+ if (err) cb(false);
+ else cb(rows.length === 1 && rows[0].n === 1);
+ });
+}
+
+function addKeyToEmailRecord(emailId, pubkey, cb) {
+ client.query(
+ // XXX: 2 weeks is wrong, but then so is keypairs.
+ "INSERT INTO pubkey(email, content, expiry) VALUES(?, ?, DATE_ADD(NOW(), INTERVAL 2 WEEK))",
+ [ emailId, pubkey ],
+ function(err, info) {
+ if (err) logUnexpectedError(err);
+ // smash null into undefined.
+ cb(err ? err : undefined);
+ });
+}
+
+exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
+ // this function will NOT add a new email address to a user record. The only
+ // way that happens is when a verification secret is provided to us. Limiting
+ // the code paths that result in us concluding that a user owns an email address
+ // is a Good Thing.
+ exports.emailsBelongToSameAccount(existing_email, email, function(ok) {
+ if (!ok) {
+ cb("authenticated user doesn't have permission to add a public key to " + email);
+ return;
+ }
+
+ // now we know that the user has permission to add a key.
+ client.query(
+ "SELECT id FROM email WHERE address = ?", [ email ],
+ function(err, rows) {
+ if (err) { logUnexpectedError(err); cb(err); }
+ else if (rows.length === 0) cb("cannot find email address: " + email);
+ else {
+ addKeyToEmailRecord(rows[0].id, pubkey, cb);
+ }
+ });
+ });
+}
+
+exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
+ var secret = secrets.generate(48);
+ // overwrite previously staged users
+ client.query('INSERT INTO staged (secret, new_acct, existing, email, pubkey) VALUES(?,FALSE,?,?,?) ' +
+ 'ON DUPLICATE KEY UPDATE secret=?, existing=?, new_acct=FALSE, pubkey=?, passwd=""',
+ [ secret, existing_email, new_email, pubkey, secret, existing_email, pubkey],
+ function(err) {
+ if (err) {
+ logUnexpectedError(err);
+ cb(undefined, err);
+ }
+ else cb(secret);
+ });
+}
+
+exports.checkAuth = function(email, cb) {
+ client.query(
+ 'SELECT passwd FROM user WHERE id = ( SELECT user FROM email WHERE address = ? )',
+ [ email ],
+ function (err, rows) {
+ if (err) logUnexpectedError(err);
+ cb((rows && rows.length == 1) ? rows[0].passwd : undefined);
+ });
+}
+
+function emailHasPubkey(email, pubkey, cb) {
+ client.query(
+ 'SELECT pubkey.content FROM pubkey, email WHERE email.address = ? AND pubkey.email = email.id AND pubkey.content = ?',
+ [ email, pubkey ],
+ function(err, rows) {
+ if (err) logUnexpectedError(err);
+ cb(rows && rows.length === 1);
+ });
+}
+
+/* a high level operation that attempts to sync a client's view with that of the
+ * server. email is the identity of the authenticated channel with the user,
+ * identities is a map of email -> pubkey.
+ * We'll return an object that expresses three different types of information:
+ * there are several things we need to express:
+ * 1. emails that the client knows about but we do not
+ * 2. emails that we know about and the client does not
+ * 3. emails that we both know about but who need to be re-keyed
+ * NOTE: it's not neccesary to differentiate between #2 and #3, as the client action
+ * is the same (regen keypair and tell us about it).
+ */
+exports.getSyncResponse = function(email, identities, cb) {
+ var respBody = {
+ unknown_emails: [ ],
+ key_refresh: [ ]
+ };
+
+ client.query(
+ 'SELECT address FROM email WHERE user = ( SELECT user FROM email WHERE address = ? ) ',
+ [ email ],
+ function (err, rows) {
+ if (err) cb(err);
+ else {
+ var emails = [ ];
+ var keysToCheck = [ ];
+ for (var i = 0; i < rows.length; i++) emails.push(rows[i].address);
+
+ // #1
+ for (var e in identities) {
+ if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e);
+ else keysToCheck.push(e);
+ }
+
+ // #2
+ for (var e in emails) {
+ e = emails[e];
+ if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e);
+ }
+
+ // #3 -- yes, this is sub-optimal in terms of performance. when we
+ // move away from public keys this will be unnec.
+ if (keysToCheck.length) {
+ var checked = 0;
+ keysToCheck.forEach(function(e) {
+ emailHasPubkey(e, identities[e], function(v) {
+ checked++;
+ if (!v) respBody.key_refresh.push(e);
+ if (checked === keysToCheck.length) {
+ cb(undefined, respBody);
+ }
+ });
+ });
+ } else {
+ cb(undefined, respBody);
+ }
+ }
+ });
+};
+
+
+exports.pubkeysForEmail = function(email, cb) {
+ client.query(
+ 'SELECT content FROM pubkey WHERE email = (SELECT id FROM email WHERE address = ?)',
+ [ email ],
+ function (err, rows) {
+ var ar = [ ];
+ if (!err) rows.forEach(function(r) { ar.push(r.content); });
+ else logUnexpectedError(err);
+ cb(ar);
+ });
+}
+
+exports.removeEmail = function(authenticated_email, email, cb) {
+ exports.emailsBelongToSameAccount(authenticated_email, email, function(ok) {
+ if (!ok) {
+ logger.warn(authenticated_email + ' attempted to delete an email that doesn\'t belong to her: ' + email);
+ cb("authenticated user doesn't have permission to remove specified email " + email);
+ return;
+ }
+
+ client.query(
+ 'DELETE FROM pubkey WHERE email = ( SELECT id FROM email WHERE address = ? )',
+ [ email ],
+ function (err, info) {
+ if (err) {
+ logUnexpectedError(err);
+ cb(err);
+ } else {
+ client.query(
+ 'DELETE FROM email WHERE address = ?',
+ [ email ],
+ function(err, info) {
+ if (err) logUnexpectedError(err);
+ // smash null into undefined
+ cb(err ? err : undefined);
+ });
+ }
+ });
+ });
+}
+
+exports.cancelAccount = function(email, cb) {
+ function reportErr(err) { if (err) logUnexpectedError(err); }
+ client.query(
+ "SELECT user FROM email WHERE address = ?", [ email ],
+ function (err, rows) {
+ if (err) {
+ logUnexpectedError(err)
+ cb(err);
+ return
+ }
+ var uid = rows[0].user;
+ client.query("DELETE LOW_PRIORITY FROM pubkey WHERE email in ( SELECT id FROM email WHERE user = ? )", [ uid ], reportErr);
+ client.query("DELETE LOW_PRIORITY FROM email WHERE user = ?", [ uid ], reportErr);
+ client.query("DELETE LOW_PRIORITY FROM user WHERE id = ?", [ uid ], reportErr);
+ cb();
+ });
+}
View
84 browserid/lib/email.js
@@ -1,25 +1,85 @@
+/* ***** 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 ***** */
+
const
db = require('./db'),
emailer = require('nodemailer'),
fs = require('fs'),
path = require('path'),
mustache = require('mustache'),
-config = require('../../libs/configuration.js');
+config = require('../../libs/configuration.js'),
+logger = require('../../libs/logging.js').logger;
const template = fs.readFileSync(path.join(__dirname, "prove_template.txt")).toString();
+var interceptor = undefined;
+
+/**
+ * allow clients to intercept email messages programatically for local
+ * testing. The `interceptor` is a function which accepts three arguments,
+ *
+ * * `email` - the email that is being verified
+ * * `site` - the RP
+ * * `secret` - the verification secret (usually embedded into a url)
+ *
+ * Limitations: only a single interceptor may be set, generalize
+ * as needed.
+ */
+exports.setInterceptor = function(callback) {
+ interceptor = callback;
+};
+
exports.sendVerificationEmail = function(email, site, secret) {
var url = config.get('URL') + "/prove?token=" + encodeURIComponent(secret);
- emailer.send_mail({
- sender: "noreply@browserid.org",
- to: email,
- subject : "Complete Login to " + site + " using BrowserID",
- body: mustache.to_html(template, { email: email, link: url, site: site })
- }, function(err, success){
- if(!success) {
- console.log("error sending email: ", err);
- console.log("verification URL: ", url);
- }
- });
+ if (interceptor) {
+ interceptor(email, site, secret);
+ } else if (config.get('email_to_console')) {
+ // log verification email to console separated by whitespace.
+ console.log("\nVERIFICATION URL:\n" + url + "\n");
+ } else {
+ emailer.send_mail({
+ sender: "noreply@browserid.org",
+ to: email,
+ subject : "Complete Login to " + site + " using BrowserID",
+ body: mustache.to_html(template, { email: email, link: url, site: site })
+ }, function(err, success){
+ if(!success) {
+ logger.error("error sending email: " + err);
+ logger.error("verification URL: " + url);
+ }
+ });
+ };
};
View
35 browserid/lib/httputils.js
@@ -1,3 +1,38 @@
+/* ***** 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 ***** */
+
// various little utilities to make crafting boilerplate responses
// simple
View
41 browserid/lib/secrets.js
@@ -1,11 +1,46 @@
+/* ***** 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 ***** */
+
const
path = require('path'),
fs = require('fs');
-function generateSecret() {
+exports.generate = function(chars) {
var str = "";
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- for (var i=0; i < 128; i++) {
+ for (var i=0; i < chars; i++) {
str += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
}
return str;
@@ -19,7 +54,7 @@ exports.hydrateSecret = function(name, dir) {
try{ secret = fs.readFileSync(p).toString(); } catch(e) {};
if (secret === undefined) {
- secret = generateSecret();
+ secret = exports.generate(128);
fs.writeFileSync(p, secret);
}
return secret;
View
35 browserid/lib/webfinger.js
@@ -1,3 +1,38 @@
+/* ***** 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 ***** */
+
const
db = require('./db.js'),
fs = require('fs'),
View
146 browserid/lib/wsapi.js
@@ -1,3 +1,38 @@
+/* ***** 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
@@ -8,7 +43,9 @@ db = require('./db.js'),
url = require('url'),
httputils = require('./httputils.js');
email = require('./email.js'),
-bcrypt = require('bcrypt');
+bcrypt = require('bcrypt'),
+crypto = require('crypto'),
+logger = require('../../libs/logging.js').logger;
function checkParams(params) {
return function(req, resp, next) {
@@ -26,7 +63,7 @@ function checkParams(params) {
}
});
} catch(e) {
- console.log("error : " + e.toString());
+ logger.error(e.toString());
return httputils.badRequest(resp, "missing '" + e + "' argument");
}
next();
@@ -48,6 +85,34 @@ function checkAuthed(req, resp, next) {
}
function setup(app) {
+ // 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];
+ });
+ }
+
+ // return the CSRF token
+ // IMPORTANT: this should be safe because it's only readable by same-origin code
+ // but we must be careful that this is never a JSON structure that could be hijacked
+ // by a third party
+ app.get('/wsapi/csrf', 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);
+ }
+
+ res.write(req.session.csrf);
+ res.end();
+ });
+
+
/* 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) {
@@ -64,33 +129,41 @@ function setup(app) {
* the staged user account transitions to a valid user account */
app.post('/wsapi/stage_user', checkParams([ "email", "pass", "pubkey", "site" ]), function(req, resp) {
- // bcrypt the password
// we should be cloning this object here.
var stageParams = req.body;
+
+ // issue #155, valid password length is between 8 and 80 chars.
+ if (stageParams.pass.length < 8 || stageParams.pass.length > 80) {
+ httputils.badRequest(resp, "valid passwords are between 8 and 80 chars");
+ return;
+ }
+
+ // bcrypt the password
stageParams['hash'] = bcrypt.encrypt_sync(stageParams.pass, bcrypt.gen_salt_sync(10));
try {
// upon success, stage_user returns a secret (that'll get baked into a url
// and given to the user), on failure it throws
- var secret = db.stageUser(stageParams);
-
- // store the email being registered in the session data
- if (!req.session) req.session = {};
+ db.stageUser(stageParams, function(secret) {
+ // store the email being registered in the session data
+ if (!req.session) req.session = {};
- // store inside the session the details of this pending verification
- req.session.pendingVerification = {
- email: stageParams.email,
- hash: stageParams.hash // we must store both email and password to handle the case where
- // a user re-creates an account - specifically, registration status
- // must ensure the new credentials work to properly verify that
- // the user has clicked throught the email link. note, this salted, bcrypted
- // representation of a user's password will get thrust into an encrypted cookie
- // served over an encrypted (SSL) session. guten, yah.
- };
+ // store inside the session the details of this pending verification
+ req.session.pendingVerification = {
+ email: stageParams.email,
+ hash: stageParams.hash // we must store both email and password to handle the case where
+ // a user re-creates an account - specifically, registration status
+ // must ensure the new credentials work to properly verify that
+ // the user has clicked throught the email link. note, this salted, bcrypted
+ // representation of a user's password will get thrust into an encrypted cookie
+ // served over an encrypted (SSL) session. guten, yah.
+ };
- resp.json(true);
- email.sendVerificationEmail(stageParams.email, stageParams.site, secret);
+ resp.json(true);
+ // let's now kick out a verification email!
+ email.sendVerificationEmail(stageParams.email, stageParams.site, secret);
+ });
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
@@ -128,7 +201,6 @@ function setup(app) {
} else {
// this is a pending registration, let's check if the creds stored on the
// session are good yet.
-
var v = req.session.pendingVerification;
db.checkAuth(v.email, function(hash) {
if (hash === v.hash) {
@@ -161,17 +233,17 @@ function setup(app) {
app.post('/wsapi/add_email', checkAuthed, checkParams(["email", "pubkey", "site"]), function (req, resp) {
try {
- // upon success, stage_user returns a secret (that'll get baked into a url
- // and given to the user), on failure it throws
- var secret = db.stageEmail(req.session.authenticatedUser, req.body.email, req.body.pubkey);
+ // on failure stageEmail may throw
+ db.stageEmail(req.session.authenticatedUser, req.body.email, req.body.pubkey, function(secret) {
- // store the email being added in session data
- req.session.pendingAddition = req.body.email;
+ // store the email being added in session data
+ req.session.pendingAddition = req.body.email;
- resp.json(true);
+ resp.json(true);
- // let's now kick out a verification email!
- email.sendVerificationEmail(req.body.email, req.body.site, secret);
+ // let's now kick out a verification email!
+ email.sendVerificationEmail(req.body.email, req.body.site, secret);
+ });
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
@@ -183,7 +255,7 @@ function setup(app) {
db.removeEmail(req.session.authenticatedUser, email, function(error) {
if (error) {
- console.log("error removing email " + email);
+ logger.error("error removing email " + email);
httputils.badRequest(resp, error.toString());
} else {
resp.json(true);
@@ -193,7 +265,7 @@ function setup(app) {
app.post('/wsapi/account_cancel', checkAuthed, function(req, resp) {
db.cancelAccount(req.session.authenticatedUser, function(error) {
if (error) {
- console.log("error cancelling account : " + error.toString());
+ logger.error("error cancelling account : " + error.toString());
httputils.badRequest(resp, error.toString());
} else {
resp.json(true);
@@ -218,17 +290,23 @@ function setup(app) {
// if they're authenticated for an email address that we don't know about,
// then we should purge the stored cookie
i