diff --git a/browserid/app.js b/browserid/app.js index b70899b148d12b1429fffce798061616a567d214..5b454608791035eb835bbebffa051ce45aa7db0f 100644 --- a/browserid/app.js +++ b/browserid/app.js @@ -37,7 +37,6 @@ const fs = require('fs'), path = require('path'), url = require('url'), -crypto = require('crypto'), wsapi = require('./lib/wsapi.js'), httputils = require('./lib/httputils.js'), webfinger = require('./lib/webfinger.js'), @@ -86,15 +85,6 @@ function router(app) { // simple redirects (internal for now) app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html')); - // return the CSRF token - // IMPORTANT: this should be safe because it's only readable by same-origin code - // but we must be careful that this is never a JSON structure that could be hijacked - // by a third party - app.get('/csrf', function(req, res) { - res.write(req.session.csrf); - res.end(); - }); - app.get('/', function(req,res) { res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true}); }); @@ -116,7 +106,7 @@ function router(app) { }); app.get(/^\/manage(\.html)?$/, function(req,res) { - res.render('manage.ejs', {title: 'My Account', fullpage: false, csrf: req.session.csrf}); + res.render('manage.ejs', {title: 'My Account', fullpage: false}); }); app.get(/^\/tos(\.html)?$/, function(req, res) { @@ -166,7 +156,7 @@ exports.setup = function(server) { secret: COOKIE_SECRET, key: COOKIE_KEY, cookie: { - path: '/', + path: '/wsapi', httpOnly: true, // IMPORTANT: we allow users to go 1 weeks on the same device // without entering their password again @@ -177,32 +167,27 @@ exports.setup = function(server) { // cookie sessions server.use(function(req, resp, next) { - // we set this parameter so the connect-cookie-session - // sends the cookie even though the local connection is HTTP - // (the load balancer does SSL) - if (overSSL) - req.connection.proxySecure = true; + // cookie sessions are only applied to calls to /wsapi + // as all other resources can be aggressively cached + // by layers higher up based on cache control headers. + // the fallout is that all code that interacts with sessions + // should be under /wsapi + if (/^\/wsapi/.test(req.url)) { + // we set this parameter so the connect-cookie-session + // sends the cookie even though the local connection is HTTP + // (the load balancer does SSL) + if (overSSL) + req.connection.proxySecure = true; + + return cookieSessionMiddleware(req, resp, next); - return cookieSessionMiddleware(req, resp, next); + } else { + return next(); + } }); server.use(express.bodyParser()); - // we make sure that everyone has a session, otherwise we can't do CSRF properly - server.use(function(req, resp, next) { - if (typeof req.session == 'undefined') - req.session = {}; - - if (typeof req.session.csrf == 'undefined') { - // FIXME: using express-csrf's approach for generating randomness - // not awesome, but probably sufficient for now. - req.session.csrf = crypto.createHash('md5').update('' + new Date().getTime()).digest('hex'); - logger.debug("NEW csrf token created: " + req.session.csrf); - } - - next(); - }); - // a tweak to get the content type of host-meta correct server.use(function(req, resp, next) { if (req.url === '/.well-known/host-meta') { @@ -226,18 +211,6 @@ exports.setup = function(server) { next(); }); - // check CSRF token - server.use(function(req, resp, next) { - // only on POSTs - if (req.method == "POST" && req.body.csrf != req.session.csrf) { - // error, problem with CSRF - logger.warn("CSRF token mismatch. got:" + req.body.csrf + " wanted:" + req.session.csrf); - httputils.badRequest(resp, "CSRF violation"); - } else { - next(); - } - }); - // add middleware to re-write urls if needed configuration.performSubstitution(server); diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js index ef14ecb6153d2a81ffcfbffde43bf69b79b9f979..d65382a9d1f252506e39c2ed01197dd626c7e734 100644 --- a/browserid/lib/wsapi.js +++ b/browserid/lib/wsapi.js @@ -44,6 +44,7 @@ url = require('url'), httputils = require('./httputils.js'); email = require('./email.js'), bcrypt = require('bcrypt'), +crypto = require('crypto'), logger = require('../../libs/logging.js').logger; function checkParams(params) { @@ -84,6 +85,40 @@ function checkAuthed(req, resp, next) { } function setup(app) { + // check CSRF token before routing the request to the proper handler + // (iff the request is to /wsapi AND it's a post) + app.use(function(req, resp, next) { + // only on POSTs to /wsapi + if (req.method == "POST" && /^\/wsapi/.test(req.url) && req.body.csrf != req.session.csrf) { + // error, problem with CSRF + logger.warn("CSRF token mismatch. got:" + req.body.csrf + " wanted:" + req.session.csrf); + httputils.badRequest(resp, "CSRF violation"); + } else { + next(); + } + }); + + // return the CSRF token + // IMPORTANT: this should be safe because it's only readable by same-origin code + // but we must be careful that this is never a JSON structure that could be hijacked + // by a third party + app.get('/wsapi/csrf', function(req, res) { + if (typeof req.session == 'undefined') { + req.session = {}; + } + + if (typeof req.session.csrf == 'undefined') { + // FIXME: using express-csrf's approach for generating randomness + // not awesome, but probably sufficient for now. + req.session.csrf = crypto.createHash('md5').update('' + new Date().getTime()).digest('hex'); + logger.debug("NEW csrf token created: " + req.session.csrf); + } + + res.write(req.session.csrf); + res.end(); + }); + + /* checks to see if an email address is known to the server * takes 'email' as a GET argument */ app.get('/wsapi/have_email', function(req, resp) { diff --git a/browserid/static/dialog/resources/browserid-network.js b/browserid/static/dialog/resources/browserid-network.js index 31cc0f8921ab0f4277f79615a47400ad8c83e1cc..aaa3f75412d972379b7636252f8f9bfb19110997 100644 --- a/browserid/static/dialog/resources/browserid-network.js +++ b/browserid/static/dialog/resources/browserid-network.js @@ -38,7 +38,7 @@ var BrowserIDNetwork = (function() { var Network = { csrf: function(onSuccess) { - $.get('/csrf', {}, function(result) { + $.get('/wsapi/csrf', {}, function(result) { BrowserIDNetwork.csrf_token = result; if(onSuccess) { onSuccess();