Newer
Older
/* 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/. */
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
Austin King
committed
// exports.i18n - boolean, does this operation display user facing strings
Lloyd Hilaiel
committed
const
sessions = require('client-sessions'),
Zachary Carter
committed
express = require('express'),
Lloyd Hilaiel
committed
secrets = require('./secrets'),
config = require('./configuration'),
logger = require('./logging.js').logger,
httputils = require('./httputils.js'),
Lloyd Hilaiel
committed
forward = require('./http_forward.js').forward,
Lloyd Hilaiel
committed
url = require('url'),
fs = require('fs'),
path = require('path'),
validate = require('./validate'),
Zachary Carter
committed
statsd = require('./statsd'),
bcrypt = require('./bcrypt'),
Austin King
committed
i18n = require('./i18n');
var abide = i18n.abide({
supported_languages: config.get('supported_languages'),
default_lang: config.get('default_lang'),
locale_directory: config.get('locale_directory'),
disable_locale_check: config.get('disable_locale_check')
Austin King
committed
});
Lloyd Hilaiel
committed
const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path'));
Lloyd Hilaiel
committed
var COOKIE_KEY = 'browserid_state';
// to support testing of browserid, we'll add a hash fragment to the cookie name for
Lloyd Hilaiel
committed
// sites other than login.persona.org. This is to address a bug in IE, see issue #296
if (config.get('public_url').indexOf('https://login.persona.org') !== 0) {
Lloyd Hilaiel
committed
const crypto = require('crypto');
var hash = crypto.createHash('md5');
hash.update(config.get('public_url'));
COOKIE_KEY += "_" + hash.digest('hex').slice(0, 6);
}
const WSAPI_PREFIX = '/wsapi/';
Lloyd Hilaiel
committed
logger.info('session cookie name is: ' + COOKIE_KEY);
Lloyd Hilaiel
committed
function clearAuthenticatedUser(session) {
session.reset(['csrf']);
Lloyd Hilaiel
committed
}
function isAuthed(req, requiredLevel) {
if (req.session && req.session.userid && req.session.auth_level) {
// 'password' authentication allows access to all apis.
// 'assertion' authentication, grants access to only those apis
// that don't require 'password'
if (requiredLevel === 'assertion' || req.session.auth_level === 'password') {
return true;
}
}
return false;
Lloyd Hilaiel
committed
}
function bcryptPassword(password, cb) {
var startTime = new Date();
bcrypt.encrypt(config.get('bcrypt_work_factor'), password, function() {
var reqTime = new Date - startTime;
statsd.timing('bcrypt.encrypt_time', reqTime);
cb.apply(null, arguments);
Lloyd Hilaiel
committed
});
}
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
function authenticateSession(session, uid, level, duration_ms) {
if (['assertion', 'password'].indexOf(level) === -1)
throw "invalid authentication level: " + level;
Lloyd Hilaiel
committed
// if the user is *already* authenticated as this uid with an equal or better
// level of auth, let's not lower them. Issue #1049
if (session.userid === uid && session.auth_level === 'password' &&
session.auth_level !== level) {
logger.info("not resetting cookies to 'assertion' authenticate a user who is already password authenticated");
Lloyd Hilaiel
committed
} else {
Lloyd Hilaiel
committed
if (duration_ms) {
session.setDuration(duration_ms);
}
Lloyd Hilaiel
committed
session.userid = uid;
session.auth_level = level;
}
Lloyd Hilaiel
committed
}
function checkPassword(pass) {
if (!pass || pass.length < 8 || pass.length > 80) {
return "valid passwords are between 8 and 80 chars";
}
}
Lloyd Hilaiel
committed
function langContext(req) {
return {
lang: req.lang,
locale: req.locale,
gettext: req.gettext,
ngettext: req.ngettext,
format: req.format
};
}
function databaseDown(res, err) {
logger.warn('database is down, cannot process request: ' + err);
httputils.serviceUnavailable(res, "database unavailable");
}
function operationFromURL (path) {
var purl = url.parse(path);
return purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX &&
purl.pathname.substr(WSAPI_PREFIX.length) || null;
}
var APIs;
function allAPIs () {
if (APIs) return APIs;
APIs = {};
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);
var api = require(path.join(__dirname, 'wsapi', f));
APIS[operation] = api;
});
return APIs;
}
Lloyd Hilaiel
committed
// common functions exported, for use by different api calls
exports.clearAuthenticatedUser = clearAuthenticatedUser;
exports.isAuthed = isAuthed;
exports.bcryptPassword = bcryptPassword;
exports.authenticateSession = authenticateSession;
exports.checkPassword = checkPassword;
Lloyd Hilaiel
committed
exports.langContext = langContext;
exports.databaseDown = databaseDown;
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
exports.setup = function(options, app) {
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
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
var cookieSessionMiddleware = sessions({
secret: COOKIE_SECRET,
cookieName: COOKIE_KEY,
Lloyd Hilaiel
committed
duration: config.get('authentication_duration_ms'),
Lloyd Hilaiel
committed
cookie: {
path: '/wsapi',
httpOnly: true,
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
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'])
Lloyd Hilaiel
committed
return httputils.badRequest(resp, "no such api");
Lloyd Hilaiel
committed
}
// 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") {
if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session
logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
return httputils.forbidden(resp, "no cookie");
}
// and the token must match what is sent in the post body
else if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
// if any of these things are false, then we'll block the request
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);
return httputils.badRequest(resp, "CSRF violation");
Lloyd Hilaiel
committed
}
}
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(", ");
}
if (op.internal) str += ' - internal';
str += ")";
logger.debug(str);
}
var all = allAPIs();
Object.keys(all).forEach(function (operation) {
Lloyd Hilaiel
committed
try {
var api = all[operation];
Lloyd Hilaiel
committed
// - don't register read apis if we are configured as a writer,
Lloyd Hilaiel
committed
// with the exception of ping which tests database connection health.
// - don't register write apis if we are not configured as a writer
if ((options.only_write_apis && !api.writes_db && operation != 'ping') ||
(!options.only_write_apis && api.writes_db))
return;
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
wsapis[operation] = api;
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) {
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) {
Lloyd Hilaiel
committed
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, wsapis[operation].authed)) {
Lloyd Hilaiel
committed
return httputils.badRequest(resp, "requires authentication");
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
// validate the arguments of the request
wsapis[operation].validate(req, resp, function() {
Austin King
committed
if (wsapis[operation].i18n) {
abide(req, resp, function () {
wsapis[operation].process(req, resp);
Austin King
committed
});
} else {
wsapis[operation].process(req, resp);
}
Lloyd Hilaiel
committed
});
Lloyd Hilaiel
committed
} else {
next();
}
});
};
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
exports.routeSetup = function (app, options) {
var wsapis = allAPIs();
app.use(function(req, resp, next) {
var operation = operationFromURL(req.url);
// not a WSAPI request
if (!operation) return next();
var api = wsapis[operation];
// check to see if the api is known here, before spending more time with
// the request.
if (!wsapis.hasOwnProperty(operation) ||
api.method.toLowerCase() !== req.method.toLowerCase()) {
// 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");
}
if (api.internal) {
return httputils.badRequest(resp, "internal api");
}
var destination_url = api.writes_db ? options.write_url + "/wsapi/" + operation
: options.read_url + req.url;
var cb = function() {
forward(
destination_url, req, resp,
function(err) {
if (err) {
logger.error("error forwarding request:", err);
}
});
};
return express.bodyParser()(req, resp, cb);
});
};