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
// this file is an abstraction around "primary identity authority" support,
// specifically checks and a cache to see if a primary supports browserid
// natively.
const
https = require('https'),
http = require('http'),
Lloyd Hilaiel
committed
logger = require('./logging.js').logger,
Lloyd Hilaiel
committed
urlparse = require('urlparse'),
Lloyd Hilaiel
committed
jwk = require('jwcrypto/jwk'),
jwcert = require("jwcrypto/jwcert"),
vep = require("jwcrypto/vep"),
Lloyd Hilaiel
committed
jwt = require("jwcrypto/jwt"),
config = require("./configuration.js");
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
const WELL_KNOWN_URL = "/.well-known/browserid";
Lloyd Hilaiel
committed
// Protect from stack overflows and network DDOS attacks
const MAX_AUTHORITY_DELEGATIONS = 6;
const HOSTNAME = urlparse(config.get('public_url')).host;
// This becomes async
function parseWellKnownBody(body, domain, delegates, cb) {
Lloyd Hilaiel
committed
var v = JSON.parse(body);
const want = [ 'public-key', 'authentication', 'provisioning' ];
var got = [];
if (typeof v === 'object') {
got = Object.keys(v);
}
var bail = false;
got.forEach(function (k) {
if ('authority' === k) {
// Recursion
var dels = Object.keys(delegates);
if (delegates[domain] !== undefined) {
// return to break out of function, but callbacks are actual program flow
bail = true;
return cb("Circular reference in delegating authority " + JSON.stringify(delegates));
}
if (Object.keys(delegates).length > MAX_AUTHORITY_DELEGATIONS) {
bail = true;
return cb("Too many hops while delegating authority " + JSON.stringify(dels));
}
logger.debug(domain,' is delegating to', v[k]);
// recurse into low level get /.well-known/browserid and parse again?
// If everything goes well, finally call our original callback
delegates[domain] = dels.length;
getWellKnown(v[k], delegates, function (err, nbody, ndomain, ndelegates) {
if (err) {
cb(err);
}
parseWellKnownBody(nbody, ndomain, ndelegates, cb);
});
bail = true;;
}
});
if (bail) return;
Lloyd Hilaiel
committed
want.forEach(function(k) {
if (-1 === got.indexOf(k)) {
cb("missing required key: " + k);
bail = true;
}
Lloyd Hilaiel
committed
});
if (bail) return;
Lloyd Hilaiel
committed
// Allow SHIMMED_PRIMARIES to change example.com into 127.0.0.1:10005
var url_prefix = 'https://' + domain;
if (g_shim_cache[domain]) {
url_prefix = g_shim_cache[domain].origin;
}
Lloyd Hilaiel
committed
var urls = {
auth: url_prefix + v.authentication,
prov: url_prefix + v.provisioning,
Lloyd Hilaiel
committed
};
// validate the urls
urlparse(urls.auth).validate();
urlparse(urls.prov).validate();
// parse the public key
return cb(null, {
Lloyd Hilaiel
committed
publicKey: jwk.PublicKey.fromSimpleObject(v['public-key']),
urls: urls
Lloyd Hilaiel
committed
}
Lloyd Hilaiel
committed
Lloyd Hilaiel
committed
// Support "shimmed primaries" for local development. That is an environment variable that is any number of
// CSV values of the form:
// <domain>|<origin>|<path to .well-known/browserid>,
// where 'domain' is the domain that we would like to shim. 'origin' is the origin to which traffic should
// be directed, and 'path to .well-known/browserid' is a path to the browserid file for the domain
//
// defining this env var will pre-seed the cache so local testing can take place. example:
//
// SHIMMED_PRIMARIES=eyedee.me|http://127.0.0.1:10005|example/primary/.well-known/browserid
Lloyd Hilaiel
committed
if (process.env['SHIMMED_PRIMARIES']) {
var shims = process.env['SHIMMED_PRIMARIES'].split(',');
shims.forEach(function(shim) {
var a = shim.split('|');
var domain = a[0], origin = a[1], path = a[2];
var body = require('fs').readFileSync(path);
g_shim_cache[domain] = {
origin: origin,
body: body
Lloyd Hilaiel
committed
};
logger.info("inserted primary info for '" + domain + "' into cache, TODO point at '" + origin + "'");
});
}
Lloyd Hilaiel
committed
var getWellKnown = function (domain, delegates, cb) {
function handleResponse(res) {
Lloyd Hilaiel
committed
if (res.statusCode !== 200) {
logger.debug(domain + ' is not a browserid primary - non-200 response code to ' + WELL_KNOWN_URL);
return cb(null, false, null);
Lloyd Hilaiel
committed
}
if (res.headers['content-type'].indexOf('application/json') !== 0) {
logger.debug(domain + ' is not a browserid primary - non "application/json" response to ' + WELL_KNOWN_URL);
return cb(null, false, null);
Lloyd Hilaiel
committed
}
var body = "";
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
cb(null, body, domain, delegates);
Lloyd Hilaiel
committed
});
};
return cb(null, g_shim_cache[domain].body, domain, delegates);
// now we need to check to see if domain purports to being a primary
// for browserid
var httpProxy = config.has('http_proxy') ? config.get('http_proxy') : null;
var req;
if (httpProxy && httpProxy.port && httpProxy.host) {
// In production we use Squid as a reverse proxy cache to reduce how often
// we request this resource.
req = http.get({
host: httpProxy.host,
port: httpProxy.port,
path: 'https://' + domain + WELL_KNOWN_URL,
headers: {
host: domain
}
}, handleResponse);
} else {
req = https.get({
host: domain,
path: WELL_KNOWN_URL,
agent: false
}, handleResponse);
}
req.on('error', function(e) {
logger.debug(domain + ' is not a browserid primary: ' + e.toString());
cb(null, false, null);
Lloyd Hilaiel
committed
});
};
Lloyd Hilaiel
committed
exports.checkSupport = function(domain, cb, delegates) {
// Delegates will be populatd via recursion to detect cycles
if (! delegates) {
delegates = {};
}
if (!cb) throw "missing required callback function";
if (config.get('disable_primary_support')) {
return process.nextTick(function() { cb(null, false); });
}
if (typeof domain !== 'string' || !domain.length) {
return process.nextTick(function() { cb("invalid domain"); });
}
getWellKnown(domain, delegates, function (err, body, domain, delegates) {
if (err) {
logger.debug(err);
return cb(err);
} else {
if (! body) {
return cb(null, null, null);
}
try {
var r = parseWellKnownBody(body, domain, delegates, function (err, r) {
if (err) {
logger.debug(err);
cb(err);
} else {
logger.info(domain + ' is a valid browserid primary');
return cb(null, r.urls, r.publicKey);
}
});
} catch(e) {
var msg = domain + ' is a broken browserid primary, malformed dec of support: ' + e.toString();
logger.debug(msg);
return cb(msg);
}
}
});
};
exports.getPublicKey = function(domain, cb) {
exports.checkSupport(domain, function(err, urls, publicKey) {
if (publicKey === null) {
return cb("can't get public key for " + domain);
}
});
};
Lloyd Hilaiel
committed
// verify an assertion generated to authenticate to browserid
exports.verifyAssertion = function(assertion, cb) {
Lloyd Hilaiel
committed
if (config.get('disable_primary_support')) {
return process.nextTick(function() { cb("primary support disabled") });
}
Lloyd Hilaiel
committed
try {
var bundle = vep.unbundleCertsAndAssertion(assertion);
} catch(e) {
return process.nextTick(function() { cb("malformed assertion: " + e); });
}
jwcert.JWCert.verifyChain(
bundle.certificates,
new Date(), function(issuer, next) {
// issuer cannot be the browserid
if (issuer === HOSTNAME) {
Lloyd Hilaiel
committed
cb("cannot authenticate to browserid with a certificate issued by it.");
} else {
exports.getPublicKey(issuer, function(err, pubKey) {
Lloyd Hilaiel
committed
if (err) return cb(err);
next(pubKey);
});
}
}, function(pk, principal) {
try {
var tok = new jwt.JWT();
tok.parse(bundle.assertion);
// audience must be browserid itself
var want = urlparse(config.get('public_url')).originOnly();
Lloyd Hilaiel
committed
var got = urlparse(tok.audience).originOnly();
if (want.toString() !== got.toString()) {
return cb("can't log in with an assertion for '" + got.toString() + "'");
}
if (!tok.verify(pk)) throw "verification failure";
cb(null, principal.email);
} catch(e) {
cb("can't verify assertion: " + e.toString());
}
}, cb);