Newer
Older
Lloyd Hilaiel
committed
// an abstraction that implements all of the cookie handling, CSRF protection,
// etc of the wsapi. This module also routes request to the approriate handlers
// underneath wsapi/
//
// each handler under wsapi/ supports the following exports:
// exports.process - function(req, res) - process a request
// exports.writes_db - must be true if the processing causes a database write
// exports.method - either 'get' or 'post'
// exports.authed - whether the wsapi requires authentication
// exports.args - an array of arguments that should be verified
const
sessions = require('connect-cookie-session'),
express = require('express');
secrets = require('./secrets'),
config = require('./configuration'),
logger = require('./logging.js').logger,
httputils = require('./httputils.js'),
Lloyd Hilaiel
committed
forward = require('./http_forward.js'),
Lloyd Hilaiel
committed
url = require('url'),
fs = require('fs'),
path = require('path'),
validate = require('./validate'),
bcrypt = require('bcrypt'),
statsd = require('./statsd');
Lloyd Hilaiel
committed
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path'));
const COOKIE_KEY = 'browserid_state';
function clearAuthenticatedUser(session) {
Object.keys(session).forEach(function(k) {
if (k !== 'csrf') delete session[k];
});
}
function isAuthed(req) {
var who;
try {
if (req.session.authenticatedUser) {
if (!Date.parse(req.session.authenticatedAt) > 0) throw "bad timestamp";
if (new Date() - new Date(req.session.authenticatedAt) >
config.get('authentication_duration_ms'))
{
throw "expired";
}
who = req.session.authenticatedUser;
}
} catch(e) {
logger.debug("Session authentication has expired:", e);
clearAuthenticatedUser(req.session);
}
return who;
}
function bcryptPassword(password, cb) {
var bcryptWorkFactor = config.get('bcrypt_work_factor');
bcrypt.gen_salt(bcryptWorkFactor, function (err, salt) {
if (err) {
var msg = "error generating salt with bcrypt: " + err;
logger.error(msg);
return cb(msg);
}
bcrypt.encrypt(password, salt, function(err, hash) {
if (err) {
var msg = "error generating password hash with bcrypt: " + err;
logger.error(msg);
return cb(msg);
}
return cb(undefined, hash);
});
});
};
function setAuthenticatedUser(session, email) {
session.authenticatedUser = email;
session.authenticatedAt = new Date();
}
// common functions exported, for use by different api calls
exports.clearAuthenticatedUser = clearAuthenticatedUser;
exports.isAuthed = isAuthed;
exports.bcryptPassword = bcryptPassword;
exports.setAuthenticatedUser = setAuthenticatedUser;
Lloyd Hilaiel
committed
exports.fowardWritesTo = undefined;
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
exports.setup = function(options, app) {
Lloyd Hilaiel
committed
const WSAPI_PREFIX = '/wsapi/';
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// all operations that are being forwarded
var forwardedOperations = [];
Lloyd Hilaiel
committed
// If externally we're serving content over SSL we can enable things
// like strict transport security and change the way cookies are set
const overSSL = (config.get('scheme') == 'https');
Lloyd Hilaiel
committed
var cookieParser = express.cookieParser();
var bodyParser = express.bodyParser();
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// stash our forward-to url so different wsapi handlers can use it
exports.fowardWritesTo = options.forward_writes;
Lloyd Hilaiel
committed
var cookieSessionMiddleware = sessions({
secret: COOKIE_SECRET,
key: COOKIE_KEY,
cookie: {
path: '/wsapi',
httpOnly: true,
// IMPORTANT: we allow users to go 1 weeks on the same device
// without entering their password again
maxAge: config.get('authentication_duration_ms'),
secure: overSSL
}
});
app.use(function(req, resp, next) {
Lloyd Hilaiel
committed
var purl = url.parse(req.url);
Lloyd Hilaiel
committed
// 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
Lloyd Hilaiel
committed
if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX)
{
Lloyd Hilaiel
committed
// explicitly disallow caching on all /wsapi calls (issue #294)
resp.setHeader('Cache-Control', 'no-cache, max-age=0');
// we set this parameter so the connect-cookie-session
// sends the cookie even though the local connection is HTTP
// (the load balancer does SSL)
if (overSSL)
req.connection.proxySecure = true;
Lloyd Hilaiel
committed
const operation = purl.pathname.substr(WSAPI_PREFIX.length);
Lloyd Hilaiel
committed
// count the number of WSAPI operation
statsd.increment("wsapi." + operation);
Lloyd Hilaiel
committed
// check to see if the api is known here, before spending more time with
// the request.
if (!wsapis.hasOwnProperty(operation) ||
wsapis[operation].method.toLowerCase() !== req.method.toLowerCase())
{
Lloyd Hilaiel
committed
// if the fake verification api is enabled (for load testing),
// then let this request fall through
if (operation !== 'fake_verification' || !process.env['BROWSERID_FAKE_VERIFICATION'])
return httputils.badRequest(resp, "no such api");
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
// if this request is to be forwarded, we will not perform request validation,
// cookie parsing, nor body parsing - leaving that up to the process we're forwarding
// to.
if (-1 !== forwardedOperations.indexOf(operation)) {
// queue up the body here on and forward a single unchunked request onto the
// writer
return bodyParser(req, resp, function() {
next();
});
Lloyd Hilaiel
committed
} else {
// this is not a forwarded operation, perform full parsing and validation
return cookieParser(req, resp, function() {
bodyParser(req, resp, function() {
cookieSessionMiddleware(req, resp, function() {
// only on POSTs
if (req.method === "POST") {
var denied = false;
if (req.session === undefined) { // there must be a session
denied = true;
logger.warn("CSRF validation failure: POST calls to /wsapi require an active session");
}
// the session must have a csrf token
else if (typeof req.session.csrf !== 'string') {
denied = true;
logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set");
}
// and the token must match what is sent in the post body
Lloyd Hilaiel
committed
else if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
Lloyd Hilaiel
committed
denied = true;
// if any of these things are false, then we'll block the request
Lloyd Hilaiel
committed
var b = req.body ? req.body.csrf : "<none>";
var s = req.session ? req.session.csrf : "<none>";
logger.warn("CSRF validation failure, token mismatch. got:" + b + " want:" + s);
Lloyd Hilaiel
committed
}
if (denied) return httputils.badRequest(resp, "CSRF violation");
}
return next();
});
});
});
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
} else {
return next();
Lloyd Hilaiel
committed
}
});
// load all of the APIs supported by this process
var wsapis = { };
function describeOperation(name, op) {
var str = " " + name + " (";
str += op.method.toUpperCase() + " - ";
str += (op.authed ? "" : "not ") + "authed";
if (op.args) {
str += " - " + op.args.join(", ");
}
str += ")";
logger.debug(str);
}
Lloyd Hilaiel
committed
fs.readdirSync(path.join(__dirname, 'wsapi')).forEach(function (f) {
// skip files that don't have a .js suffix or start with a dot
if (f.length <= 3 || f.substr(-3) !== '.js' || f.substr(0,1) === '.') return;
var operation = f.substr(0, f.length - 3);
try {
var api = require(path.join(__dirname, 'wsapi', f));
Lloyd Hilaiel
committed
// don't register read apis if we are configured as a writer
if (options.only_write_apis && !api.writes_db) return;
Lloyd Hilaiel
committed
wsapis[operation] = api;
Lloyd Hilaiel
committed
// forward writes if options.forward_writes is defined
if (options.forward_writes && wsapis[operation].writes_db) {
Lloyd Hilaiel
committed
forwardedOperations.push(operation);
Lloyd Hilaiel
committed
var forward_url = options.forward_writes + "wsapi/" + operation;
wsapis[operation].process = function(req, res) {
forward(forward_url, req, res, function(err) {
if (err) {
logger.error("error forwarding '"+ operation +
"' request to '" + options.forward_writes + ":" + err);
httputils.serverError(res, "internal request forwarding error");
}
});
};
Lloyd Hilaiel
committed
// XXX: disable validation on forwarded requests
// (we cannot perform this validation because we don't parse cookies
// nor post bodies on forwarded requests)
//
// at some point we'll want to improve our cookie parser and
// fully validate forwarded requests both at the intermediate
// hop (webhead) AND final destination (secure webhead)
delete api.args; // deleting args will cause arg validation to be skipped
api.authed = false; // authed=false will prevent us from checking auth status
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
// set up the argument validator
if (api.args) {
if (!Array.isArray(api.args)) throw "exports.args must be an array of strings";
wsapis[operation].validate = validate(api.args);
} else {
wsapis[operation].validate = function(req,res,next) { next(); };
}
Lloyd Hilaiel
committed
} catch(e) {
var msg = "error registering " + operation + " api: " + e;
logger.error(msg);
throw msg;
}
});
Lloyd Hilaiel
committed
// debug output - all supported apis
logger.debug("WSAPIs:");
Object.keys(wsapis).forEach(function(api) {
if (options.forward_writes && wsapis[api].writes_db) return;
describeOperation(api, wsapis[api]);
Lloyd Hilaiel
committed
});
if (options.forward_writes) {
Lloyd Hilaiel
committed
logger.debug("forwarded WSAPIs (to " + options.forward_writes + "):");
Object.keys(wsapis).forEach(function(api) {
if (wsapis[api].writes_db) {
describeOperation(api, wsapis[api]);
}
});
}
Lloyd Hilaiel
committed
app.use(function(req, resp, next) {
var purl = url.parse(req.url);
if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) {
const operation = purl.pathname.substr(WSAPI_PREFIX.length);
Lloyd Hilaiel
committed
// the fake_verification wsapi is implemented elsewhere.
if (operation == 'fake_verification') return next();
Lloyd Hilaiel
committed
// at this point, we *know* 'operation' is valid API, give checks performed
// above
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// does the request require authentication?
if (wsapis[operation].authed && !isAuthed(req)) {
return httputils.badRequest(resp, "requires authentication");
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
// validate the arguments of the request
wsapis[operation].validate(req, resp, function() {
wsapis[operation].process(req, resp);
});
Lloyd Hilaiel
committed
} else {
next();
}
});
};