diff --git a/bin/load_gen b/bin/load_gen index aa858490e194c7a1af8834a882d31289f617d530..a5b33cf11b66c6a0917c9578c29e2bfbefe1a633 100755 --- a/bin/load_gen +++ b/bin/load_gen @@ -61,7 +61,7 @@ var argv = require('optimist') .string('s') .describe('s', 'base URL to browserid server') .check(function(argv) { - return (typeof argv.s === 'string' || argv.l) != undefined; + return (argv.h || typeof argv.s === 'string' || argv.l) != undefined; }) .alias('v', 'verifier') .describe('v', 'base URL to verifier service (default is browserid server + \'/verify\')') diff --git a/lib/load_gen/common.js b/lib/load_gen/common.js index 0a7cf61789c473e022578ff4195436de7309cd06..cd94b5ade023e161b63719957271f56ef26bdde2 100644 --- a/lib/load_gen/common.js +++ b/lib/load_gen/common.js @@ -67,7 +67,7 @@ exports.genAssertionAndVerify = function(cfg, user, ctx, email, audience, cb) { try { if (!typeof JSON.parse(r.body) === 'object') throw 'bogus response'; } catch(e) { - return cb(e.toString() + " - " + r.body); + return cb(e.toString() + (r ? (" - " + r.body) : "")); } var assertion = crypto.getAssertion({ diff --git a/lib/wsapi.js b/lib/wsapi.js index 4fb4bab3493eb30dc4f5d25b188d12a7b4f2a1dd..45ca5854e9a98398dfdaf1086ee5929f82a5bdbd 100644 --- a/lib/wsapi.js +++ b/lib/wsapi.js @@ -32,8 +32,16 @@ function clearAuthenticatedUser(session) { session.reset(['csrf']); } -function isAuthed(req) { - return (req.session) ? req.session.userid : undefined; +function isAuthed(req, requiredLevel) { + if (req.session && req.session.userid && req.session.auth_level) { + // 'password' authentication allows access to all apis. + // 'assertion' authentication, grants access to only those apis + // that don't require 'password' + if (requiredLevel === 'assertion' || req.session.auth_level === 'password') { + return true; + } + } + return false; } function bcryptPassword(password, cb) { @@ -45,8 +53,12 @@ function bcryptPassword(password, cb) { }); }; -function setAuthenticatedUser(session, uid) { +function authenticateSession(session, uid, level) { + if (['assertion', 'password'].indexOf(level) === -1) + throw "invalid authentication level: " + level; + session.userid = uid; + session.auth_level = level; } function checkPassword(pass) { @@ -59,7 +71,7 @@ function checkPassword(pass) { exports.clearAuthenticatedUser = clearAuthenticatedUser; exports.isAuthed = isAuthed; exports.bcryptPassword = bcryptPassword; -exports.setAuthenticatedUser = setAuthenticatedUser; +exports.authenticateSession = authenticateSession; exports.checkPassword = checkPassword; exports.fowardWritesTo = undefined; @@ -278,7 +290,7 @@ exports.setup = function(options, app) { // above // does the request require authentication? - if (wsapis[operation].authed && !isAuthed(req)) { + if (wsapis[operation].authed && !isAuthed(req, wsapis[operation].authed)) { return httputils.badRequest(resp, "requires authentication"); } diff --git a/lib/wsapi/account_cancel.js b/lib/wsapi/account_cancel.js index d15da66b08a9cc3ab4d968df6a54fb599690f6ed..c0b59193ec497eab8f890f55636587df3b406b29 100644 --- a/lib/wsapi/account_cancel.js +++ b/lib/wsapi/account_cancel.js @@ -5,7 +5,7 @@ logger = require('../logging.js').logger; exports.method = 'post'; exports.writes_db = true; -exports.authed = true; +exports.authed = 'assertion'; exports.process = function(req, res) { db.cancelAccount(req.session.userid, function(error) { diff --git a/lib/wsapi/add_email_with_assertion.js b/lib/wsapi/add_email_with_assertion.js index 0fdb7bd9c35b7dc2d66fd3f615c3d71dec8984a8..1e6c6f99a19616d37fdebc50a015fb93d2d966e8 100644 --- a/lib/wsapi/add_email_with_assertion.js +++ b/lib/wsapi/add_email_with_assertion.js @@ -10,7 +10,7 @@ https = require('https'); exports.method = 'post'; exports.writes_db = true; -exports.authed = true; +exports.authed = 'assertion'; exports.args = ['assertion']; // This WSAPI will be invoked when a user attempts to add a primary diff --git a/lib/wsapi/auth_with_assertion.js b/lib/wsapi/auth_with_assertion.js index 8db03f64bf3560b495df25dab8b548d9da0283ff..2e9c10f61877923682acf5b13ddb9adadc5255ed 100644 --- a/lib/wsapi/auth_with_assertion.js +++ b/lib/wsapi/auth_with_assertion.js @@ -33,7 +33,7 @@ exports.process = function(req, res) { if (type === 'primary') { return db.emailToUID(email, function(uid) { if (!uid) return res.json({ success: false, reason: "internal error" }); - wsapi.setAuthenticatedUser(req.session, uid); + wsapi.authenticateSession(req.session, uid, 'assertion'); return res.json({ success: true }); }); } @@ -82,7 +82,7 @@ exports.process = function(req, res) { } logger.info("successfully created primary acct for " + email + " (" + r.userid + ")"); - wsapi.setAuthenticatedUser(req.session, r.userid); + wsapi.authenticateSession(req.session, r.userid, 'assertion'); res.json({ success: true }); }); }).on('error', function(e) { diff --git a/lib/wsapi/authenticate_user.js b/lib/wsapi/authenticate_user.js index f6f415c9fa5e31b90d63d072517f6b41b3487519..25b073bc60ef7eeb4da8a31e3285f36eb69104c1 100644 --- a/lib/wsapi/authenticate_user.js +++ b/lib/wsapi/authenticate_user.js @@ -50,7 +50,7 @@ exports.process = function(req, res) { } else { if (!req.session) req.session = {}; - wsapi.setAuthenticatedUser(req.session, uid); + wsapi.authenticateSession(req.session, uid, 'password'); res.json({ success: true }); diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js index c5952ba40c8e5e34d96a023698715be09f994ac1..69321169b74ff449e25c564cf32359fadbc224ce 100644 --- a/lib/wsapi/cert_key.js +++ b/lib/wsapi/cert_key.js @@ -7,7 +7,7 @@ config = require('../configuration.js'); exports.method = 'post'; exports.writes_db = false; -exports.authed = true; +exports.authed = 'password'; exports.args = ['email','pubkey']; exports.process = function(req, res) { diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js index deedcd4b90e6194ffc95530c108ee818f36fa944..c0449c5bfcdf83d2cbdabc3e8ecb2f9f4e7842c9 100644 --- a/lib/wsapi/complete_email_addition.js +++ b/lib/wsapi/complete_email_addition.js @@ -45,7 +45,13 @@ exports.process = function(req, res) { return res.json({ success: false }); } db.updatePassword(uid, hash, function(err) { - if (err) logger.warn("couldn't update password during email verification: " + err); + if (err) { + logger.warn("couldn't update password during email verification: " + err); + } else { + // XXX: what if our software 503s? User doens't get a password set and + // cannot change it. + wsapi.authenticateSession(req.session, uid, 'password'); + } res.json({ success: !err }); }); }); diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js index 912e942629d8ed5876f123d259634c9d542b37a9..fd582b7ea02f0e9ff56aa0214cd02acbc823eb28 100644 --- a/lib/wsapi/complete_user_creation.js +++ b/lib/wsapi/complete_user_creation.js @@ -45,7 +45,7 @@ exports.process = function(req, res) { // 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, uid); + wsapi.authenticateSession(req.session, uid, 'password'); res.json({ success: true }); } }); diff --git a/lib/wsapi/email_addition_status.js b/lib/wsapi/email_addition_status.js index aa787fbb33b8f1aef3149ffdacc645aadaf69c4f..0139e779a137a591c3ff598eabd2add04211c5a9 100644 --- a/lib/wsapi/email_addition_status.js +++ b/lib/wsapi/email_addition_status.js @@ -9,7 +9,7 @@ db = require('../db.js'); exports.method = 'get'; exports.writes_db = false; -exports.authed = true; +exports.authed = 'assertion'; exports.args = ['email']; exports.process = function(req, res) { diff --git a/lib/wsapi/list_emails.js b/lib/wsapi/list_emails.js index fb991940d3adb1a496f8b024f226ab49f093aa8a..4314bdf000345f80b61ebc72fbe34a45b2e0fa15 100644 --- a/lib/wsapi/list_emails.js +++ b/lib/wsapi/list_emails.js @@ -11,7 +11,7 @@ logger = require('../logging.js').logger; exports.method = 'get'; exports.writes_db = false; -exports.authed = true; +exports.authed = 'assertion'; exports.process = function(req, resp) { logger.debug('listing emails for user ' + req.session.userid); diff --git a/lib/wsapi/logout.js b/lib/wsapi/logout.js index ff48f014b33cf94d5685c6dac619f8b5ed878ad9..41a84b2a0529d1053abbdc4aa6b4e7a4d4d695c9 100644 --- a/lib/wsapi/logout.js +++ b/lib/wsapi/logout.js @@ -3,7 +3,7 @@ wsapi = require('../wsapi.js'); exports.method = 'post'; exports.writes_db = false; -exports.authed = true; +exports.authed = 'assertion'; exports.process = function(req, res) { wsapi.clearAuthenticatedUser(req.session); diff --git a/lib/wsapi/remove_email.js b/lib/wsapi/remove_email.js index a98cbcda28d249ed79a7080854bd5734d533d4c6..9df5a3818836a9527e8e96ae2816de82f0078718 100644 --- a/lib/wsapi/remove_email.js +++ b/lib/wsapi/remove_email.js @@ -5,7 +5,7 @@ logger = require('../logging.js').logger; exports.method = 'post'; exports.writes_db = true; -exports.authed = true; +exports.authed = 'assertion'; exports.args = ['email']; exports.process = function(req, res) { diff --git a/lib/wsapi/session_context.js b/lib/wsapi/session_context.js index 89386ed008fda43f5a4e26660508f6cf18a9cc31..bc57b6bfe5b14bd97a025750fdc7a953d7ac4aac 100644 --- a/lib/wsapi/session_context.js +++ b/lib/wsapi/session_context.js @@ -29,13 +29,15 @@ exports.process = function(req, res) { logger.debug("NEW csrf token created: " + req.session.csrf); } - var auth_status = false; + var auth_level = undefined; + var authenticated = false; function sendResponse() { res.json({ csrf_token: req.session.csrf, server_time: (new Date()).getTime(), - authenticated: auth_status, + authenticated: authenticated, + auth_level: auth_level, domain_key_creation_time: domainKeyCreationDate.getTime(), random_seed: crypto.randomBytes(32).toString('base64') }); @@ -43,7 +45,7 @@ exports.process = function(req, res) { // 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)) { + if (!wsapi.isAuthed(req, 'assertion')) { logger.debug("user is not authenticated"); sendResponse(); } else { @@ -53,7 +55,8 @@ exports.process = function(req, res) { wsapi.clearAuthenticatedUser(req.session); } else { logger.debug("user is authenticated"); - auth_status = true; + auth_level = req.session.auth_level; + authenticated = true; } sendResponse(); }); diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js index e1c7235a5697ce3a59aab4ce3dd689745af16249..81d2f2c3e27dcb64c049db58b097f3c3ff872135 100644 --- a/lib/wsapi/stage_email.js +++ b/lib/wsapi/stage_email.js @@ -13,7 +13,7 @@ email = require('../email.js'); exports.method = 'post'; exports.writes_db = true; -exports.authed = true; +exports.authed = 'assertion'; exports.args = ['email','site']; exports.process = function(req, res) { diff --git a/lib/wsapi/update_password.js b/lib/wsapi/update_password.js index fe647160e91e4c382be6ea23956115c4a9246a27..97dfb7c91aaaac643330146328ed12783ed0f875 100644 --- a/lib/wsapi/update_password.js +++ b/lib/wsapi/update_password.js @@ -7,7 +7,7 @@ bcrypt = require('../bcrypt'); exports.method = 'post'; exports.writes_db = true; -exports.authed = true; +exports.authed = 'password'; exports.args = ['oldpass','newpass']; exports.process = function(req, res) { diff --git a/lib/wsapi/user_creation_status.js b/lib/wsapi/user_creation_status.js index c06b6f624885d87142c595b88159b68a649a38ba..f62e5ea9f3bceb347a18fe98dabffc0024805112 100644 --- a/lib/wsapi/user_creation_status.js +++ b/lib/wsapi/user_creation_status.js @@ -11,7 +11,7 @@ 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)) { + if (wsapi.isAuthed(req, 'assertion')) { db.userOwnsEmail(req.session.userid, email, function(owned) { if (owned) res.json({ status: 'complete' }); else notAuthed(); diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js index e2ab7fa071d129057a11d36e89218676254a93c3..a730fcad8fbcf71c4286374613892ea51c204506 100644 --- a/resources/static/include_js/include.js +++ b/resources/static/include_js/include.js @@ -817,14 +817,11 @@ } }; } - }; + } } else { return { open: function(url, winopts, arg, cb) { setTimeout(function() { cb("unsupported browser"); }, 0); - }, - onOpen: function(cb) { - setTimeout(function() { cb("unsupported browser"); }, 0); } }; } diff --git a/tests/primary-then-secondary-test.js b/tests/primary-then-secondary-test.js index 55ef5df97f0e28d75337d46cd77ea3133e87d0ed..d4d244d5748ca79b583724cc953d4cb50b73e279 100755 --- a/tests/primary-then-secondary-test.js +++ b/tests/primary-then-secondary-test.js @@ -94,6 +94,17 @@ suite.addBatch({ } }); +// now we have an account, and we're authenticated with an assertion. +// check auth_level with session_context +suite.addBatch({ + "auth_level": { + topic: wsapi.get('/wsapi/session_context'), + "is 'assertion' after authenticating with assertion" : function(r, err) { + assert.strictEqual(JSON.parse(r.body).auth_level, 'assertion'); + } + } +}); + var token; // now we have a new account. let's add a secondary to it @@ -150,6 +161,17 @@ suite.addBatch({ } }); +// after adding a secondary and setting password, we're password auth'd +suite.addBatch({ + "auth_level": { + topic: wsapi.get('/wsapi/session_context'), + "is 'password' after authenticating with password" : function(r, err) { + assert.strictEqual(JSON.parse(r.body).auth_level, 'password'); + } + } +}); + + // adding a second secondary will not let us set the password suite.addBatch({ "add a new email address to our account": { diff --git a/tests/registration-status-wsapi-test.js b/tests/registration-status-wsapi-test.js index dfb962282f18bee85ed605632593d75669778c51..73012662d29ddbb71955f097c045ca8592fdb5f1 100755 --- a/tests/registration-status-wsapi-test.js +++ b/tests/registration-status-wsapi-test.js @@ -175,7 +175,7 @@ suite.addBatch({ topic: wsapi.get("/wsapi/session_context"), "we're authenticated": function (r, err) { assert.strictEqual(r.code, 200); - assert.strictEqual(JSON.parse(r.body).authenticated, true); + assert.strictEqual(JSON.parse(r.body).auth_level, 'password'); }, "but we can easily clear cookies on the client to change that!": function(r, err) { wsapi.clearCookies(); diff --git a/tests/two-level-auth-test.js b/tests/two-level-auth-test.js new file mode 100755 index 0000000000000000000000000000000000000000..b486a42c424a876e7b3d194291efb556f0f18dad --- /dev/null +++ b/tests/two-level-auth-test.js @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +/* ***** 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 ***** */ + +require('./lib/test_env.js'); + +const +assert = require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'), +primary = require('./lib/primary.js'); + +var suite = vows.describe('primary-then-secondary'); + +start_stop.addStartupBatches(suite); + +// this test verifies that a user who has only authenticated with +// an assertion from their primary, may not call restricted apis + +const TEST_DOMAIN = 'example.domain', + TEST_EMAIL = 'testuser2@' + TEST_DOMAIN, + TEST_ORIGIN = 'http://127.0.0.1:10002'; + +var primaryUser = new primary({ + email: TEST_EMAIL, + domain: TEST_DOMAIN +}); + +// now let's generate an assertion using this user +suite.addBatch({ + "generating an assertion": { + topic: function() { + return primaryUser.getAssertion(TEST_ORIGIN); + }, + "succeeds": function(r, err) { + assert.isString(r); + }, + "and logging in with the assertion": { + topic: function(assertion) { + wsapi.post('/wsapi/auth_with_assertion', { + email: TEST_EMAIL, + assertion: assertion + }).call(this); + }, + "succeeds": function(r, err) { + var resp = JSON.parse(r.body); + assert.isObject(resp); + assert.isTrue(resp.success); + } + } + } +}); + +suite.addBatch({ + "updating our password": { + topic: wsapi.post('/wsapi/update_password', { oldpass: '', newpass: 'frobaztastic' }), + "won't work": function(r) { + assert.strictEqual(r.code, 400); + } + }, + "certifying a key": { + topic: wsapi.post('/wsapi/cert_key', { email: TEST_EMAIL, pubkey: 'fake_key' }), + "won't work": function(r) { + assert.strictEqual(r.code, 400); + } + }, + "listing emails": { + topic: wsapi.get('/wsapi/list_emails'), + "works fine": function(r) { + assert.strictEqual(r.code, 200); + assert.equal(Object.keys(JSON.parse(r.body)).length, 1); + } + } +}); + +// shut the server down and cleanup +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module);