diff --git a/bin/keysigner b/bin/keysigner index b2002a722fdb8049d6137d7f3f3854e395a21a1b..316083aabf6a50f1ac3d309c5849f0f6d8cd3826 100755 --- a/bin/keysigner +++ b/bin/keysigner @@ -75,14 +75,13 @@ try { process.exit(1); } - // and our single function -app.post('/wsapi/cert_key', validate(["email", "pubkey"]), function(req, resp) { +app.post('/wsapi/cert_key', validate(["email", "pubkey", "ephemeral"]), function(req, resp) { var startTime = new Date(); cc.enqueue({ pubkey: req.body.pubkey, email: req.body.email, - validityPeriod: config.get('certificate_validity_ms'), + validityPeriod: (req.body.ephemeral ? config.get('ephemeral_session_duration_ms') : config.get('certificate_validity_ms')), hostname: HOSTNAME }, function (err, r) { var reqTime = new Date - startTime; diff --git a/example/rp/index.html b/example/rp/index.html index d14fa1ad693c3b72b16dd057c7b6a1da0e5eaeb2..924e8f350f6dbe14787961e98a3a896fbc561259 100644 --- a/example/rp/index.html +++ b/example/rp/index.html @@ -15,10 +15,11 @@ BrowserID Relying Party body { margin: auto; font: 13px/1.5 Helvetica, Arial, 'Liberation Sans', FreeSans, sans-serif; } a:link, a:visited { font-style: italic; text-decoration: none; color: #008; } a:hover { border-bottom: 2px solid black ; } -.title { font-size: 2em; font-weight: bold; text-align: center; margin: 1.5em; } -.intro { font-size: 1.2em; width: 600px; margin: auto; } -.specify { font-size: 1.1em; width: 600px; padding-top: 2em; margin: auto; } -.assertion, .verifierResp { width: 600px; margin: auto; } +.title { font-size: 2em; font-weight: bold; text-align: center; margin: 1.5em auto 1.5em auto; } +.intro { font-size: 1.2em; } +.specify { font-size: 1.1em; padding-top: 2em; } +body div { width: 600px; margin: auto; } + pre { font-family: 'lucida console', monaco, 'andale mono', 'bitstream vera sans mono', consolas, monospace; border: 3px solid #666; @@ -31,7 +32,6 @@ pre { background-color: #333; /* white-space: pre;*/ font-size: .9em; - width:600px; word-wrap: break-word; } @@ -60,12 +60,6 @@ pre { <p>What flavor of assertion would you like?</p> <ul> <li> - <input type="checkbox" id="silent"> - <label for="silent">Silent</label> - </li><li> - <input type="checkbox" id="allowPersistent"> - <label for="allowPersistent">Allow persistent sign-in</label> - </li><li> <input type="checkbox" id="privacy"> <label for="privacy">Supply a privacy policy</label> </li><li> @@ -76,15 +70,26 @@ pre { <label for="requiredEmail">Require a specific email</label><br /> </li> </ul> - <button>Get an assertion</button> + <button class="assertion">Get an assertion</button> + <button class="logout">logout</button> +</div> + +<div class="loginEvents"> + <h2>login events</h2> + <pre> ... login events ... </pre> </div> -<div class="verifierResp"> - <pre> ... verifier response ... </pre> +<div class="logoutEvents"> + <h2>logout events</h2> + <pre> ... logout events ... </pre> </div> -<div class="assertion"> - <pre> ... ye' ol' assertion ... </pre> +<div class="loginCanceledEvents"> + <h2>loginCanceled events</h2> + <pre> ... loginCanceled events ... </pre> +</div> + + </body> </div> @@ -92,6 +97,12 @@ pre { <script src="https://browserid.org/include.js"></script> <script> +function loggit() { + try { + console.log.apply(console, arguments); + } catch(e) {} +} + // a function to check an assertion against the server function checkAssertion(assertion) { $.ajax({ @@ -103,37 +114,47 @@ function checkAssertion(assertion) { audience: window.location.protocol + "//" + window.location.host }, success: function(data, textStatus, jqXHR) { - $(".verifierResp > pre").text(JSON.stringify(data, null, 4)); + $(".loginEvents > pre").text(JSON.stringify(data, null, 4)); }, error: function(jqXHR, textStatus, errorThrown) { var resp = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : errorThrown; - $(".verifierResp > pre").text(resp); + $(".loginEvents > pre").text(resp); } }); }; +navigator.id.addEventListener('login', function(event) { + loggit("login event"); + checkAssertion(event.assertion); +}); + +navigator.id.addEventListener('logout', function(event) { + loggit("logout event"); + var txt = 'got event at ' + (new Date).toString(); + $(".logoutEvents > pre").text(txt); +}); + +navigator.id.addEventListener('loginCanceled', function(event) { + loggit("loginCanceled"); + var txt = 'got event at ' + (new Date).toString(); + $(".loginCanceledEvents > pre").text(txt); +}); + $(document).ready(function() { - $(".specify button").click(function() { + $(".specify button.assertion").click(function() { $("pre").text("... waiting ..."); var requiredEmail = $.trim($('#requiredEmail').val()); if (!requiredEmail.length) requiredEmail = undefined; - navigator.id.get(function(assertion) { - if (!assertion) { - $(".assertion pre").text("navigator.id.get() returns NULL"); - } else { - $(".assertion pre").text(assertion); - checkAssertion(assertion); - } - }, { - silent: $('#silent').attr('checked'), - allowPersistent: $('#allowPersistent').attr('checked'), + navigator.id.request({ privacyURL: $('#privacy').attr('checked') ? "/privacy.html" : undefined, tosURL: $('#tos').attr('checked') ? "/TOS.html" : undefined, requiredEmail: requiredEmail }); }); + + $(".specify button.logout").click(navigator.id.logout); }); </script> diff --git a/lib/configuration.js b/lib/configuration.js index eca775e276a164affb00a5fa666e676a9b37f779..8ba0fabc93cc20943e9df3bc9d10cda75c97c627 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -128,6 +128,10 @@ var conf = module.exports = convict({ doc: "How long may a user stay signed?", format: 'integer = 1209600000' }, + ephemeral_session_duration_ms: { + doc: "How long a user on a shared computer shall be authenticated", + format: 'integer = 3600000' // 1 hour + }, certificate_validity_ms: { doc: "For how long shall certificates issued by BrowserID be valid?", format: 'integer = 86400000' diff --git a/lib/db/mysql.js b/lib/db/mysql.js index 2931097bcde0d45dd58fdeef5265e1c1524a2a6f..19a6aa4ff01375917223f96af897e89d60817de5 100644 --- a/lib/db/mysql.js +++ b/lib/db/mysql.js @@ -118,7 +118,6 @@ exports.open = function(cfg, cb) { logger.debug("connecting to database: " + database); options.database = database; client = mysql.createClient(options); - client.ping(function(err) { logger.debug("connection to database " + (err ? ("fails: " + err) : "established")); cb(err); diff --git a/lib/http_forward.js b/lib/http_forward.js index cb4396a4b65c8281f5330fbfddd053229a9379c2..5277aa95643a31659a9a45085902e5b31cc37065 100644 --- a/lib/http_forward.js +++ b/lib/http_forward.js @@ -67,12 +67,14 @@ module.exports = function(dest, req, res, cb) { // forward header if (req.headers['accept-language']) { - preq.setHeader('Accept-Language', req.headers['accept-language']); + preq.setHeader('Accept-Language', req.headers['accept-language']); } // if the body has already been parsed, we'll write it if (req.body) { - var data = querystring.stringify(req.body); + var data; + if (req.headers['content-type'].indexOf('application/json') === 0) data = JSON.stringify(req.body); + else data = querystring.stringify(req.body); preq.setHeader('content-length', data.length); preq.write(data); preq.end(); diff --git a/lib/static_resources.js b/lib/static_resources.js index 0b25b8948b82ec45b6f48118e0a4d9846386eb33..f5dc77d8c059b7e652922993d753d7eb99094917 100644 --- a/lib/static_resources.js +++ b/lib/static_resources.js @@ -95,6 +95,7 @@ var dialog_js = und.flatten([ '/dialog/controllers/provision_primary_user.js', '/dialog/controllers/primary_user_provisioned.js', '/dialog/controllers/email_chosen.js', + '/dialog/controllers/is_this_your_computer.js', '/dialog/start.js' ]]); diff --git a/lib/validate.js b/lib/validate.js index ebe808dd35e85057f200f33cdc28900a716ffc45..b9f6d4dbc911ab54105afdff8f5719f5df430be4 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -27,7 +27,7 @@ module.exports = function (params) { try { params.forEach(function(k) { - if (!params_in_request || !params_in_request.hasOwnProperty(k) || typeof params_in_request[k] !== 'string') { + if (!params_in_request || !params_in_request.hasOwnProperty(k)) { throw k; } }); diff --git a/lib/wsapi.js b/lib/wsapi.js index 7ee1f4a8d24e85b058a316fb657d2e3585831c6a..c76c7e3e23bf501492316c40eb4dcf1b3a4c4c0a 100644 --- a/lib/wsapi.js +++ b/lib/wsapi.js @@ -77,7 +77,7 @@ function bcryptPassword(password, cb) { }); }; -function authenticateSession(session, uid, level) { +function authenticateSession(session, uid, level, duration_ms) { if (['assertion', 'password'].indexOf(level) === -1) throw "invalid authentication level: " + level; @@ -87,6 +87,9 @@ function authenticateSession(session, uid, level) { session.auth_level !== level) { logger.info("not resetting cookies to 'assertion' authenticate a user who is already password authenticated"); } else { + if (duration_ms) { + session.setDuration(duration_ms); + } session.userid = uid; session.auth_level = level; } diff --git a/lib/wsapi/auth_with_assertion.js b/lib/wsapi/auth_with_assertion.js index 8781151358379e93e4f3e0bbe09c182c62b75667..7811124f8387bae3f6092b6b392d5cd34d1457c1 100644 --- a/lib/wsapi/auth_with_assertion.js +++ b/lib/wsapi/auth_with_assertion.js @@ -15,7 +15,7 @@ https = require('https'); exports.method = 'post'; exports.writes_db = false; exports.authed = false; -exports.args = ['assertion']; +exports.args = ['assertion', 'ephemeral']; exports.i18n = false; exports.process = function(req, res) { @@ -41,8 +41,10 @@ exports.process = function(req, res) { return db.emailToUID(email, function(err, uid) { if (err) return wsapi.databaseDown(res, err); if (!uid) return res.json({ success: false, reason: "internal error" }); - wsapi.authenticateSession(req.session, uid, 'assertion'); - return res.json({ success: true }); + wsapi.authenticateSession(req.session, uid, 'assertion', + req.body.ephemeral ? config.get('ephemeral_session_duration_ms') + : config.get('authentication_duration_ms')); + return res.json({ success: true, userid: uid }); }); } else if (type === 'secondary') { @@ -90,8 +92,10 @@ exports.process = function(req, res) { } logger.info("successfully created primary acct for " + email + " (" + r.userid + ")"); - wsapi.authenticateSession(req.session, r.userid, 'assertion'); - res.json({ success: true }); + wsapi.authenticateSession(req.session, r.userid, 'assertion', + req.body.ephemeral ? config.get('ephemeral_session_duration_ms') + : config.get('authentication_duration_ms')); + res.json({ success: true, userid: r.userid }); }); }).on('error', function(e) { logger.error("failed to create primary user with assertion for " + email + ": " + e); diff --git a/lib/wsapi/authenticate_user.js b/lib/wsapi/authenticate_user.js index b1715a1b4c21fce281502e366ab4c1b47b8877fd..89e524ec8a2cde0a5e90bdfb6f38d1534c29d544 100644 --- a/lib/wsapi/authenticate_user.js +++ b/lib/wsapi/authenticate_user.js @@ -16,7 +16,7 @@ statsd = require('../statsd'); exports.method = 'post'; exports.writes_db = false; exports.authed = false; -exports.args = ['email','pass']; +exports.args = ['email','pass', 'ephemeral']; exports.i18n = false; exports.process = function(req, res) { @@ -59,8 +59,10 @@ exports.process = function(req, res) { } else { if (!req.session) req.session = {}; - wsapi.authenticateSession(req.session, uid, 'password'); - res.json({ success: true }); + wsapi.authenticateSession(req.session, uid, 'password', + req.body.ephemeral ? config.get('ephemeral_session_duration_ms') + : config.get('authentication_duration_ms')); + res.json({ success: true, userid: uid }); // if the work factor has changed, update the hash here. issue #204 diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js index 9b642eb341646f360b8e4288dd166ed15b6d6713..777d61223b6732ab620c66325baa9975eac891f5 100644 --- a/lib/wsapi/cert_key.js +++ b/lib/wsapi/cert_key.js @@ -14,7 +14,7 @@ wsapi = require('../wsapi.js'); exports.method = 'post'; exports.writes_db = false; exports.authed = 'password'; -exports.args = ['email','pubkey']; +exports.args = ['email','pubkey','ephemeral']; exports.i18n = false; exports.process = function(req, res) { diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js index 882351b630f784c34528302de0682ff9870859fc..dca109d14da9a856d1ab7fc6792ec100a932a37c 100644 --- a/lib/wsapi/complete_user_creation.js +++ b/lib/wsapi/complete_user_creation.js @@ -50,7 +50,8 @@ 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.authenticateSession(req.session, uid, 'password'); + wsapi.authenticateSession(req.session, uid, 'password', + config.get('ephemeral_session_duration_ms')); res.json({ success: true }); } }); diff --git a/lib/wsapi/prolong_session.js b/lib/wsapi/prolong_session.js new file mode 100644 index 0000000000000000000000000000000000000000..2c9d5c02edb4ab2b9d04ee76ad519573f3c7debb --- /dev/null +++ b/lib/wsapi/prolong_session.js @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const +config = require('../configuration.js'), +wsapi = require('../wsapi.js'); + +exports.method = 'post'; +exports.writes_db = false; +exports.authed = 'assertion'; +exports.i18n = false; + +exports.process = function(req, res) { + wsapi.authenticateSession(req.session, req.session.userid, req.session.auth_level, + config.get('authentication_duration_ms')); + res.send(200); +}; diff --git a/lib/wsapi/session_context.js b/lib/wsapi/session_context.js index 8b7f9e13d7a058f28192ae354cfe55b2a9e5b09b..c62ec790622de67d6681e8700db50db13308f8b5 100644 --- a/lib/wsapi/session_context.js +++ b/lib/wsapi/session_context.js @@ -50,6 +50,10 @@ exports.process = function(req, res) { if (config.get('enable_code_version')) { respObj.code_version = version(); } + if (req.session && req.session.userid) { + respObj.userid = req.session.userid; + } + res.json(respObj); }; diff --git a/lib/wsapi_client.js b/lib/wsapi_client.js index a86095e8f5b6f5fdbeca736e7a98c9d6c9b8c4c9..256fcf71c6e395fad3a18dae23fadbb5cef2c0fc 100644 --- a/lib/wsapi_client.js +++ b/lib/wsapi_client.js @@ -42,6 +42,15 @@ exports.clearCookies = function(ctx) { if (ctx && ctx.session) delete ctx.session; }; +exports.getCookie = function(ctx, which) { + if (typeof which === 'string') which = new Regex('/^' + which + '$/'); + var cookieNames = Object.keys(ctx.cookieJar); + for (var i = 0; i < cookieNames.length; i++) { + if (which.test(cookieNames[i])) return ctx.cookieJar[cookieNames[i]]; + } + return null; +}; + exports.injectCookies = injectCookies; exports.get = function(cfg, path, context, getArgs, cb) { @@ -113,13 +122,13 @@ exports.post = function(cfg, path, context, postArgs, cb) { return; } var headers = { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/json' }; injectCookies(context, headers); if (typeof postArgs === 'object') { postArgs['csrf'] = csrf; - body = querystring.stringify(postArgs); + body = JSON.stringify(postArgs); headers['Content-Length'] = body.length; } diff --git a/package.json b/package.json index 99689b57be155e8d11f1206d1cb757f06e029b38..465efdb4ea432f051411094241366ed717d0bc9c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "connect": "1.7.2", "convict": "0.0.6", "cjson": "0.0.6", - "client-sessions": "0.0.3", + "client-sessions": "0.0.5", "connect-cachify": "0.0.9", "connect-cookie-session": "0.0.2", "connect-logger-statsd": "0.0.1", diff --git a/resources/static/communication_iframe/start.js b/resources/static/communication_iframe/start.js index 386ca965ebb036038498164264af023583456e91..ec70a5b08b97e023640a6b26a45f3233645976be 100644 --- a/resources/static/communication_iframe/start.js +++ b/resources/static/communication_iframe/start.js @@ -6,7 +6,8 @@ (function() { var bid = BrowserID, network = bid.Network, - user = bid.User; + user = bid.User, + storage = bid.Storage; network.init(); @@ -25,28 +26,71 @@ } } - chan.bind("getPersistentAssertion", function(trans, params) { - setRemoteOrigin(trans.origin); + var loggedInUser = undefined; + + // the controlling page may "pause" the iframe when someone else (the dialog) + // is supposed to emit events + var pause = false; - trans.delayReturn(true); + function checkAndEmit(oncomplete) { + if (pause) return; - user.getPersistentSigninAssertion(function(rv) { - trans.complete(rv); - }, function() { - trans.error(); + // this will re-certify the user if neccesary + user.getSilentAssertion(loggedInUser, function(email, assertion) { + if (email) { + // only send login events when the assertion is defined - when + // the 'loggedInUser' is already logged in, it's false - that is + // when the site already has the user logged in and does not want + // the resources or cost required to generate an assertion + if (assertion) chan.notify({ method: 'login', params: assertion }); + loggedInUser = email; + } else if (loggedInUser !== null) { + // only send logout events when loggedInUser is not null, which is an + // indicator that the site thinks the user is logged out + chan.notify({ method: 'logout' }); + loggedInUser = null; + } + oncomplete && oncomplete(); + }, function(err) { + chan.notify({ method: 'logout' }); + loggedInUser = null; + oncomplete && oncomplete(); }); + } + + function watchState() { + storage.watchLoggedIn(remoteOrigin, checkAndEmit); + } + + // one of two events will cause us to begin checking to + // see if an event shall be emitted - either an explicit + // loggedInUser event or page load. + chan.bind("loggedInUser", function(trans, email) { + loggedInUser = email; }); - chan.bind("logout", function(trans, params) { + chan.bind("loaded", function(trans, params) { setRemoteOrigin(trans.origin); + checkAndEmit(watchState); + trans.complete(); + }); - trans.delayReturn(true); + chan.bind("logout", function(trans, params) { + if (loggedInUser != null) { + storage.setLoggedIn(remoteOrigin, false); + chan.notify({ method: 'logout' }); + } + }); - user.clearPersistentSignin(function(rv) { - trans.complete(rv); - }, function() { - trans.error(); - }); + chan.bind("dialog_running", function(trans, params) { + pause = true; }); -}()); + chan.bind("dialog_complete", function(trans, params) { + pause = false; + // the dialog running can change authentication status, + // lets manually purge our network cache + network.clearContext(); + checkAndEmit(); + }); +}()); diff --git a/resources/static/css/common.css b/resources/static/css/common.css index c330b9a99a28cd3401a67365026c4ee7d65a6922..b034067e79737569e34fa651addccfc4726a6a90 100644 --- a/resources/static/css/common.css +++ b/resources/static/css/common.css @@ -168,6 +168,21 @@ button, white-space: nowrap; } +button.negative { + border: 1px solid #E70227; + color: #fff; + text-shadow: -1px -1px 0 #E70227; + + -webkit-box-shadow: 0 0 0 1px #FF1560 inset; + -moz-box-shadow: 0 0 0 1px #FF1560 inset; + -o-box-shadow: 0 0 0 1px #FF1560 inset; + box-shadow: 0 0 0 1px #FF1560 inset; + + background-color: #E70227; + background-image: -moz-linear-gradient(center top , #FF1560 0pt, #E70227 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #FF1560), color-stop(100%, #E70227)); +} + button:hover, button:focus, .button:hover, @@ -178,6 +193,17 @@ button:focus, } +button.negative:hover, +button.negative:focus, +.button.negative:hover, +.button.negative:focus{ + background-color: #FF1560; + background-image: -moz-linear-gradient(center top , #FF1560 70%, #E70227 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(70%, #FF1560), color-stop(100%, #E70227)); + +} + + button:active, .button:active { background-color: #006EC6; diff --git a/resources/static/css/style.css b/resources/static/css/style.css index e2c74432efbaf327f993f5b19397d1f99fa7a50a..23781ec37d78e6dbb92f6b337e3e8432b4f6fd98 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -404,6 +404,13 @@ div.steps { margin-right: 0; } +#logout_everywhere .completion_text { + float: right; + display: none; + color: #090; +} + + button.delete { background-color: #EA7676; border: 1px solid #B13D3D; @@ -655,20 +662,6 @@ h1 { height: 28px; } -#signUpForm .remember { - display: inline-block; - line-height: 28px; -} - -#signUpForm .remember .checkAlign { - float: left; -} - -#signUpForm .remember label { - margin-left: 5px; - float: left; -} - #signUpForm .error { margin-top: 20px; color: red; diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index 320877daf875844026c6909919e5660447cc13c7..315b787cf5b8dc12d0b45261ed2b353ab339bc57 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -11,7 +11,7 @@ BrowserID.Modules.Actions = (function() { serviceManager = bid.module, user = bid.User, errors = bid.Errors, - wait = bid.Wait, + dialogHelpers = bid.Helpers.Dialog, runningService, onsuccess, onerror; @@ -113,12 +113,15 @@ BrowserID.Modules.Actions = (function() { }, self.getErrorDialog(errors.getAssertion)); }, - doAssertionGenerated: function(assertion) { + doAssertionGenerated: function(info) { // Clear onerror before the call to onsuccess - the code to onsuccess // calls window.close, which would trigger the onerror callback if we // tried this afterwards. - onerror = null; - if(onsuccess) onsuccess(assertion); + this.hideWait(); + dialogHelpers.animateClose(function() { + onerror = null; + if(onsuccess) onsuccess(info); + }); }, doNotMe: function() { @@ -151,6 +154,10 @@ BrowserID.Modules.Actions = (function() { startService("primary_user_provisioned", info); }, + doIsThisYourComputer: function(info) { + startService("is_this_your_computer", info); + }, + doEmailChosen: function(info) { startService("email_chosen", info); } diff --git a/resources/static/dialog/controllers/check_registration.js b/resources/static/dialog/controllers/check_registration.js index 4cf14f76f9b573111f7b15cf6a3f594583461f75..cc7fdf53ed7c68f1e67779c855f95d21c5bc0588 100644 --- a/resources/static/dialog/controllers/check_registration.js +++ b/resources/static/dialog/controllers/check_registration.js @@ -32,6 +32,7 @@ BrowserID.Modules.CheckRegistration = (function() { var self=this; user[self.verifier](self.email, function(status) { if (status === "complete") { + // TODO - move the syncEmails somewhere else, perhaps into user.js user.syncEmails(function() { self.close(self.verificationMessage); oncomplete && oncomplete(); diff --git a/resources/static/dialog/controllers/email_chosen.js b/resources/static/dialog/controllers/email_chosen.js index 763b14572cb2fd4eaedde4af53f757155be68c37..8ce4747938702165bd391cb447fbc4176fa9e3fa 100644 --- a/resources/static/dialog/controllers/email_chosen.js +++ b/resources/static/dialog/controllers/email_chosen.js @@ -8,7 +8,9 @@ BrowserID.Modules.EmailChosen = (function() { var bid = BrowserID, dialogHelpers = bid.Helpers.Dialog, - sc; + sc, + user = bid.User, + storage = bid.Storage; var EmailChosen = bid.Modules.PageModule.extend({ start: function(options) { @@ -20,7 +22,8 @@ BrowserID.Modules.EmailChosen = (function() { } dialogHelpers.getAssertion.call(self, email, options.ready); - + // TODO, this is not needed here, it is done in the state machine. + storage.setLoggedIn(user.getOrigin(), options.email); sc.start.call(self, options); } }); diff --git a/resources/static/dialog/controllers/is_this_your_computer.js b/resources/static/dialog/controllers/is_this_your_computer.js new file mode 100644 index 0000000000000000000000000000000000000000..1c2bc2c9eb3599f62239f7130a074f89f640d672 --- /dev/null +++ b/resources/static/dialog/controllers/is_this_your_computer.js @@ -0,0 +1,53 @@ +/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */ +/*global BrowserID:true, PageController: true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +BrowserID.Modules.IsThisYourComputer = (function() { + "use strict"; + + var bid = BrowserID, + user = bid.User, + network = bid.Network, + storage = bid.Storage, + errors = bid.Errors, + email; + + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + options = options || {}; + email = options.email; + + var self = this; + + self.renderWait("is_this_your_computer", options); + + // TODO - Make the selectors use ids instead of classes. + self.click("button.this_is_my_computer", self.yes); + self.click("button.this_is_not_my_computer", self.no); + + Module.sc.start.call(self, options); + }, + + yes: function() { + // TODO - Move this to user.js where it could be used by other clients in + // other areas. + storage.usersComputer.setConfirmed(network.userid()); + this.confirmed(true); + }, + + no: function() { + storage.usersComputer.setDenied(network.userid()); + this.confirmed(false); + }, + + confirmed: function(status) { + this.publish("user_computer_status_set", { users_computer: status }); + } + + }); + + + return Module; + +}()); diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js index 87a311462475ff78ceae1e4d588dc969a60d5825..3a352f19d60e757caf362bacb00539a9b96568a5 100644 --- a/resources/static/dialog/controllers/pick_email.js +++ b/resources/static/dialog/controllers/pick_email.js @@ -51,10 +51,6 @@ BrowserID.Modules.PickEmail = (function() { var origin = user.getOrigin(); storage.site.set(origin, "email", email); - if (self.allowPersistent) { - storage.site.set(origin, "remember", $("#remember").is(":checked")); - } - self.close("email_chosen", { email: email }); } } @@ -90,14 +86,11 @@ BrowserID.Modules.PickEmail = (function() { options = options || {}; - self.allowPersistent = options.allow_persistent; dom.addClass("body", "pickemail"); self.renderDialog("pick_email", { identities: getSortedIdentities(), siteemail: storage.site.get(origin, "email"), - allow_persistent: options.allow_persistent || false, - remember: storage.site.get(origin, "remember") || false, privacy_url: options.privacyURL, tos_url: options.tosURL }); diff --git a/resources/static/dialog/controllers/provision_primary_user.js b/resources/static/dialog/controllers/provision_primary_user.js index 700e113806ce6baa8846a70cc3e3d71ff94c5ad9..367195e4a0191f0054288f801d04534d4362a014 100644 --- a/resources/static/dialog/controllers/provision_primary_user.js +++ b/resources/static/dialog/controllers/provision_primary_user.js @@ -6,8 +6,7 @@ BrowserID.Modules.ProvisionPrimaryUser = (function() { "use strict"; - var ANIMATION_TIME = 250, - bid = BrowserID, + var bid = BrowserID, user = bid.User, errors = bid.Errors; diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css index 08b24fe3105acd15ac6dc8915ee5e52af9e22fd4..f3e6dc32525b26460c9cf42e990e374fc336b81d 100644 --- a/resources/static/dialog/css/m.css +++ b/resources/static/dialog/css/m.css @@ -139,10 +139,8 @@ margin-bottom: 20px; } - label[for=remember] { - display: block; - font-size: 15px; - margin-bottom: 25px; + .form_section { + margin-top: 20px; } #content, .form_section, .inputs, .vertical { diff --git a/resources/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css index d3765789e63f85d99d05c94960e50bbb8b3a7c33..c82f4b8246e05d488f068f2a1cb54027467d821c 100644 --- a/resources/static/dialog/css/popup.css +++ b/resources/static/dialog/css/popup.css @@ -372,11 +372,6 @@ footer { display: none; } -label[for=remember] { - display: inline-block; - margin-bottom: 10px; -} - a.emphasize { background-color: #F0EFED; color: #4E4E4E; @@ -407,3 +402,24 @@ a.emphasize { #checkemail { text-align: center; } + +#your_computer_content { + width: 90%; + margin: auto; + text-align: left; +} + +#your_computer_content p button { + float: left; + margin: 0 1em 0 0; + vertical-align: middle; + font-size: 1em; + width: 4em; +} + +#your_computer_content p { + padding-bottom: 1em; + line-height: 1.3em; + margin-top: 2em; + margin-bottom: 2em; +} \ No newline at end of file diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js index 50db742a114d042af9e667e4ecd9329e7fe1beb0..12029173c581474065a9f0cf4a895003673930cf 100644 --- a/resources/static/dialog/resources/helpers.js +++ b/resources/static/dialog/resources/helpers.js @@ -39,13 +39,11 @@ user.getAssertion(email, user.getOrigin(), function(assert) { assert = assert || null; wait.hide(); - animateClose(function() { - self.close("assertion_generated", { - assertion: assert - }); - - complete(callback, assert); + self.close("assertion_generated", { + assertion: assert }); + + complete(callback, assert); }, self.getErrorDialog(errors.getAssertion, complete)); } @@ -129,7 +127,8 @@ createUser: createUser, addEmail: addEmail, resetPassword: resetPassword, - cancelEvent: helpers.cancelEvent + cancelEvent: helpers.cancelEvent, + animateClose: animateClose }); }()); diff --git a/resources/static/dialog/resources/internal_api.js b/resources/static/dialog/resources/internal_api.js index 70d81219a29c5a626d17fa7f18bbfde5dea62678..6285f8cb788357cf7ac11f80b46b76d82f309840 100644 --- a/resources/static/dialog/resources/internal_api.js +++ b/resources/static/dialog/resources/internal_api.js @@ -27,7 +27,7 @@ } user.checkAuthentication(function onComplete(authenticated) { - if(authenticated) { + if (authenticated) { storage.site.set(origin, "remember", true); } diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js index a955170a02bc17f24994a7dc9b72602efaecdc95..6dfa32485ce929e54ebfe992a0efdc95b4b0af00 100644 --- a/resources/static/dialog/resources/state.js +++ b/resources/static/dialog/resources/state.js @@ -6,9 +6,9 @@ BrowserID.State = (function() { var bid = BrowserID, storage = bid.Storage, + network = bid.Network, mediator = bid.Mediator, helpers = bid.Helpers, - publish = mediator.publish.bind(mediator), user = bid.User, moduleManager = bid.module, complete = bid.Helpers.complete, @@ -20,8 +20,9 @@ BrowserID.State = (function() { function startStateMachine() { var self = this, - subscribe = self.subscribe.bind(self), - startState = function(save, msg, options) { + handleState = self.subscribe.bind(self), + redirectToState = mediator.publish.bind(mediator), + startAction = function(save, msg, options) { if(typeof save !== "boolean") { options = msg; msg = save; @@ -33,96 +34,97 @@ BrowserID.State = (function() { }, cancelState = self.popState.bind(self); - subscribe("start", function(msg, info) { + handleState("start", function(msg, info) { info = info || {}; self.hostname = info.hostname; - self.allowPersistent = !!info.allowPersistent; self.privacyURL = info.privacyURL; self.tosURL = info.tosURL; requiredEmail = info.requiredEmail; if ((typeof(requiredEmail) !== "undefined") && (!bid.verifyEmail(requiredEmail))) { // Invalid format - startState("doError", "invalid_required_email", {email: requiredEmail}); + startAction("doError", "invalid_required_email", {email: requiredEmail}); } else if(info.email && info.type === "primary") { primaryVerificationInfo = info; - publish("primary_user", info); + redirectToState("primary_user", info); } else { - startState("doCheckAuth"); + startAction("doCheckAuth"); } }); - subscribe("cancel", function() { - startState("doCancel"); + handleState("cancel", function() { + startAction("doCancel"); }); - subscribe("window_unload", function() { + handleState("window_unload", function() { if (!self.success) { - bid.Storage.setStagedOnBehalfOf(""); - startState("doCancel"); + storage.setStagedOnBehalfOf(""); + startAction("doCancel"); } }); - subscribe("authentication_checked", function(msg, info) { + handleState("authentication_checked", function(msg, info) { var authenticated = info.authenticated; if (requiredEmail) { - startState("doAuthenticateWithRequiredEmail", { + self.email = requiredEmail; + startAction("doAuthenticateWithRequiredEmail", { email: requiredEmail, privacyURL: self.privacyURL, tosURL: self.tosURL }); } else if (authenticated) { - publish("pick_email"); + redirectToState("pick_email"); } else { - publish("authenticate"); + redirectToState("authenticate"); } }); - subscribe("authenticate", function(msg, info) { + handleState("authenticate", function(msg, info) { info = info || {}; info.privacyURL = self.privacyURL; info.tosURL = self.tosURL; - startState("doAuthenticate", info); + startAction("doAuthenticate", info); }); - subscribe("user_staged", function(msg, info) { + handleState("user_staged", function(msg, info) { self.stagedEmail = info.email; info.required = !!requiredEmail; - startState("doConfirmUser", info); + startAction("doConfirmUser", info); }); - subscribe("user_confirmed", function() { - startState("doEmailConfirmed", { email: self.stagedEmail} ); + handleState("user_confirmed", function() { + self.email = self.stagedEmail; + startAction("doEmailConfirmed", { email: self.stagedEmail} ); }); - subscribe("primary_user", function(msg, info) { + handleState("primary_user", function(msg, info) { addPrimaryUser = !!info.add; email = info.email; var idInfo = storage.getEmail(email); if(idInfo && idInfo.cert) { - publish("primary_user_ready", info); + redirectToState("primary_user_ready", info); } else { // We don't want to put the provisioning step on the stack, instead when // a user cancels this step, they should go back to the step before the // provisioning. - startState(false, "doProvisionPrimaryUser", info); + startAction(false, "doProvisionPrimaryUser", info); } }); - subscribe("primary_user_provisioned", function(msg, info) { + handleState("primary_user_provisioned", function(msg, info) { info = info || {}; info.add = !!addPrimaryUser; - startState("doPrimaryUserProvisioned", info); + startAction("doPrimaryUserProvisioned", info); }); - subscribe("primary_user_unauthenticated", function(msg, info) { + handleState("primary_user_unauthenticated", function(msg, info) { info = helpers.extend(info || {}, { add: !!addPrimaryUser, email: email, @@ -134,49 +136,50 @@ BrowserID.State = (function() { if(primaryVerificationInfo) { primaryVerificationInfo = null; if(requiredEmail) { - startState("doCannotVerifyRequiredPrimary", info); + startAction("doCannotVerifyRequiredPrimary", info); } else if(info.add) { // Add the pick_email in case the user cancels the add_email screen. // The user needs something to go "back" to. - publish("pick_email"); - publish("add_email", info); + redirectToState("pick_email"); + redirectToState("add_email", info); } else { - publish("authenticate", info); + redirectToState("authenticate", info); } } else { - startState("doVerifyPrimaryUser", info); + startAction("doVerifyPrimaryUser", info); } }); - subscribe("primary_user_authenticating", function(msg, info) { + handleState("primary_user_authenticating", function(msg, info) { // Keep the dialog from automatically closing when the user browses to // the IdP for verification. moduleManager.stopAll(); self.success = true; }); - subscribe("primary_user_ready", function(msg, info) { - startState("doEmailChosen", info); + handleState("primary_user_ready", function(msg, info) { + redirectToState("email_chosen", info); }); - subscribe("pick_email", function() { - startState("doPickEmail", { + handleState("pick_email", function() { + startAction("doPickEmail", { origin: self.hostname, - allow_persistent: self.allowPersistent, privacyURL: self.privacyURL, tosURL: self.tosURL }); }); - subscribe("email_chosen", function(msg, info) { + handleState("email_chosen", function(msg, info) { info = info || {}; var email = info.email, idInfo = storage.getEmail(email); + self.email = email; + function oncomplete() { complete(info.complete); } @@ -184,23 +187,22 @@ BrowserID.State = (function() { if(idInfo) { if(idInfo.type === "primary") { if(idInfo.cert) { - startState("doEmailChosen", info); + startAction("doEmailChosen", info); } else { // If the email is a primary, and their cert is not available, // throw the user down the primary flow. // Doing so will catch cases where the primary certificate is expired - // and the user must re-verify with their IdP. This flow will - // generate its own assertion when ready. - publish("primary_user", info); + // and the user must re-verify with their IdP. + redirectToState("primary_user", info); } } else { user.checkAuthentication(function(authentication) { if(authentication === "assertion") { - // user not authenticated, kick them over to the required email - // screen. - startState("doAuthenticateWithRequiredEmail", { + // user must authenticate with their password, kick them over to + // the required email screen to enter the password. + startAction("doAuthenticateWithRequiredEmail", { email: email, secondary_auth: true, privacyURL: self.privacyURL, @@ -208,7 +210,7 @@ BrowserID.State = (function() { }); } else { - startState("doEmailChosen", info); + startAction("doEmailChosen", info); } oncomplete(); }, oncomplete); @@ -219,59 +221,79 @@ BrowserID.State = (function() { } }); - subscribe("notme", function() { - startState("doNotMe"); + handleState("notme", function() { + startAction("doNotMe"); }); - subscribe("logged_out", function() { - publish("authenticate"); + handleState("logged_out", function() { + redirectToState("authenticate"); }); - subscribe("authenticated", function(msg, info) { - publish("email_chosen", info); + handleState("authenticated", function(msg, info) { + redirectToState("email_chosen", info); }); - subscribe("forgot_password", function(msg, info) { + handleState("forgot_password", function(msg, info) { // forgot password initiates the forgotten password flow. - startState(false, "doForgotPassword", info); + startAction(false, "doForgotPassword", info); }); - subscribe("reset_password", function(msg, info) { + handleState("reset_password", function(msg, info) { // reset password says the password has been reset, now waiting for // confirmation. - startState(false, "doResetPassword", info); + startAction(false, "doResetPassword", info); }); - subscribe("assertion_generated", function(msg, info) { + handleState("assertion_generated", function(msg, info) { self.success = true; if (info.assertion !== null) { - startState("doAssertionGenerated", info.assertion); + if (storage.usersComputer.shouldAsk(network.userid())) { + // We have to confirm the user's status + self.assertion_info = info; + redirectToState("is_this_your_computer", info); + } + else { + storage.setLoggedIn(user.getOrigin(), self.email); + startAction("doAssertionGenerated", { assertion: info.assertion, email: self.email }); + } } else { - publish("pick_email"); + redirectToState("pick_email"); } }); - subscribe("add_email", function(msg, info) { + handleState("is_this_your_computer", function(msg, info) { + startAction("doIsThisYourComputer", info); + }); + + handleState("user_computer_status_set", function(msg, info) { + // User's status has been confirmed, redirect them back to the + // assertion_generated state with the stored assertion_info + var assertion_info = self.assertion_info; + self.assertion_info = null; + redirectToState("assertion_generated", assertion_info); + }); + + handleState("add_email", function(msg, info) { info = helpers.extend(info || {}, { privacyURL: self.privacyURL, tosURL: self.tosURL }); - startState("doAddEmail", info); + startAction("doAddEmail", info); }); - subscribe("email_staged", function(msg, info) { + handleState("email_staged", function(msg, info) { self.stagedEmail = info.email; info.required = !!requiredEmail; - startState("doConfirmEmail", info); + startAction("doConfirmEmail", info); }); - subscribe("email_confirmed", function() { - startState("doEmailConfirmed", { email: self.stagedEmail} ); + handleState("email_confirmed", function() { + startAction("doEmailConfirmed", { email: self.stagedEmail} ); }); - subscribe("cancel_state", function(msg, info) { + handleState("cancel_state", function(msg, info) { cancelState(info); }); diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js index 8768ea7acd0e2a85d013d2175bb3d189a11ac534..970034d1afe9fea098c09969146267db1c8ac527 100644 --- a/resources/static/dialog/start.js +++ b/resources/static/dialog/start.js @@ -24,6 +24,7 @@ moduleManager.register("authenticate", modules.Authenticate); moduleManager.register("check_registration", modules.CheckRegistration); moduleManager.register("forgot_password", modules.ForgotPassword); + moduleManager.register("is_this_your_computer", modules.IsThisYourComputer); moduleManager.register("pick_email", modules.PickEmail); moduleManager.register("required_email", modules.RequiredEmail); moduleManager.register("verify_primary_user", modules.VerifyPrimaryUser); @@ -33,7 +34,6 @@ moduleManager.register("xhr_delay", modules.XHRDelay); moduleManager.register("xhr_disable_form", modules.XHRDisableForm); - moduleManager.start("xhr_delay"); moduleManager.start("xhr_disable_form"); moduleManager.start("dialog"); diff --git a/resources/static/dialog/views/is_this_your_computer.ejs b/resources/static/dialog/views/is_this_your_computer.ejs new file mode 100644 index 0000000000000000000000000000000000000000..2ab5bd1789788cf4abb72f2860898db25bd4982c --- /dev/null +++ b/resources/static/dialog/views/is_this_your_computer.ejs @@ -0,0 +1,20 @@ +<% /* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. */ %> + + <div id="your_computer_content"> + <h2><%= gettext('If you don\'t mind me asking, is this your computer?') %></h2> + + <p> + <button class="this_is_my_computer" tabindex="3"><%= gettext('yes') %></button> + <%= gettext('If so, we\'ll keep you logged in for a couple weeks.') %> + </p> + + <p> + <button class="this_is_not_my_computer negative" tabindex="3"><%= gettext('no') %></button> + <%= gettext('If you\'re at a public computer such as a library or internet cafe, we\'ll ask you ' + + 'for your password again in an hour.') %> + </p> + </div> + + diff --git a/resources/static/dialog/views/pick_email.ejs b/resources/static/dialog/views/pick_email.ejs index be2fdd3fae2d01aadfb975af222eeedfaa79154d..a9dbdae0af95ae247065e29a8b318bfa83185357 100644 --- a/resources/static/dialog/views/pick_email.ejs +++ b/resources/static/dialog/views/pick_email.ejs @@ -23,28 +23,18 @@ <div class="submit add cf"> - <% if (allow_persistent) { %> - <label for="remember" class="selectable"> - <input type="checkbox" id="remember" name="remember" <% if (remember) { %> checked="checked" <% } %> /> - <%= gettext('Always sign in using this email') %> - </label> - <% } %> - - <% if (privacy_url && tos_url) { %> - <p> -<%= format( - gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), - [ gettext('sign in'), - format(' href="%s" target="_new"', [tos_url]), - format(' href="%s" target="_new"', [privacy_url]) - ]) %> - </p> - <p> - <% } %> - <button id="signInButton"><%= gettext('sign in') %></button> <% if (privacy_url && tos_url) { %> + <p class="tospp"> + <%= format( + gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), + [ gettext('sign in'), + format(' href="%s" target="_new"', [tos_url]), + format(' href="%s" target="_new"', [privacy_url]) + ]) %> </p> <% } %> + + <button id="signInButton"><%= gettext('sign in') %></button> + <br style="clear: both" /> </div> </div> - diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js index 89fef47ee479e6352bff774465fe3fb99b370beb..01a67fe39fd6e13c5c74aa6ea2997deb292bd5ab 100644 --- a/resources/static/include_js/include.js +++ b/resources/static/include_js/include.js @@ -918,26 +918,11 @@ }; }()); - - // this is for calls that are non-interactive - function _open_hidden_iframe(doc) { - var iframe = doc.createElement("iframe"); - iframe.style.display = "none"; - doc.body.appendChild(iframe); - iframe.src = ipServer + "/communication_iframe"; - return iframe; - } - - /** - * The meat and potatoes of the verified email protocol - */ - - if (!navigator.id) { navigator.id = {}; } - if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed) { + if (!navigator.id.request || navigator.id._shimmed) { var ipServer = "https://browserid.org"; var userAgent = navigator.userAgent; // We must check for both XUL and Java versions of Fennec. Both have @@ -951,105 +936,223 @@ var w; - navigator.id.get = function(callback, options) { - if (typeof callback !== 'function') { - throw "navigator.id.get() requires a callback argument"; + // table of registered event listeners + var listeners = { + login: [ ], + logout: [ ], + loginCanceled: [ ] + }; + + var compatMode = undefined; + function checkCompat(requiredMode) { + if (requiredMode === true) { + try { console.log("this site uses deprecated APIs (see documentation for navigator.id.request())"); } catch(e) { } } - if (options && options.silent) { - _noninteractiveCall('getPersistentAssertion', { }, function(rv) { - callback(rv); - }, function(e, msg) { - callback(null); - }); - } else { - // focus an existing window - if (w) { - try { - w.focus(); + if (compatMode === undefined) compatMode = requiredMode; + else if (compatMode != requiredMode) { + throw "you cannot combine browserid event APIs with navigator.id.getVerifiedEmail() or navigator.id.get()" + + "this site should instead use navigator.id.request() and the browserid event API"; + } + } + + function emitEvent(type, params) { + if (listeners[type]) { + var evt = document.createEvent('Event'); + evt.initEvent(type, true, true); + // XXX: we should probably implement .stopImmediatePropagation() + if (params) { + for (var k in params) { + if (params.hasOwnProperty(k)) { + evt[k] = params[k]; + } } - catch(e) { - /* IE7 blows up here, do nothing */ + } + for (var i = 0; i < listeners[type].length; i++) { + try { + listeners[type][i](evt); + } catch(e) { + // XXX: what shall we do when an exception is raised by an event handler? } - return; } + } + } - if (!BrowserSupport.isSupported()) { - var reason = BrowserSupport.getNoSupportReason(), - url = "unsupported_dialog"; - - if(reason === "LOCALSTORAGE_DISABLED") { - url = "cookies_disabled"; + var commChan; + + // this is for calls that are non-interactive + function _open_hidden_iframe() { + if (!commChan) { + var doc = window.document; + var iframe = doc.createElement("iframe"); + iframe.style.display = "none"; + doc.body.appendChild(iframe); + iframe.src = ipServer + "/communication_iframe"; + commChan = Channel.build({ + window: iframe.contentWindow, + origin: ipServer, + scope: "mozid_ni", + onReady: function() { + // once the channel is set up, we'll fire a loaded message. this is the + // cutoff point where we'll say if 'setLoggedInUser' was not called before + // this point, then it wont be called (XXX: optimize and improve me) + commChan.call({ method: 'loaded', success: function(){}, error: function() {} }); } + }); - w = window.open( - ipServer + "/" + url, - null, - windowOpenOpts); - return; - } + commChan.bind('logout', function(trans, params) { + emitEvent('logout'); + }); - w = WinChan.open({ - url: ipServer + '/sign_in', - relay_url: ipServer + '/relay', - window_features: windowOpenOpts, - params: { - method: "get", - params: options - } - }, function(err, r) { - // clear the window handle - w = undefined; - // ignore err! - callback(err ? null : (r ? r : null)); + commChan.bind('login', function(trans, params) { + emitEvent('login', { assertion: params }); }); } + } + + function internalAddEventListener(type, listener) { + // add event to listeners table if it's not there already + if (!listeners[type]) throw "unsupported event type: '" + type + "'"; + + // is the function already registered? + for (var i = 0; i < listeners[type].length; i++) { + if (listeners[type][i] === listener) return; + } + listeners[type].push(listener); + } + + navigator.id.addEventListener = function(type, listener) { + checkCompat(false); + + // allocate iframe if it is not allocated + _open_hidden_iframe(); + internalAddEventListener(type,listener); + }; + + function internalRemoveEventListener(type, listener ) { + // remove event from listeners table + var i; + for (i = 0; i < listeners[type].length; i++) { + if (listeners[type][i] === listener) break; + } + if (i < listeners[type][i].length) { + listeners[type].splice(i, 1); + } + } + + navigator.id.removeEventListener = function(type, listener/*, useCapture */) { + checkCompat(false); + internalRemoveEventListener(type, listener); }; - navigator.id.getVerifiedEmail = function (callback, options) { - if (options) { - throw "getVerifiedEmail doesn't accept options. use navigator.id.get() instead."; + navigator.id.logout = function() { + checkCompat(false); + + // allocate iframe if it is not allocated + _open_hidden_iframe(); + + // send logout message + commChan.notify({ method: 'logout' }); + }; + + navigator.id.setLoggedInUser = function(email) { + checkCompat(false); + + // 1. allocate iframe if it is not allocated + _open_hidden_iframe(); + + // 2. send a "loggedInUser" message to iframe + commChan.notify({ method: 'loggedInUser', params: email }); + }; + + // backwards compatibility function + navigator.id.get = function(callback, options) { + checkCompat(true); + + if (options && options.silent) { + if (callback) setTimeout(function() { callback(null); }, 0); + } else { + function handleEvent(e) { + internalRemoveEventListener('login', handleEvent); + callback((e && e.assertion) ? e.assertion : null); + } + internalAddEventListener('login', handleEvent); + internalRequest(options); } + }; + + // backwards compatibility function + navigator.id.getVerifiedEmail = function(callback) { + checkCompat(true); navigator.id.get(callback); }; - navigator.id.logout = function(callback) { - _noninteractiveCall('logout', { }, function(rv) { - callback(rv); - }, function() { - callback(null); - }); + navigator.id.request = function(options) { + checkCompat(false); + return internalRequest(options); }; - var _noninteractiveCall = function(method, args, onsuccess, onerror) { - var doc = window.document; - var ni_iframe = _open_hidden_iframe(doc); + function internalRequest(options) { + // focus an existing window + if (w) { + try { + w.focus(); + } + catch(e) { + /* IE7 blows up here, do nothing */ + } + return; + } + + if (!BrowserSupport.isSupported()) { + var reason = BrowserSupport.getNoSupportReason(), + url = "unsupported_dialog"; - var chan = Channel.build({window: ni_iframe.contentWindow, origin: ipServer, scope: "mozid_ni"}); + if(reason === "LOCALSTORAGE_DISABLED") { + url = "cookies_disabled"; + } - function cleanup() { - chan.destroy(); - chan = undefined; - doc.body.removeChild(ni_iframe); + w = window.open( + ipServer + "/" + url, + null, + windowOpenOpts); + return; } - chan.call({ - method: method, - params: args, - success: function(rv) { - if (onsuccess) { - onsuccess(rv); + // notify the iframe that the dialog is running so we + // don't do duplicative work + if (commChan) commChan.notify({ method: 'dialog_running' }); + + w = WinChan.open({ + url: ipServer + '/sign_in', + relay_url: ipServer + '/relay', + window_features: windowOpenOpts, + params: { + method: "get", + params: options + } + }, function(err, r) { + // unpause the iframe to detect future changes in login state + if (commChan) { + // update the loggedInUser in the case that an assertion was generated, as + // this will prevent the comm iframe from thinking that state has changed + // and generating a new assertion. IF, however, this request is not a success, + // then we do not change the loggedInUser - and we will let the comm frame determine + // if generating a logout event is the right thing to do + if (!err && r && r.email) { + commChan.notify({ method: 'loggedInUser', params: r.email }); } - cleanup(); - }, - error: function(code, msg) { - if (onerror) onerror(code, msg); - cleanup(); + commChan.notify({ method: 'dialog_complete' }); } + + // clear the window handle + w = undefined; + if (!err && r && r.assertion) emitEvent('login', { assertion: r.assertion }); + else emitEvent('loginCanceled'); }); }; - navigator.id._getVerifiedEmailIsShimmed = true; + navigator.id._shimmed = true; } }()); diff --git a/resources/static/pages/manage_account.js b/resources/static/pages/manage_account.js index 9bb92ea310d2570617ea49a45a78dcb5aa8d947d..59bc9ccc3154516c4956f224e02b4e13ecff9938 100644 --- a/resources/static/pages/manage_account.js +++ b/resources/static/pages/manage_account.js @@ -96,6 +96,11 @@ BrowserID.manageAccount = (function() { } } + function logoutEverywhere(oncomplete) { + storage.logoutEverywhere(); + setTimeout(oncomplete, 0); + } + function startEdit(event) { // XXX add some helpers in the dom library to find section. event.preventDefault(); @@ -194,6 +199,13 @@ BrowserID.manageAccount = (function() { dom.bindEvent("button.edit", "click", startEdit); dom.bindEvent("button.done", "click", cancelEdit); + dom.bindEvent("button.logout_everywhere", "click", function() { + logoutEverywhere(function() { + $("button.logout_everywhere").fadeOut(700, function() { + $("#logout_everywhere .completion_text").show(); + }); + }); + }); dom.bindEvent("#edit_password_form", "submit", cancelEvent(changePassword)); user.checkAuthentication(function(auth_level) { diff --git a/resources/static/shared/browserid.js b/resources/static/shared/browserid.js index a129416825e0e7dce113a463ab03d41ea04d8e00..34481fb5c7a0a74fff17cce319daf5134d49d546 100644 --- a/resources/static/shared/browserid.js +++ b/resources/static/shared/browserid.js @@ -7,5 +7,9 @@ window.BrowserID = window.BrowserID || {}; - + // Define some constants. + _.extend(window.BrowserID, { + // always use 1024 DSA keys - see issue #1293 + KEY_LENGTH: 128 + }); }()); diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index a91dc475e0885c4b463f635d858bfb7f5f32e6c5..7e8834285df71ed5a38f36308dc85ce882f3dd5a 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -13,11 +13,25 @@ BrowserID.Network = (function() { domain_key_creation_time, auth_status, code_version, + userid, time_until_delay, mediator = bid.Mediator, xhr = bid.XHR, post = xhr.post, - get = xhr.get; + get = xhr.get, + storage = bid.Storage; + + function setUserID(uid) { + userid = uid; + + // TODO - Get this out of here and put it into user! + + // when session context returns with an authenticated user, update localstorage + // to indicate we've seen this user on this device + if (userid) { + storage.usersComputer.setSeen(userid); + } + } function onContextChange(msg, result) { context = result; @@ -28,6 +42,7 @@ BrowserID.Network = (function() { domain_key_creation_time = result.domain_key_creation_time; auth_status = result.auth_level; code_version = result.code_version; + setUserID(result.userid); // seed the PRNG // FIXME: properly abstract this out, probably by exposing a jwcrypto @@ -54,6 +69,11 @@ BrowserID.Network = (function() { if (typeof authenticated !== 'boolean') throw status; + // now update the userid which is set once the user is authenticated. + // this is used to key off client side state, like whether this user has + // confirmed ownership of this device + setUserID(status.userid); + // at this point we know the authentication status of the // session, let's set it to perhaps save a network request // (to fetch session context). @@ -90,7 +110,8 @@ BrowserID.Network = (function() { url: "/wsapi/authenticate_user", data: { email: email, - pass: password + pass: password, + ephemeral: !storage.usersComputer.confirmed(email) }, success: handleAuthenticationResponse.curry("password", onComplete, onFailure), error: onFailure @@ -111,7 +132,8 @@ BrowserID.Network = (function() { url: "/wsapi/auth_with_assertion", data: { email: email, - assertion: assertion + assertion: assertion, + ephemeral: !storage.usersComputer.confirmed(email) }, success: handleAuthenticationResponse.curry("assertion", onComplete, onFailure), error: onFailure @@ -135,6 +157,14 @@ BrowserID.Network = (function() { }, onFailure); }, + /** + * clear local cache, including authentication status and + * other session data. + * + * @method clearContext + */ + clearContext: clearContext, + /** * Log the authenticated user out * @method logout @@ -151,6 +181,7 @@ BrowserID.Network = (function() { // FIXME: we should return a confirmation that the // user was successfully logged out. auth_status = false; + setUserID(undefined); complete(onComplete); }, error: function(info, xhr, textStatus) { @@ -487,7 +518,8 @@ BrowserID.Network = (function() { url: "/wsapi/cert_key", data: { email: email, - pubkey: pubkey.serialize() + pubkey: pubkey.serialize(), + ephemeral: !storage.usersComputer.confirmed(email) }, success: onComplete, error: onFailure @@ -501,11 +533,30 @@ BrowserID.Network = (function() { listEmails: function(onComplete, onFailure) { get({ url: "/wsapi/list_emails", - success: onComplete, + success: function(emails) { + // TODO - Put this into user.js or storage.js when emails are synced/saved to + // storage. + // update our local storage map of email addresses to user ids + if (userid) { + storage.updateEmailToUserIDMapping(userid, _.keys(emails)); + } + + onComplete && onComplete(emails); + }, error: onFailure }); }, + /** + * Return the user's userid, which will an integer if the user + * is authenticated, undefined otherwise. + * + * @method userid + */ + userid: function() { + return userid; + }, + /** * Get the current time on the server in the form of a * date object. diff --git a/resources/static/shared/provisioning.js b/resources/static/shared/provisioning.js index 5c90de51a6143241c73c611a8a5a16d5cd252886..4eba6b1561caf66f590a4b528f72de6335344439 100644 --- a/resources/static/shared/provisioning.js +++ b/resources/static/shared/provisioning.js @@ -28,7 +28,7 @@ BrowserID.Provisioning = (function() { if (!failureCB) throw "missing required failure callback"; - if (!args || !args.email || !args.url) { + if (!args || !args.email || !args.url || !args.hasOwnProperty('ephemeral')) { return fail('internal', 'missing required arguments'); } @@ -61,21 +61,15 @@ BrowserID.Provisioning = (function() { chan.bind('beginProvisioning', function(trans, s) { return { email: args.email, - // XXX: certificate duration should vary depending on a variety of factors: - // * user is on a device that is not her own - // * user is in an environment that can't handle the crypto - cert_duration_s: (6 * 60 * 60) + // XXX: {non,}ephemeral auth duration should be stored somewhere central and + // should be common between primary and secondary cert provisioning. Because + // the latter occurs on the server, it should probably be sent session_context. + cert_duration_s: ((args.ephemeral === false) ? (6 * 60 * 60) : (60 * 60)) }; }); chan.bind('genKeyPair', function(trans, s) { - // this will take a little bit - // FIXME: refactor so code that makes this decision is shared. - var keysize = 256; - var ie_version = BrowserID.BrowserSupport.getInternetExplorerVersion(); - if (ie_version > -1 && ie_version < 9) - keysize = 128; - keypair = jwk.KeyPair.generate("DS", keysize); + keypair = jwk.KeyPair.generate("DS", BrowserID.KEY_LENGTH); return keypair.publicKey.toSimpleObject(); }); diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js index 40288033af84e75d874a8fc4f677bd31b1649d8a..f395fcb1856575d3a4e923ee4b8471b855476923 100644 --- a/resources/static/shared/storage.js +++ b/resources/static/shared/storage.js @@ -5,7 +5,8 @@ BrowserID.Storage = (function() { var jwk, - storage = localStorage; + storage = localStorage, + ONE_DAY_IN_MS = (1000 * 60 * 60 * 24); function prepareDeps() { if (!jwk) { @@ -159,7 +160,6 @@ BrowserID.Storage = (function() { } } - function managePageGet(key) { var allInfo = JSON.parse(storage.managePage || "{}"); return allInfo[key]; @@ -176,6 +176,190 @@ BrowserID.Storage = (function() { delete allInfo[key]; storage.managePage = JSON.stringify(allInfo); } + + function setLoggedIn(origin, email) { + var allInfo = JSON.parse(storage.loggedIn || "{}"); + if (email) allInfo[origin] = email; + else delete allInfo[origin]; + storage.loggedIn = JSON.stringify(allInfo); + } + + function getLoggedIn(origin) { + var allInfo = JSON.parse(storage.loggedIn || "{}"); + return allInfo[origin]; + } + + function watchLoggedIn(origin, callback) { + var lastState = getLoggedIn(origin); + + function checkState() { + var currentState = getLoggedIn(origin); + if (lastState !== currentState) { + callback(); + lastState = currentState; + }; + } + + // IE8 does not have addEventListener, nor does it support storage events. + if (window.addEventListener) window.addEventListener('storage', checkState, false); + else window.setInterval(checkState, 2000); + } + function logoutEverywhere() { + storage.loggedIn = "{}"; + } + + function mapEmailToUserID(emailOrUserID) { + if (typeof(emailOrUserID) === 'number') return emailOrUserID; + var allInfo = JSON.parse(storage.emailToUserID || "{}"); + return allInfo[emailOrUserID]; + } + + // tools to manage knowledge of whether this is the user's computer, + // which helps us set appropriate authentication duration. + function validState(state) { + return (state === 'seen' || state === 'confirmed' || state === 'denied'); + } + + function setConfirmationState(userid, state) { + userid = mapEmailToUserID(userid); + + if (typeof userid !== 'number') throw 'bad userid ' + userid; + + if (!validState(state)) throw "invalid state"; + + var allInfo; + var currentState; + var lastUpdated = 0; + + try { + allInfo = JSON.parse(storage.usersComputer); + if (typeof allInfo !== 'object') throw 'bogus'; + + var userInfo = allInfo[userid]; + if (userInfo) { + currentState = userInfo.state; + lastUpdated = Date.parse(userInfo.updated); + + if (!validState(currentState)) throw "corrupt/outdated"; + if (NaN === lastUpdated) throw "corrupt/outdated"; + } + } catch(e) { + currentState = undefined; + lastUpdated = 0; + allInfo = {}; + } + + // ...now determine if we should update the state... + + // first if the user said this wasn't their computer over 24 hours ago, + // forget that setting (we will revisit this) + if (currentState === 'denied' && + ((new Date()).getTime() - lastUpdated) > ONE_DAY_IN_MS) { + currentState = undefined; + lastUpdated = 0; + } + + // if the user has a non-null state and this is another user sighting + // (seen), then forget it + if (state === 'seen' && currentState) return; + + // good to go! let's make the update + allInfo[userid] = {state: state, updated: new Date().toString()}; + storage.usersComputer = JSON.stringify(allInfo); + } + + function userConfirmedOnComputer(userid) { + try { + userid = mapEmailToUserID(userid); + var allInfo = JSON.parse(storage.usersComputer || "{}"); + return allInfo[userid].state === 'confirmed'; + } catch(e) { + return false; + } + } + + function shouldAskUserAboutHerComputer(userid) { + // we should ask the user if this is their computer if they were + // first seen over a minute ago, if they haven't denied ownership + // of this computer in the last 24 hours, and they haven't confirmed + // ownership of this computer + try { + userid = mapEmailToUserID(userid); + var allInfo = JSON.parse(storage.usersComputer); + var userInfo = allInfo[userid]; + if(userInfo) { + var s = userInfo.state; + var timeago = new Date() - Date.parse(userInfo.updated); + + // The ask state is an artificial state that should never be seen in + // the wild. It is used in testing. + if (s === 'ask') return true; + if (s === 'confirmed') return false; + if (s === 'denied' && timeago > ONE_DAY_IN_MS) return true; + if (s === 'seen' && timeago > (60 * 1000)) return true; + } + } catch (e) { + return true; + } + + return false; + } + + function setUserSeenOnComputer(userid) { + setConfirmationState(userid, 'seen'); + } + + function setUserConfirmedOnComputer(userid) { + setConfirmationState(userid, 'confirmed'); + } + + function setNotMyComputer(userid) { + setConfirmationState(userid, 'denied'); + } + + function setUserMustConfirmComputer(userid) { + try { + userid = mapEmailToUserID(userid); + var allInfo = JSON.parse(storage.usersComputer); + if (typeof allInfo !== 'object') throw 'bogus'; + + var userInfo = allInfo[userid] || {}; + userInfo.state = 'ask'; + storage.usersComputer = JSON.stringify(allInfo); + } catch(e) {} + } + + function clearUsersComputerOwnershipStatus(userid) { + try { + allInfo = JSON.parse(storage.usersComputer); + if (typeof allInfo !== 'object') throw 'bogus'; + + var userInfo = allInfo[userid]; + if (userInfo) { + allInfo[userid] = null; + delete allInfo[userid]; + storage.usersComputer = JSON.stringify(allInfo); + } + } catch (e) {} + } + + // update our local storage based mapping of email addresses to userids, + // this map helps us determine whether a specific email address belongs + // to a user who has already confirmed their ownership of a computer. + function updateEmailToUserIDMapping(userid, emails) { + var allInfo; + try { + allInfo = JSON.parse(storage.emailToUserID); + if (typeof allInfo != 'object' || allInfo === null) throw "bogus"; + } catch(e) { + allInfo = {}; + } + _.each(emails, function(email) { + allInfo[email] = userid; + }); + storage.emailToUserID = JSON.stringify(allInfo); + } + return { /** * Add an email address and optional key pair. @@ -242,6 +426,80 @@ BrowserID.Storage = (function() { remove: managePageRemove }, + usersComputer: { + /** + * Query whether the user has confirmed that this is their computer + * @param {integer} userid - the user's numeric id, returned from session_context when authed. + * @method usersComputer.confirmed */ + confirmed: userConfirmedOnComputer, + /** + * Save the fact that a user confirmed that this is their computer + * @param {integer} userid - the user's numeric id, returned from session_context when authed. + * @method usersComputer.setConfirmed */ + setConfirmed: setUserConfirmedOnComputer, + /** + * Save the fact that a user denied that this is their computer + * @param {integer} userid - the user's numeric id, returned from session_context when authed. + * @method usersComputer.setDenied */ + setDenied: setNotMyComputer, + /** + * Should we ask the user if this is their computer, based on the last + * time they used browserid and the last time they answered a question + * about this device + * @param {integer} userid - the user's numeric id, returned + * from session_context when authed. + * @method usersComputer.seen */ + shouldAsk: shouldAskUserAboutHerComputer, + /** + * Save the fact that a user has been seen on this computer before, but do not overwrite + * existing state + * @param {integer} userid - the user's numeric id, returned from session_context when authed. + * @method usersComputer.setSeen */ + setSeen: setUserSeenOnComputer, + /** + * Clear the status for the user + * @param {integer} userid - the user's numeric id, returned from session_context when authed. + * @method usersComputer.clear */ + clear: clearUsersComputerOwnershipStatus, + /** + * Force the user to be asked their status + * @param {integer} userid - the user's numeric id, returned from session_context when authed. + * @method usersComputer.forceAsk */ + forceAsk: setUserMustConfirmComputer + }, + + /** add email addresses to the email addy to userid mapping used when we're trying to determine + * if a user has used this computer before and what their auth duration should be + * @param {number} userid - the userid of the user + * @param {array} emails - a list of email addresses belonging to the user + * @returns zilch + */ + updateEmailToUserIDMapping: updateEmailToUserIDMapping, + + /** set logged in state for a site + * @param {string} origin - the site to set logged in state for + * @param {string} email - the email that the user is logged in with or falsey if login state should be cleared + */ + setLoggedIn: setLoggedIn, + + /** check if the user is logged into a site + * @param {string} origin - the site to set check the logged in state of + * @returns the email with which the user is logged in + */ + getLoggedIn: getLoggedIn, + + /** watch for changes in the logged in state of a page + * @param {string} origin - the site to watch the status of + * @param {function} callback - a callback to invoke when state changes + */ + watchLoggedIn: watchLoggedIn, + + /** clear all logged in preferences + * @param {string} origin - the site to watch the status of + * @param {function} callback - a callback to invoke when state changes + */ + logoutEverywhere: logoutEverywhere, + /** * Clear all stored data - email addresses, key pairs, temporary key pairs, * site/email associations. diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index d4a23aec70aacea814a0e07caa9c6b341bc52c08..e1fe4502ff97e6eba2c196df3d16b4be2094989f 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -33,9 +33,9 @@ BrowserID.User = (function() { // if it was issued *before* the domain key was last updated or // if the certificate expires in less that 5 minutes from now. function isExpired(cert) { - // if it expires in less than 5 minutes, it's too old to use. + // if it expires in less than 2 minutes, it's too old to use. var diff = cert.expires.valueOf() - serverTime.valueOf(); - if (diff < (60 * 5 * 1000)) { + if (diff < (60 * 2 * 1000)) { return true; } @@ -436,7 +436,11 @@ BrowserID.User = (function() { } provisioning( - { email: email, url: info.prov }, + { + email: email, + url: info.prov, + ephemeral: !storage.usersComputer.confirmed(email) + }, function(keypair, cert) { var userInfo = _.extend({ keypair: keypair, @@ -458,7 +462,6 @@ BrowserID.User = (function() { } } ); - }, /** @@ -626,6 +629,10 @@ BrowserID.User = (function() { * @param {function} [onFailure] - called on error. */ logoutUser: function(onComplete, onFailure) { + // logout of all websites + storage.logoutEverywhere(); + + // log out of browserid network.logout(function() { setAuthenticationStatus(false); if (onComplete) { @@ -905,12 +912,7 @@ BrowserID.User = (function() { */ syncEmailKeypair: function(email, onComplete, onFailure) { prepareDeps(); - // FIXME: parameterize! - var keysize = 256; - var ie_version = BrowserID.BrowserSupport.getInternetExplorerVersion(); - if (ie_version > -1 && ie_version < 9) - keysize = 128; - var keypair = jwk.KeyPair.generate("DS", keysize); + var keypair = jwk.KeyPair.generate("DS", bid.KEY_LENGTH); setTimeout(function() { certifyEmailKeypair(email, keypair, onComplete, onFailure); }, 0); @@ -1046,27 +1048,38 @@ BrowserID.User = (function() { }, /** - * Get an assertion for the current domain, as long as the user has - * selected that they want the email/site remembered + * Get an assertion for the current domain if the user is signed into it * @method getPersistentSigninAssertion * @param {function} onComplete - called on completion. Called with an * assertion if successful, null otw. * @param {function} onFailure - called on XHR failure. */ - getPersistentSigninAssertion: function(onComplete, onFailure) { + getSilentAssertion: function(siteSpecifiedEmail, onComplete, onFailure) { + // XXX: why do we need to check authentication status here explicitly. + // why can't we fail later? the problem with doing this is that + // knowing correct present authentication status requires that we + // talk to the server, because you can be logged in or logged out + // in many different contexts (dialog, manage page, cookies expire). + // so if we rely on localstorage only and check authentication status + // only when we know a network request will be required, we very well + // might have fewer race conditions and do fewer network requests. User.checkAuthentication(function(authenticated) { if (authenticated) { - var remembered = storage.site.get(origin, "remember"); - var email = storage.site.get(origin, "email"); - if (remembered && email) { - User.getAssertion(email, origin, onComplete, onFailure); - } - else if (onComplete) { - onComplete(null); + var loggedInEmail = storage.getLoggedIn(origin); + if (loggedInEmail !== siteSpecifiedEmail) { + if (loggedInEmail) { + User.getAssertion(loggedInEmail, origin, function(assertion) { + onComplete(assertion ? loggedInEmail : null, assertion); + }, onFailure); + } else { + onComplete(null, null); + } + } else { + onComplete(loggedInEmail, null); } } else if (onComplete) { - onComplete(null); + onComplete(null, null); } }, onFailure); }, @@ -1078,15 +1091,14 @@ BrowserID.User = (function() { * a boolean, true if successful, false otw. * @param {function} onFailure - called on XHR failure. */ - clearPersistentSignin: function(onComplete, onFailure) { + logout: function(onComplete, onFailure) { User.checkAuthentication(function(authenticated) { if (authenticated) { - storage.site.set(origin, "remember", false); - if (onComplete) { - onComplete(true); - } - } else if (onComplete) { - onComplete(false); + storage.setLoggedIn(origin, false); + } + + if (onComplete) { + onComplete(!!authenticated); } }, onFailure); }, @@ -1111,8 +1123,6 @@ BrowserID.User = (function() { onComplete(hasSecondary); } - - }; User.setOrigin(document.location.host); diff --git a/resources/static/shared/xhr.js b/resources/static/shared/xhr.js index c174b189652dd6e002507d9158e74bd168d15791..5c22abb882086c7cee5a3a3b728c6f19ca616c1b 100644 --- a/resources/static/shared/xhr.js +++ b/resources/static/shared/xhr.js @@ -136,7 +136,9 @@ BrowserID.XHR = (function() { var req = _.extend(options, { type: "POST", - data: data, + data: JSON.stringify(data), + contentType: 'application/json', + processData: false, defer_success: true }); request(req); diff --git a/resources/static/test/cases/controllers/is_this_your_computer.js b/resources/static/test/cases/controllers/is_this_your_computer.js new file mode 100644 index 0000000000000000000000000000000000000000..8e6702c9bb7e4bb29eeadb1dc3119434460a054c --- /dev/null +++ b/resources/static/test/cases/controllers/is_this_your_computer.js @@ -0,0 +1,50 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +(function() { + "use strict"; + + var controller, + el = $("body"), + bid = BrowserID, + user = bid.User, + xhr = bid.Mocks.xhr, + modules = bid.Modules, + testHelpers = bid.TestHelpers, + register = testHelpers.register; + + + module("controllers/is_this_your_computer", { + setup: function() { + testHelpers.setup(); + }, + + teardown: function() { + if (controller) { + try { + controller.destroy(); + controller = null; + } catch(e) { + // could already be destroyed from the close + } + } + testHelpers.teardown(); + } + }); + + function createController(options) { + controller = modules.IsThisYourComputer.create(); + controller.start(options || {}); + } + + test("yes - sets ownership flag to true for the user", function() { + console.log("add a test"); + }); + + test("no - set the ownership flag to false for the user", function() { + console.log("add a test"); + }); +}()); + diff --git a/resources/static/test/cases/controllers/pick_email.js b/resources/static/test/cases/controllers/pick_email.js index 0e6f1b0c1a2de7b2fe365852383be493163b5fbe..9d3678f8c2c53b28dc7fc9c2d4050513038082ef 100644 --- a/resources/static/test/cases/controllers/pick_email.js +++ b/resources/static/test/cases/controllers/pick_email.js @@ -32,11 +32,9 @@ }); - function createController(allowPersistent) { + function createController() { controller = bid.Modules.PickEmail.create(); - controller.start({ - allow_persistent: allowPersistent || false - }); + controller.start({}); } test("multiple emails - print emails in alphabetical order", function() { @@ -78,73 +76,26 @@ equal(label.hasClass("preselected"), false, "the label has no class"); }); - function testRemember(allowPersistent, remember) { + asyncTest("signIn - saves picked email to storage", function() { storage.addEmail("testuser@testuser.com", {}); storage.addEmail("testuser2@testuser.com", {}); - storage.site.set(testOrigin, "remember", remember); - - createController(allowPersistent); - - // remember can only be checked if allowPersistent is allowed - var rememberChecked = allowPersistent ? remember : false; - - equal($("#remember").is(":checked"), rememberChecked, "remember should " + (rememberChecked ? "" : " not " ) + " be checked"); - } - - test("pickemail controller with allow_persistent and remember set to false", function() { - testRemember(false, false); - }); - - test("pickemail controller with allow_persistent set to false and remember set to true", function() { - testRemember(false, true); - }); - - test("pickemail controller with allow_persistent and remember set to true", function() { - testRemember(true, true); - }); - - asyncTest("signIn saves email, remember status to storage when allow_persistent set to true", function() { - storage.addEmail("testuser@testuser.com", {}); - storage.addEmail("testuser2@testuser.com", {}); - - createController(true); + createController(); $("input[type=radio]").eq(0).trigger("click"); - $("#remember").attr("checked", true); var assertion; register("email_chosen", function(msg, info) { equal(storage.site.get(testOrigin, "email"), "testuser2@testuser.com", "email saved correctly"); - equal(storage.site.get(testOrigin, "remember"), true, "remember saved correctly"); ok(info.email, "email_chosen message triggered with email"); start(); }); controller.signIn(); }); - asyncTest("signIn saves email, but not remember status when allow_persistent set to false", function() { - storage.addEmail("testuser@testuser.com", {}); - storage.addEmail("testuser2@testuser.com", {}); - storage.site.set(testOrigin, "remember", false); - - createController(false); - - $("input[type=radio]").eq(0).trigger("click"); - $("#remember").attr("checked", true); - - register("email_chosen", function(msg, info) { - equal(storage.site.get(testOrigin, "email"), "testuser2@testuser.com", "email saved correctly"); - equal(storage.site.get(testOrigin, "remember"), false, "remember saved correctly"); - - start(); - }); - controller.signIn(); - }); - asyncTest("addEmail triggers an 'add_email' message", function() { - createController(false); + createController(); register("add_email", function(msg, info) { ok(true, "add_email triggered"); @@ -157,7 +108,7 @@ storage.addEmail("testuser2@testuser.com", {}); storage.addEmail("testuser@testuser.com", {}); - createController(false); + createController(); equal($("#email_1").is(":checked"), false, "radio button is not selected before click."); @@ -174,7 +125,7 @@ storage.addEmail("testuser+test0@testuser.com", {}); storage.addEmail("testuser+test1@testuser.com", {}); - createController(false); + createController(); equal($("#email_1").is(":checked"), false, "radio button is not selected before click."); @@ -187,30 +138,5 @@ equal($("#email_0").is(":checked"), true, "radio button is correctly selected"); }); - test("click on the 'Always sign in...' label and checkbox - correct toggling", function() { - createController(true); - - var label = $("label[for=remember]"), - checkbox = $("#remember").removeAttr("checked"); - - equal(checkbox.is(":checked"), false, "checkbox is not yet checked"); - - // toggle checkbox to on clicking on label - label.trigger("click"); - equal(checkbox.is(":checked"), true, "checkbox is correctly checked"); - - // toggle checkbox to off clicking on label - label.trigger("click"); - equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked"); - - // toggle checkbox to on clicking on checkbox - checkbox.trigger("click"); - equal(checkbox.is(":checked"), true, "checkbox is correctly checked"); - - // toggle checkbox to off clicking on checkbox - checkbox.trigger("click"); - equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked"); - }); - }()); diff --git a/resources/static/test/cases/include.js b/resources/static/test/cases/include.js index 5c54c0bcfc9a4ac47cb3acb4b5bf3896827a030e..42aa6c6562488a4dfba94cba7b438641c8d0ba02 100644 --- a/resources/static/test/cases/include.js +++ b/resources/static/test/cases/include.js @@ -12,10 +12,18 @@ equal(typeof navigator.id, "object", "navigator.id namespace is available"); }); - test("navigator.id.getVerifiedEmail is available", function() { - equal(typeof navigator.id.getVerifiedEmail, "function", "navigator.id.getVerifiedEmail is available"); + test("expected public API functions available", function() { + _.each([ + "get", + "request", + "setLoggedInUser", + "logout", + "addEventListener", + "removeEventListener" + ], function(item, index) { + equal(typeof navigator.id[ item ], "function", "navigator.id." + item + " is available"); + }); }); - }()); diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js index c951c5ec98f1b11a55af5b52b98c58a17522a534..1c32995418eb58c6e2c785a5530d5a8bd791f23b 100644 --- a/resources/static/test/cases/resources/state.js +++ b/resources/static/test/cases/resources/state.js @@ -12,9 +12,11 @@ user = bid.User, machine, actions, + network = bid.Network, storage = bid.Storage, testHelpers = bid.TestHelpers, - xhr = bid.Mocks.xhr; + xhr = bid.Mocks.xhr, + TEST_EMAIL = "testuser@testuser.com"; var ActionsMock = function() { this.called = {}; @@ -38,6 +40,19 @@ machine.start({controller: actions}); } + function setContextInfo(auth_status) { + // Make sure there is context info for network. + var serverTime = (new Date().getTime()) - 10; + mediator.publish("context_info", { + server_time: serverTime, + domain_key_creation_time: serverTime, + code_version: "ABCDEF", + auth_status: auth_status || "password", + userid: 1, + random_seed: "ABCDEFGH" + }); + } + module("resources/state", { setup: function() { testHelpers.setup(); @@ -51,10 +66,6 @@ }); - test("can create and start the machine", function() { - ok(machine, "Machine has been created"); - }); - test("attempt to create a state machine without a controller", function() { var error; try { @@ -69,15 +80,15 @@ test("user_staged - call doConfirmUser", function() { mediator.publish("user_staged", { - email: "testuser@testuser.com" + email: TEST_EMAIL }); - equal(actions.info.doConfirmUser.email, "testuser@testuser.com", "waiting for email confirmation for testuser@testuser.com"); + equal(actions.info.doConfirmUser.email, TEST_EMAIL, "waiting for email confirmation for testuser@testuser.com"); }); test("user_staged with required email - call doConfirmUser with required = true", function() { - mediator.publish("start", { requiredEmail: "testuser@testuser.com" }); - mediator.publish("user_staged", { email: "testuser@testuser.com" }); + mediator.publish("start", { requiredEmail: TEST_EMAIL }); + mediator.publish("user_staged", { email: TEST_EMAIL }); equal(actions.info.doConfirmUser.required, true, "doConfirmUser called with required flag"); }); @@ -89,31 +100,31 @@ }); test("email_staged - call doConfirmEmail", function() { - mediator.publish("email_staged", { email: "testuser@testuser.com" }); + mediator.publish("email_staged", { email: TEST_EMAIL }); equal(actions.info.doConfirmEmail.required, false, "doConfirmEmail called without required flag"); }); test("email_staged with required email - call doConfirmEmail with required = true", function() { - mediator.publish("start", { requiredEmail: "testuser@testuser.com" }); - mediator.publish("email_staged", { email: "testuser@testuser.com" }); + mediator.publish("start", { requiredEmail: TEST_EMAIL }); + mediator.publish("email_staged", { email: TEST_EMAIL }); equal(actions.info.doConfirmEmail.required, true, "doConfirmEmail called with required flag"); }); test("primary_user with already provisioned primary user - call doEmailChosen", function() { - storage.addEmail("testuser@testuser.com", { type: "primary", cert: "cert" }); - mediator.publish("primary_user", { email: "testuser@testuser.com" }); + storage.addEmail(TEST_EMAIL, { type: "primary", cert: "cert" }); + mediator.publish("primary_user", { email: TEST_EMAIL }); ok(actions.called.doEmailChosen, "doEmailChosen called"); }); test("primary_user with unprovisioned primary user - call doProvisionPrimaryUser", function() { - mediator.publish("primary_user", { email: "testuser@testuser.com" }); + mediator.publish("primary_user", { email: TEST_EMAIL }); ok(actions.called.doProvisionPrimaryUser, "doPrimaryUserProvisioned called"); }); test("primary_user_provisioned - call doEmailChosen", function() { - mediator.publish("primary_user_provisioned", { email: "testuser@testuser.com" }); + mediator.publish("primary_user_provisioned", { email: TEST_EMAIL }); ok(actions.called.doPrimaryUserProvisioned, "doPrimaryUserProvisioned called"); }); @@ -124,19 +135,19 @@ }); test("primary_user_unauthenticated after required email - call doCannotVerifyRequiredPrimary", function() { - mediator.publish("start", { requiredEmail: "testuser@testuser.com", type: "primary", add: false, email: "testuser@testuser.com" }); + mediator.publish("start", { requiredEmail: TEST_EMAIL, type: "primary", add: false, email: TEST_EMAIL }); mediator.publish("primary_user_unauthenticated"); ok(actions.called.doCannotVerifyRequiredPrimary, "doCannotVerifyRequiredPrimary called"); }); test("primary_user_unauthenticated after verification of new user - call doAuthenticate", function() { - mediator.publish("start", { email: "testuser@testuser.com", type: "primary", add: false }); + mediator.publish("start", { email: TEST_EMAIL, type: "primary", add: false }); mediator.publish("primary_user_unauthenticated"); ok(actions.called.doAuthenticate, "doAuthenticate called"); }); test("primary_user_unauthenticated after verification of additional email to current user - call doPickEmail and doAddEmail", function() { - mediator.publish("start", { email: "testuser@testuser.com", type: "primary", add: true }); + mediator.publish("start", { email: TEST_EMAIL, type: "primary", add: true }); mediator.publish("primary_user_unauthenticated"); ok(actions.called.doPickEmail, "doPickEmail called"); ok(actions.called.doAddEmail, "doAddEmail called"); @@ -153,39 +164,46 @@ }); test("primary_user - call doProvisionPrimaryUser", function() { - mediator.publish("primary_user", { email: "testuser@testuser.com", assertion: "assertion" }); + mediator.publish("primary_user", { email: TEST_EMAIL, assertion: "assertion" }); ok(actions.called.doProvisionPrimaryUser, "doProvisionPrimaryUser called"); }); - test("primary_user_ready - call doEmailChosen", function() { - mediator.publish("primary_user_ready", { email: "testuser@testuser.com", assertion: "assertion" }); + asyncTest("primary_user_ready - redirect to `email_chosen`", function() { + storage.addEmail(TEST_EMAIL, {}); + mediator.subscribe("email_chosen", function(msg, info) { + equal(info.email, TEST_EMAIL, "correct email passed"); + start(); + }); - ok(actions.called.doEmailChosen, "doEmailChosen called"); - }); + mediator.publish("primary_user_ready", { email: TEST_EMAIL, assertion: "assertion" }); - test("authenticated - call doEmailChosen", function() { - storage.addEmail("testuser@testuser.com", {}); - mediator.publish("authenticated", { email: "testuser@testuser.com" }); + }); - ok(actions.called.doEmailChosen, "doEmailChosen has been called"); + asyncTest("authenticated - redirect to `email_chosen`", function() { + storage.addEmail(TEST_EMAIL, {}); + mediator.subscribe("email_chosen", function(msg, data) { + equal(data.email, TEST_EMAIL); + start(); + }); + mediator.publish("authenticated", { email: TEST_EMAIL }); }); test("forgot_password", function() { mediator.publish("forgot_password", { - email: "testuser@testuser.com", + email: TEST_EMAIL, requiredEmail: true }); - equal(actions.info.doForgotPassword.email, "testuser@testuser.com", "correct email passed"); + equal(actions.info.doForgotPassword.email, TEST_EMAIL, "correct email passed"); equal(actions.info.doForgotPassword.requiredEmail, true, "correct requiredEmail passed"); }); test("reset_password - call doResetPassword", function() { // XXX how is this different from forgot_password? mediator.publish("reset_password", { - email: "testuser@testuser.com" + email: TEST_EMAIL }); - equal(actions.info.doResetPassword.email, "testuser@testuser.com", "reset password with the correct email"); + equal(actions.info.doResetPassword.email, TEST_EMAIL, "reset password with the correct email"); }); test("cancel reset_password flow - go two steps back", function() { @@ -193,34 +211,50 @@ // screens back. Do do this, we are simulating the steps necessary to get // to the reset_password flow. mediator.publish("authenticate"); - mediator.publish("forgot_password", undefined, { email: "testuser@testuser.com" }); + mediator.publish("forgot_password", undefined, { email: TEST_EMAIL }); mediator.publish("reset_password"); actions.info.doAuthenticate = {}; mediator.publish("cancel_state"); - equal(actions.info.doAuthenticate.email, "testuser@testuser.com", "authenticate called with the correct email"); + equal(actions.info.doAuthenticate.email, TEST_EMAIL, "authenticate called with the correct email"); }); - test("assertion_generated with null assertion", function() { + asyncTest("assertion_generated with null assertion - redirect to pick_email", function() { + mediator.subscribe("pick_email", function() { + ok(true, "redirect to pick_email"); + start(); + }); mediator.publish("assertion_generated", { assertion: null }); - - equal(actions.called.doPickEmail, true, "now picking email because of null assertion"); }); - test("assertion_generated with assertion", function() { + asyncTest("assertion_generated with assertion, need to ask user whether it's their computer - redirect to is_this_your_computer", function() { + setContextInfo("password"); + storage.usersComputer.forceAsk(network.userid()); + mediator.subscribe("is_this_your_computer", function() { + ok(true, "redirect to is_this_your_computer"); + start(); + }); + mediator.publish("assertion_generated", { assertion: "assertion" }); - - equal(actions.info.doAssertionGenerated, "assertion", "assertion generated with good assertion"); }); - test("add_email - call doAddEmail", function() { - mediator.publish("add_email", { email: "testuser@testuser.com" }); + test("assertion_generated with assertion, do not ask user whether it's their computer - doAssertionGenerated called", function() { + setContextInfo("password"); + // First, set up the context info for the email. - ok(actions.called.doAddEmail, "user wants to add an email"); - ok(actions.info.doAddEmail.email, "testuser@testuser.com", "correct email passed"); + storage.addEmail(TEST_EMAIL, {}); + mediator.publish("email_chosen", { email: TEST_EMAIL }); + mediator.publish("assertion_generated", { + assertion: "assertion" + }); + + equal(actions.info.doAssertionGenerated.assertion, "assertion", + "doAssertionGenerated called with assertion"); + equal(actions.info.doAssertionGenerated.email, TEST_EMAIL, + "doAssertionGenerated called with email"); }); test("email_confirmed", function() { @@ -247,10 +281,10 @@ test("authenticate", function() { mediator.publish("authenticate", { - email: "testuser@testuser.com" + email: TEST_EMAIL }); - equal(actions.info.doAuthenticate.email, "testuser@testuser.com", "authenticate with testuser@testuser.com"); + equal(actions.info.doAuthenticate.email, TEST_EMAIL, "authenticate with testuser@testuser.com"); }); test("start with no special parameters - go straight to checking auth", function() { @@ -276,19 +310,19 @@ }); test("start with valid requiredEmail - go to doCheckAuth", function() { - mediator.publish("start", { requiredEmail: "testuser@testuser.com" }); + mediator.publish("start", { requiredEmail: TEST_EMAIL }); equal(actions.called.doCheckAuth, true, "checking auth on start"); }); asyncTest("start to complete successful primary email verification - goto 'primary_user'", function() { mediator.subscribe("primary_user", function(msg, info) { - equal(info.email, "testuser@testuser.com", "correct email given"); + equal(info.email, TEST_EMAIL, "correct email given"); equal(info.add, true, "correct add flag"); start(); }); - mediator.publish("start", { email: "testuser@testuser.com", type: "primary", add: true }); + mediator.publish("start", { email: TEST_EMAIL, type: "primary", add: true }); }); test("cancel", function() { @@ -299,7 +333,7 @@ asyncTest("email_chosen with secondary email, user must authenticate - call doAuthenticateWithRequiredEmail", function() { - var email = "testuser@testuser.com"; + var email = TEST_EMAIL; storage.addEmail(email, { type: "secondary" }); xhr.setContextInfo("auth_level", "assertion"); @@ -314,7 +348,7 @@ }); asyncTest("email_chosen with secondary email, user authenticated to secondary - call doEmailChosen", function() { - var email = "testuser@testuser.com"; + var email = TEST_EMAIL; storage.addEmail(email, { type: "secondary" }); xhr.setContextInfo("auth_level", "password"); @@ -334,7 +368,7 @@ // generate its own assertion when ready. For efficiency, we could // check here whether the cert is ready, but it is early days yet and // the format may change. - var email = "testuser@testuser.com"; + var email = TEST_EMAIL; storage.addEmail(email, { type: "primary" }); mediator.publish("email_chosen", { email: email }); @@ -342,7 +376,7 @@ }); test("email_chosen with invalid email - throw exception", function() { - var email = "testuser@testuser.com", + var email = TEST_EMAIL, error; try { @@ -355,11 +389,17 @@ }); test("null assertion generated - preserve original options in doPickEmail", function() { - mediator.publish("start", { allowPersistent: true }); + mediator.publish("start", { + hostname: "http://example.com", + privacyURL: "http://example.com/priv.html", + tosURL: "http://example.com/tos.html" + }); mediator.publish("assertion_generated", { assertion: null }); equal(actions.called.doPickEmail, true, "doPickEmail callled"); - equal(actions.info.doPickEmail.allow_persistent, true, "allow_persistent preserved"); + equal(actions.info.doPickEmail.origin, "http://example.com", "hostname preserved"); + equal(actions.info.doPickEmail.privacyURL, "http://example.com/priv.html", "privacyURL preserved"); + equal(actions.info.doPickEmail.tosURL, "http://example.com/tos.html", "tosURL preserved"); }); }()); diff --git a/resources/static/test/cases/shared/user.js b/resources/static/test/cases/shared/user.js index 3fd25130ee4260378c98effe2d0e7e06eaccf052..976b55de08e10a565d26acedbfcabf42e1a40b1e 100644 --- a/resources/static/test/cases/shared/user.js +++ b/resources/static/test/cases/shared/user.js @@ -1015,95 +1015,22 @@ var vep = require("./vep"); failureCheck(lib.cancelUser); }); - asyncTest("getPersistentSigninAssertion with invalid login - expect null assertion", function() { - xhr.setContextInfo("auth_level", undefined); - - lib.syncEmailKeypair("testuser@testuser.com", function() { - storage.site.set(testOrigin, "remember", false); - storage.site.set(testOrigin, "email", "testuser@testuser.com"); - xhr.useResult("invalid"); - - lib.getPersistentSigninAssertion(function onComplete(assertion) { - strictEqual(assertion, null, "assertion with invalid login is null"); - start(); - }, testHelpers.unexpectedXHRFailure); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("getPersistentSigninAssertion without email set for site - expect null assertion", function() { - xhr.setContextInfo("auth_level", "primary"); - storage.site.set(testOrigin, "remember", true); - storage.site.remove(testOrigin, "email"); - - lib.getPersistentSigninAssertion(function onComplete(assertion) { - strictEqual(assertion, null, "assertion with no email is null"); - start(); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("getPersistentSigninAssertion without remember set for site - expect null assertion", function() { - xhr.setContextInfo("auth_level", "primary"); - lib.syncEmailKeypair("testuser@testuser.com", function() { - storage.site.set(testOrigin, "remember", false); - storage.site.set(testOrigin, "email", "testuser@testuser.com"); - // invalidate the email so that we force a fresh key certification with - // the server - storage.invalidateEmail("testuser@testuser.com"); - - lib.getPersistentSigninAssertion(function onComplete(assertion) { - strictEqual(assertion, null, "assertion with remember=false is null"); - start(); - }, testHelpers.unexpectedXHRFailure); - }); - }); - - asyncTest("getPersistentSigninAssertion with valid login, email, and remember set to true - expect assertion", function() { - xhr.setContextInfo("auth_level", "primary"); - lib.syncEmailKeypair("testuser@testuser.com", function() { - storage.site.set(testOrigin, "remember", true); - storage.site.set(testOrigin, "email", "testuser@testuser.com"); - // invalidate the email so that we force a fresh key certification with - // the server - storage.invalidateEmail("testuser@testuser.com"); - - lib.getPersistentSigninAssertion(function onComplete(assertion) { - ok(assertion, "we have an assertion!"); - start(); - }, testHelpers.unexpectedXHRFailure); - }); - }); - - asyncTest("getPersistentSigninAssertion with XHR failure", function() { - xhr.setContextInfo("auth_level", "primary"); - lib.syncEmailKeypair("testuser@testuser.com", function() { - storage.site.set(testOrigin, "remember", true); - storage.site.set(testOrigin, "email", "testuser@testuser.com"); - // invalidate the email so that we force a fresh key certification with - // the server - storage.invalidateEmail("testuser@testuser.com"); - - failureCheck(lib.getPersistentSigninAssertion); - }); - - - }); - asyncTest("clearPersistentSignin with invalid login", function() { + asyncTest("logout with invalid login", function() { xhr.setContextInfo("auth_level", undefined); - lib.clearPersistentSignin(function onComplete(success) { + lib.logout(function onComplete(success) { strictEqual(success, false, "success with invalid login is false"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("clearPersistentSignin with valid login with remember set to true", function() { + asyncTest("logout with valid login with remember set to true", function() { xhr.setContextInfo("auth_level", "primary"); storage.site.set(testOrigin, "remember", true); - lib.clearPersistentSignin(function onComplete(success) { + lib.logout(function onComplete(success) { strictEqual(success, true, "success flag good"); - strictEqual(storage.site.get(testOrigin, "remember"), false, "remember flag set to false"); start(); }, testHelpers.unexpectedXHRFailure); }); diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js index 6b82fb2362359f5607173d8f1a2679a60c50160c..e33be1b0dfde34ddbf28f6dbdb0a896fb7a20e73 100644 --- a/resources/static/test/mocks/xhr.js +++ b/resources/static/test/mocks/xhr.js @@ -34,12 +34,12 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" }, "get /wsapi/email_for_token?token=token needsPassword": { email: "testuser@testuser.com", needs_password: true }, "get /wsapi/email_for_token?token=token invalid": { success: false }, - "post /wsapi/authenticate_user valid": { success: true }, + "post /wsapi/authenticate_user valid": { success: true, userid: 1 }, "post /wsapi/authenticate_user invalid": { success: false }, "post /wsapi/authenticate_user incorrectPassword": { success: false }, "post /wsapi/authenticate_user ajaxError": undefined, - "post /wsapi/auth_with_assertion primary": { success: true }, - "post /wsapi/auth_with_assertion valid": { success: true }, + "post /wsapi/auth_with_assertion primary": { success: true, userid: 1 }, + "post /wsapi/auth_with_assertion valid": { success: true, userid: 1 }, "post /wsapi/auth_with_assertion invalid": { success: false }, "post /wsapi/auth_with_assertion ajaxError": undefined, "post /wsapi/cert_key valid": random_cert, @@ -141,7 +141,7 @@ BrowserID.Mocks.xhr = (function() { }; - if(type === "post" && !obj.data.csrf) { + if(type === "post" && obj.data.indexOf("csrf") === -1) { ok(false, "missing csrf token on POST request"); } diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js index 493ea1db0756779b69735a7f18c104b51c1d3e26..31ac2d086649287fd4b4d68810fed152a9cdeab7 100644 --- a/resources/static/test/testHelpers/helpers.js +++ b/resources/static/test/testHelpers/helpers.js @@ -47,6 +47,12 @@ BrowserID.TestHelpers = (function() { ok($("#error #network").text().length, "network contents have been written"); } + function clearStorage() { + for(var key in localStorage) { + localStorage.removeItem(key); + } + } + var TestHelpers = { XHR_TIME_UNTIL_DELAY: 100, setup: function() { @@ -63,7 +69,7 @@ BrowserID.TestHelpers = (function() { transport.useResult("valid"); network.init(); - storage.clear(); + clearStorage(); $("body").stop().show(); $("body")[0].className = ""; @@ -93,7 +99,7 @@ BrowserID.TestHelpers = (function() { time_until_delay: 10 * 1000 }); network.init(); - storage.clear(); + clearStorage(); screens.wait.hide(); screens.error.hide(); screens.delay.hide(); diff --git a/resources/views/communication_iframe.ejs b/resources/views/communication_iframe.ejs index 5a1e2c07fe3562992a5f3c3fbc8ce53630a4884a..147b0228a8e6a400f841f2cbadbf60459ef7823b 100644 --- a/resources/views/communication_iframe.ejs +++ b/resources/views/communication_iframe.ejs @@ -5,24 +5,7 @@ <html> <head><title>non-interactive iframe</title> <meta charset="utf-8"> - <% if(production) { %> - <script type="text/javascript" src="/production/communication_iframe.js"></script> - <% } else { %> - <script type="text/javascript" src="/lib/jquery-1.7.1.min.js"></script> - <script type="text/javascript" src="/lib/jschannel.js"></script> - <script type="text/javascript" src="/lib/underscore-min.js"></script> - <script type="text/javascript" src="/lib/vepbundle.js"></script> - <script type="text/javascript" src="/lib/hub.js"></script> - <script type="text/javascript" src="/shared/javascript-extensions.js"></script> - <script type="text/javascript" src="/shared/browserid.js"></script> - <script type="text/javascript" src="/shared/mediator.js"></script> - <script type="text/javascript" src="/shared/helpers.js"></script> - <script type="text/javascript" src="/shared/storage.js"></script> - <script type="text/javascript" src="/shared/xhr.js"></script> - <script type="text/javascript" src="/shared/network.js"></script> - <script type="text/javascript" src="/shared/user.js"></script> - <script type="text/javascript" src="/communication_iframe/start.js"></script> - <% } %> + <%- cachify_js('/production/communication_iframe.js') %> </head> <body></body> </html> diff --git a/resources/views/index.ejs b/resources/views/index.ejs index 57f1c8d7b96254292d735fb74bf6bab32639dd4a..6b1f49aec9e17322ae8d9f2f5d9c3ce2d079850c 100644 --- a/resources/views/index.ejs +++ b/resources/views/index.ejs @@ -21,6 +21,16 @@ </ul> </section> + <section id="logout_everywhere"> + <header class="cf buttonrow"> + <h2>Logout from all websites</h2> + <div class="completion_text"> Logged Out! </div> + <button class="logout_everywhere">logout</button> + </header> + + <div class="email">You can sign in again by entering your password. None of your website accounts will be lost.</div></li> + </section> + <section id="edit_password"> <header class="buttonrow cf"> <h2>Password</h2> diff --git a/resources/views/test.ejs b/resources/views/test.ejs index ce0c52d747d9715261ccf0d15fc0cf9747544f2d..dc0bbea64df28b06d94f65f432e8bd552e9c395b 100644 --- a/resources/views/test.ejs +++ b/resources/views/test.ejs @@ -125,6 +125,7 @@ <script src="/dialog/controllers/email_chosen.js"></script> <script src="/dialog/controllers/provision_primary_user.js"></script> <script src="/dialog/controllers/primary_user_provisioned.js"></script> + <script src="/dialog/controllers/is_this_your_computer.js"></script> <script src="/pages/page_helpers.js"></script> <script src="/pages/add_email_address.js"></script> @@ -183,6 +184,7 @@ <script src="cases/controllers/email_chosen.js"></script> <script src="cases/controllers/provision_primary_user.js"></script> <script src="cases/controllers/primary_user_provisioned.js"></script> + <script src="cases/controllers/is_this_your_computer.js"></script> <!-- must go last or all other tests will fail. --> <script src="cases/controllers/dialog.js"></script> diff --git a/tests/auth-with-assertion-test.js b/tests/auth-with-assertion-test.js index 8784afce7d48a987dd596be63308827f32b50c65..eda9f1768df9931dbebd5c39817f1ef2fbd997ed 100755 --- a/tests/auth-with-assertion-test.js +++ b/tests/auth-with-assertion-test.js @@ -48,8 +48,8 @@ suite.addBatch({ "and logging in with the assertion succeeds": { topic: function(assertion) { wsapi.post('/wsapi/auth_with_assertion', { - email: TEST_EMAIL, - assertion: assertion + assertion: assertion, + ephemeral: true }).call(this); }, "works": function(err, r) { diff --git a/tests/cert-emails-test.js b/tests/cert-emails-test.js index d58dea80a63c7fe6f2e1e16a95168febe59f5e38..1262ffefa2b89fb66c8358219d10afe14e17fc57 100755 --- a/tests/cert-emails-test.js +++ b/tests/cert-emails-test.js @@ -12,9 +12,7 @@ start_stop = require('./lib/start-stop.js'), wsapi = require('./lib/wsapi.js'), email = require('../lib/email.js'), ca = require('../lib/keysigner/ca.js'), -jwcert = require('jwcrypto/jwcert'), jwk = require('jwcrypto/jwk'), -jws = require('jwcrypto/jws'), jwt = require('jwcrypto/jwt'); var suite = vows.describe('cert-emails'); @@ -98,7 +96,11 @@ suite.addBatch({ } }, "cert key invoked with proper argument": { - topic: wsapi.post(cert_key_url, { email: 'syncer@somehost.com', pubkey: kp.publicKey.serialize() }), + topic: wsapi.post(cert_key_url, { + email: 'syncer@somehost.com', + pubkey: kp.publicKey.serialize(), + ephemeral: false + }), "returns a response with a proper content-type" : function(err, r) { assert.strictEqual(r.code, 200); }, @@ -143,7 +145,11 @@ suite.addBatch({ } }, "cert key invoked proper arguments but incorrect email address": { - topic: wsapi.post(cert_key_url, { email: 'syncer2@somehost.com', pubkey: kp.publicKey.serialize() }), + topic: wsapi.post(cert_key_url, { + email: 'syncer2@somehost.com', + pubkey: kp.publicKey.serialize(), + ephemeral: false + }), "returns a response with a proper error content-type" : function(err, r) { assert.strictEqual(r.code, 400); } diff --git a/tests/delegated-primary-test.js b/tests/delegated-primary-test.js old mode 100644 new mode 100755 index a9a286a231c8866d6a9ff8cc2f4a5acfbe6a0b22..94818c231b8c280c583346190f519e7c960ae3a3 --- a/tests/delegated-primary-test.js +++ b/tests/delegated-primary-test.js @@ -1,3 +1,9 @@ +#!/usr/bin/env node + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + require('./lib/test_env.js'); const diff --git a/tests/forgotten-email-test.js b/tests/forgotten-email-test.js index ed0fbc8ce9801f0e03ce04a9cd88bac833e5425e..9fc0bc43025b0ecf91e422d7d08c266fce6bb731 100755 --- a/tests/forgotten-email-test.js +++ b/tests/forgotten-email-test.js @@ -162,13 +162,21 @@ suite.addBatch({ // valid (this is so *until* someone clicks through) suite.addBatch({ "first email works": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'firstfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), "should work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); } }, "second email works": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'second@fakeemail.com', pass: 'firstfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'second@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), "should work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); } @@ -192,13 +200,21 @@ suite.addBatch({ // password, and all other combinations should fail suite.addBatch({ "first email, first pass bad": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'firstfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), "shouldn't work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, false); } }, "first email, second pass good": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'secondfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'secondfakepass', + ephemeral: false + }), "should work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); } @@ -210,13 +226,21 @@ suite.addBatch({ } }, "second email, first pass good": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'second@fakeemail.com', pass: 'firstfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'second@fakeemail.com', + pass: 'firstfakepass', + ephemeral: false + }), "should work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); } }, "second email, second pass bad": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'second@fakeemail.com', pass: 'secondfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'second@fakeemail.com', + pass: 'secondfakepass', + ephemeral: false + }), "shouldn' work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, false); } diff --git a/tests/lib/wsapi.js b/tests/lib/wsapi.js index 0bfd9989cce0914e027b4603a3cfdf0a61a73c1b..868c86d02756fd53ed624cbe543b403355cb510e 100644 --- a/tests/lib/wsapi.js +++ b/tests/lib/wsapi.js @@ -21,6 +21,10 @@ exports.injectCookies = function(cookies) { wcli.injectCookies({cookieJar: cookies}, context); }; +exports.getCookie = function(which) { + return wcli.getCookie(context, which); +}; + exports.get = function (path, getArgs) { return function () { wcli.get(configuration, path, context, getArgs, this.callback); diff --git a/tests/password-bcrypt-update-test.js b/tests/password-bcrypt-update-test.js index c403d6ef715d1250b6671128abd9a64b1b7af923..51d9660810960fc4a55c95ec269e5be901dc9ed4 100755 --- a/tests/password-bcrypt-update-test.js +++ b/tests/password-bcrypt-update-test.js @@ -117,7 +117,8 @@ suite.addBatch({ "re-authentication": { topic: wsapi.post('/wsapi/authenticate_user', { email: TEST_EMAIL, - pass: TEST_PASSWORD + pass: TEST_PASSWORD, + ephemeral: false }), "should work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); @@ -153,7 +154,8 @@ suite.addBatch({ "and re-authentication": { topic: wsapi.post('/wsapi/authenticate_user', { email: TEST_EMAIL, - pass: TEST_PASSWORD + pass: TEST_PASSWORD, + ephemeral: false }), "should still work": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); diff --git a/tests/password-length-test.js b/tests/password-length-test.js index 2c956b1a1313a8a46f94718b0860364b6314f29f..eb33773510aaf29e298d7eb8364e0bf98774c5c9 100755 --- a/tests/password-length-test.js +++ b/tests/password-length-test.js @@ -20,11 +20,7 @@ suite.options.error = false; start_stop.addStartupBatches(suite); -// surpress console output of emails with a noop email intercepto var token = undefined; -start_stop.browserid.on('token', function(secret) { - token = secret; -}); suite.addBatch({ "get csrf token": { @@ -52,33 +48,53 @@ suite.addBatch({ } }); +// wait for the token +suite.addBatch({ + "a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "is obtained": function (t) { + assert.strictEqual(typeof t, 'string'); + token = t; + } + } +}); + + // create a new account via the api with (first address) suite.addBatch({ "a password that is too short": { - topic: wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: '0123456' // less than 8 chars, invalid - }), + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { + token: token, + pass: '0123456' // less than 8 chars, invalid + }).call(this) + }, "causes a HTTP error response": function(err, r) { assert.equal(r.code, 400); assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars"); } }, "a password that is too long": { - topic: wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: '012345678901234567890123456789012345678901234567890123456789012345678901234567891', // more than 81 chars, invalid. - }), + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { + token: token, + pass: '012345678901234567890123456789012345678901234567890123456789012345678901234567891', // more than 81 chars, invalid. + }).call(this); + }, "causes a HTTP error response": function(err, r) { assert.equal(r.code, 400); assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars"); } }, "but a password that is just right": { - topic: wsapi.post('/wsapi/complete_user_creation', { - token: token, - pass: 'ahhh. this is just right.' - }), + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { + token: token, + pass: 'ahhh. this is just right.' + }).call(this); + }, "works just fine": function(err, r) { assert.equal(r.code, 200); } diff --git a/tests/password-update-test.js b/tests/password-update-test.js index 9d484f56f47025c365bedee2af4a39fac4caf7eb..a8cbf9fc9320c9c59438f75e8fa210129db07aa2 100755 --- a/tests/password-update-test.js +++ b/tests/password-update-test.js @@ -74,7 +74,8 @@ suite.addBatch({ "authenticating with the password": { topic: wsapi.post('/wsapi/authenticate_user', { email: TEST_EMAIL, - pass: OLD_PASSWORD + pass: OLD_PASSWORD, + ephemeral: false }), "works as expected": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); @@ -83,7 +84,8 @@ suite.addBatch({ "authenticating with the wrong password": { topic: wsapi.post('/wsapi/authenticate_user', { email: TEST_EMAIL, - pass: NEW_PASSWORD + pass: NEW_PASSWORD, + ephemeral: false }), "fails as expected": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, false); @@ -131,7 +133,8 @@ suite.addBatch({ "authenticating with the password": { topic: wsapi.post('/wsapi/authenticate_user', { email: TEST_EMAIL, - pass: NEW_PASSWORD + pass: NEW_PASSWORD, + ephemeral: false }), "works as expected": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, true); @@ -140,7 +143,8 @@ suite.addBatch({ "authenticating with the wrong password": { topic: wsapi.post('/wsapi/authenticate_user', { email: TEST_EMAIL, - pass: OLD_PASSWORD + pass: OLD_PASSWORD, + ephemeral: false }), "fails as expected": function(err, r) { assert.strictEqual(JSON.parse(r.body).success, false); diff --git a/tests/primary-then-secondary-test.js b/tests/primary-then-secondary-test.js index 6b47385a950056d788ba6c144def02c805cacb05..6720802ce321229428541d86d045793d21567c3d 100755 --- a/tests/primary-then-secondary-test.js +++ b/tests/primary-then-secondary-test.js @@ -50,8 +50,8 @@ suite.addBatch({ "and logging in with the assertion succeeds": { topic: function(assertion) { wsapi.post('/wsapi/auth_with_assertion', { - email: TEST_EMAIL, - assertion: assertion + assertion: assertion, + ephemeral: true }).call(this); }, "works": function(err, r) { @@ -198,7 +198,8 @@ suite.addBatch({ "authentication with first email": { topic: wsapi.post('/wsapi/authenticate_user', { email: TEST_EMAIL, - pass: TEST_PASS + pass: TEST_PASS, + ephemeral: false }), "works": function(err, r) { assert.strictEqual(r.code, 200); @@ -207,7 +208,8 @@ suite.addBatch({ "authentication with second email": { topic: wsapi.post('/wsapi/authenticate_user', { email: SECONDARY_EMAIL, - pass: TEST_PASS + pass: TEST_PASS, + ephemeral: false }), "works": function(err, r) { assert.strictEqual(r.code, 200); diff --git a/tests/registration-status-wsapi-test.js b/tests/registration-status-wsapi-test.js index 394383c207dabeb1b78e1a924d4f91ec55064c37..f4515775c3d4e8789c18045abec75813ce43e3e4 100755 --- a/tests/registration-status-wsapi-test.js +++ b/tests/registration-status-wsapi-test.js @@ -35,7 +35,11 @@ suite.addBatch({ suite.addBatch({ "authentication as an unknown user": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'secondfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'secondfakepass', + ephemeral: false + }), "fails": function (err, r) { assert.isFalse(JSON.parse(r.body).success); } @@ -234,7 +238,11 @@ suite.addBatch({ suite.addBatch({ "after re-registration, authenticating with new credetials": { - topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'secondfakepass' }), + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'first@fakeemail.com', + pass: 'secondfakepass', + ephemeral: false + }), "works as you might expect": function (err, r) { assert.strictEqual(JSON.parse(r.body).success, true); } diff --git a/tests/session-context-test.js b/tests/session-context-test.js new file mode 100755 index 0000000000000000000000000000000000000000..563042f5024109dc9dc9069cb201be308fbd951f --- /dev/null +++ b/tests/session-context-test.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +require('./lib/test_env.js'); + +const assert = +require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'), +db = require('../lib/db.js'), +config = require('../lib/configuration.js'), +bcrypt = require('bcrypt'); + +var suite = vows.describe('session-context'); + +// disable vows (often flakey?) async error behavior +suite.options.error = false; + +start_stop.addStartupBatches(suite); + +const TEST_EMAIL = 'someuser@somedomain.com', + PASSWORD = 'thisismypassword'; + +var token = undefined; + +// first stage the account +suite.addBatch({ + "account staging": { + topic: wsapi.post('/wsapi/stage_user', { + email: TEST_EMAIL, + site: 'fakesite.com' + }), + "works": function(err, r) { + assert.equal(r.code, 200); + } + } +}); + +// wait for the token +suite.addBatch({ + "a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "is obtained": function (t) { + assert.strictEqual(typeof t, 'string'); + token = t; + } + } +}); + +// create a new account via the api with (first address) +suite.addBatch({ + "setting password": { + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { + token: token, + pass: PASSWORD + }).call(this); + }, + "works just fine": function(err, r) { + assert.equal(r.code, 200); + } + } +}); + +suite.addBatch({ + "authenticating with the password": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: PASSWORD, + ephemeral: true + }), + "works as expected": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + } + } +}); + +suite.addBatch({ + "session context": { + topic: wsapi.get('/wsapi/session_context'), + "contains values expected": function(err, r) { + assert.isNull(err); + var resp = JSON.parse(r.body); + assert.strictEqual(typeof resp.csrf_token, 'string'); + var serverTime = new Date(resp.server_time); + assert.ok(new Date() - serverTime < 5000); + assert.strictEqual(resp.authenticated, true); + assert.strictEqual(resp.auth_level, 'password'); + var domainKeyCreation = new Date(resp.domain_key_creation_time); + assert.ok(new Date() - serverTime < 365 * 24 * 60 * 60 * 1000); + assert.strictEqual(typeof resp.random_seed, 'string'); + assert.strictEqual(resp.userid, 1); + } + } +}); + +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/session-duration-test.js b/tests/session-duration-test.js new file mode 100755 index 0000000000000000000000000000000000000000..0e5fdd8257f01b037ca5ff355161534af447611d --- /dev/null +++ b/tests/session-duration-test.js @@ -0,0 +1,233 @@ +#!/usr/bin/env node + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +require('./lib/test_env.js'); + +const assert = +require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'), +db = require('../lib/db.js'), +config = require('../lib/configuration.js'), +bcrypt = require('bcrypt'), +primary = require('./lib/primary.js'), +ca = require('../lib/keysigner/ca.js'), +jwk = require('jwcrypto/jwk'), +jwt = require('jwcrypto/jwt'), +jws = require('jwcrypto/jws'); + +var suite = vows.describe('session-context'); + +// disable vows (often flakey?) async error behavior +suite.options.error = false; + +start_stop.addStartupBatches(suite); + +// test that auth_with_assertion also respects the 'ephemeral' argument +const PRIMARY_DOMAIN = 'example.domain', + PRIMARY_EMAIL = 'testuser@' + PRIMARY_DOMAIN, + PRIMARY_ORIGIN = 'http://127.0.0.1:10002'; + +// here we go! let's authenticate with an assertion from +// a primary. + +var primaryUser = new primary({ + email: PRIMARY_EMAIL, + domain: PRIMARY_DOMAIN +}); + +suite.addBatch({ + "generating an assertion": { + topic: function() { + return primaryUser.getAssertion(PRIMARY_ORIGIN); + }, + "succeeds": function(r, err) { + assert.isString(r); + }, + "and logging in with the assertion with ephemeral = true": { + topic: function(assertion) { + wsapi.post('/wsapi/auth_with_assertion', { + assertion: assertion, + ephemeral: true + }).call(this); + }, + "works": function(err, r) { + var resp = JSON.parse(r.body); + assert.isObject(resp); + assert.isTrue(resp.success); + }, + "has expected duration": function(err, r) { + assert.strictEqual(parseInt(wsapi.getCookie(/^browserid_state/).split('.')[3], 10), config.get('ephemeral_session_duration_ms')); + } + } + } +}); + +suite.addBatch({ + "generating an assertion": { + topic: function() { + return primaryUser.getAssertion(PRIMARY_ORIGIN); + }, + "succeeds": function(r, err) { + assert.isString(r); + }, + "and logging in with the assertion with ephemeral = false": { + topic: function(assertion) { + wsapi.post('/wsapi/auth_with_assertion', { + assertion: assertion, + ephemeral: false + }).call(this); + }, + "works": function(err, r) { + var resp = JSON.parse(r.body); + assert.isObject(resp); + assert.isTrue(resp.success); + }, + "has expected duration": function(err, r) { + assert.strictEqual(parseInt(wsapi.getCookie(/^browserid_state/).split('.')[3], 10), config.get('authentication_duration_ms')); + } + } + } +}); + +// now test that authenticate_user & secondary emails properly respect the 'ephemeral' argument to +// alter session length +const TEST_EMAIL = 'someuser@somedomain.com', + PASSWORD = 'thisismypassword'; + +var token = undefined; + +// first stage the account +suite.addBatch({ + "account staging": { + topic: wsapi.post('/wsapi/stage_user', { + email: TEST_EMAIL, + site: 'fakesite.com' + }), + "works": function(err, r) { + assert.equal(r.code, 200); + } + } +}); + +// wait for the token +suite.addBatch({ + "a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "is obtained": function (t) { + assert.strictEqual(typeof t, 'string'); + token = t; + } + } +}); + +// create a new account via the api with (first address) +suite.addBatch({ + "setting password": { + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { + token: token, + pass: PASSWORD + }).call(this); + }, + "works just fine": function(err, r) { + assert.equal(r.code, 200); + } + } +}); + +suite.addBatch({ + "authenticating with the password and ephemeral = true": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: PASSWORD, + ephemeral: true + }), + "works as expected": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + }, + "yields a session of expected length": function(err, r) { + assert.strictEqual(parseInt(wsapi.getCookie(/^browserid_state/).split('.')[3], 10), config.get('ephemeral_session_duration_ms')); + } + } +}); + +suite.addBatch({ + "authenticating with the password and ephemeral = false": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: PASSWORD, + ephemeral: false + }), + "works as expected": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + }, + "yields a session of expected length": function(err, r) { + assert.strictEqual(parseInt(wsapi.getCookie(/^browserid_state/).split('.')[3], 10), config.get('authentication_duration_ms')); + } + } +}); + +// finally, let's verify that ephemeral is properly handled when certifying keys for a user + +var kp = jwk.KeyPair.generate("RS", 64); + +suite.addBatch({ + "cert_key invoked with ephemeral = false": { + topic: wsapi.post('/wsapi/cert_key', { + email: TEST_EMAIL, + pubkey: kp.publicKey.serialize(), + ephemeral: false + }), + "returns a response with a proper content-type" : function(err, r) { + assert.strictEqual(r.code, 200); + }, + "returns a valid cert": function(err, r) { + ca.verifyChain('127.0.0.1', [r.body], function(pk) { + assert.isTrue(kp.publicKey.equals(pk)); + }); + }, + "has the correct expiration": function(err, r) { + var cert = new jws.JWS(); + cert.parse(r.body); + var pl = JSON.parse(cert.payload); + assert.strictEqual(pl.exp - pl.iat, config.get('certificate_validity_ms')); + } + } +}); + +suite.addBatch({ + "cert_key invoked with ephemeral = true": { + topic: wsapi.post('/wsapi/cert_key', { + email: TEST_EMAIL, + pubkey: kp.publicKey.serialize(), + ephemeral: true + }), + "returns a response with a proper content-type" : function(err, r) { + assert.strictEqual(r.code, 200); + }, + "returns a valid cert": function(err, r) { + ca.verifyChain('127.0.0.1', [r.body], function(pk) { + assert.isTrue(kp.publicKey.equals(pk)); + }); + }, + "has the correct expiration": function(err, r) { + var cert = new jws.JWS(); + cert.parse(r.body); + var pl = JSON.parse(cert.payload); + assert.strictEqual(pl.exp - pl.iat, config.get('ephemeral_session_duration_ms')); + } + } +}); + +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/session-prolong-test.js b/tests/session-prolong-test.js new file mode 100755 index 0000000000000000000000000000000000000000..16b0ef8d4431a88fe6d5194e6605c9b831e1a5d3 --- /dev/null +++ b/tests/session-prolong-test.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +require('./lib/test_env.js'); + +const assert = +require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'), +db = require('../lib/db.js'), +config = require('../lib/configuration.js'), +bcrypt = require('bcrypt'); + +var suite = vows.describe('session-prolong'); + +// disable vows (often flakey?) async error behavior +suite.options.error = false; + +start_stop.addStartupBatches(suite); + +const TEST_EMAIL = 'someuser@somedomain.com', + PASSWORD = 'thisismypassword'; + +var token = undefined; + +// first stage the account +suite.addBatch({ + "account staging": { + topic: wsapi.post('/wsapi/stage_user', { + email: TEST_EMAIL, + site: 'fakesite.com' + }), + "works": function(err, r) { + assert.equal(r.code, 200); + } + } +}); + +// wait for the token +suite.addBatch({ + "a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "is obtained": function (t) { + assert.strictEqual(typeof t, 'string'); + token = t; + } + } +}); + +// create a new account via the api with (first address) +suite.addBatch({ + "setting password": { + topic: function() { + wsapi.post('/wsapi/complete_user_creation', { + token: token, + pass: PASSWORD + }).call(this); + }, + "works just fine": function(err, r) { + assert.equal(r.code, 200); + } + } +}); + +suite.addBatch({ + "authenticating with the password": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: TEST_EMAIL, + pass: PASSWORD, + ephemeral: true + }), + "works as expected": function(err, r) { + assert.strictEqual(JSON.parse(r.body).success, true); + } + } +}); + +suite.addBatch({ + "session length": { + topic: function() { + this.callback(wsapi.getCookie(/^browserid_state/)); + }, + "is short (ephemeral)": function(cookie) { + assert.equal(cookie.split('.')[3], config.get('ephemeral_session_duration_ms')); + } + } +}); + +suite.addBatch({ + "session prolonging": { + topic: wsapi.post('/wsapi/prolong_session', {}), + "returns 200": function(err, r) { + assert.strictEqual(r.code, 200); + } + } +}); + +suite.addBatch({ + "session length": { + topic: function() { + this.callback(wsapi.getCookie(/^browserid_state/)); + }, + "becomes long": function(cookie) { + assert.equal(cookie.split('.')[3], config.get('authentication_duration_ms')); + } + } +}); + +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js index cd40adba4c379ee97fa585626ba816ff46eb6cd7..fc37017c3f888c00bb90e1cd4b98a72748be3fb6 100755 --- a/tests/stalled-mysql-test.js +++ b/tests/stalled-mysql-test.js @@ -112,7 +112,8 @@ suite.addBatch({ "authenticate_user": { topic: wsapi.post('/wsapi/authenticate_user', { email: 'test@example.com', - pass: 'oogabooga' + pass: 'oogabooga', + ephemeral: false }), "fails with 503": function(err, r) { assert.strictEqual(r.code, 503); @@ -227,7 +228,8 @@ suite.addBatch({ "cert_key": { topic: wsapi.post('/wsapi/cert_key', { email: "test@whatev.er", - pubkey: "bogus" + pubkey: "bogus", + ephemeral: false }), "fails with 503": function(err, r) { assert.strictEqual(r.code, 503); @@ -364,7 +366,8 @@ suite.addBatch({ "auth_with_assertion": { topic: function() { wsapi.post('/wsapi/auth_with_assertion', { - assertion: g_assertion + assertion: g_assertion, + ephemeral: true }).call(this); }, "fails with 503": function(err, r) { diff --git a/tests/two-level-auth-test.js b/tests/two-level-auth-test.js index d73c3c812e9b78d5e3b6ca1e641fc9ba0b0e06c9..0353d89acc9c5b91cfdbddcfabffeb55b161292a 100755 --- a/tests/two-level-auth-test.js +++ b/tests/two-level-auth-test.js @@ -41,8 +41,8 @@ suite.addBatch({ "and logging in with the assertion": { topic: function(assertion) { wsapi.post('/wsapi/auth_with_assertion', { - email: TEST_EMAIL, - assertion: assertion + assertion: assertion, + ephemeral: true }).call(this); }, "succeeds": function(err, r) { diff --git a/tests/verifier-test.js b/tests/verifier-test.js index b2a455dc04807502630199c49333d5a9a42e7572..499855873f1eb3250de9603b7d79c8f390b71f60 100755 --- a/tests/verifier-test.js +++ b/tests/verifier-test.js @@ -97,7 +97,8 @@ suite.addBatch({ topic: function() { wsapi.post('/wsapi/cert_key', { email: TEST_EMAIL, - pubkey: g_keypair.publicKey.serialize() + pubkey: g_keypair.publicKey.serialize(), + ephemeral: false }).call(this); }, "works swimmingly": function(err, r) {