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'),
Lloyd Hilaiel
committed
translation_directory: config.get('translation_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
}
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));
});
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;
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) {
Zachary Carter
committed
var keys = Array.isArray(op.args) ? op.args : Object.keys(op.args);
str += " - " + keys.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) {
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();
}
});
};
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.notFound(resp);
}
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);
});
};