diff --git a/bin/browserid b/bin/browserid index 6f23273cbcb5e5c86adc231d3831e548b7175c6b..03fa955e44d9c29e754dee7b3bc5721ec32c2155 100755 --- a/bin/browserid +++ b/bin/browserid @@ -24,7 +24,7 @@ config = require('../lib/configuration.js'), heartbeat = require('../lib/heartbeat.js'), metrics = require('../lib/metrics.js'), logger = require('../lib/logging.js').logger, -forward = require('../lib/http_forward'), +forward = require('../lib/http_forward').forward, shutdown = require('../lib/shutdown'), views = require('../lib/browserid/views.js'); diff --git a/bin/proxy b/bin/proxy index 02b2da157cdbf29799957d5d764aec5cdf2e45ce..ce26d11277480ae7ea312db34bcf5c17da061a24 100755 --- a/bin/proxy +++ b/bin/proxy @@ -14,6 +14,9 @@ config = require('../lib/configuration.js'); var port = config.has('bind_to.port') ? config.get('bind_to.port') : 0; var addy = config.has('bind_to.host') ? config.get('bind_to.host') : "127.0.0.1"; +// set a maximum allowed time on responses to declaration of support requests +forward.setTimeout(config.get('declaration_of_support_timeout_ms')); + const allowed = /^https:\/\/[a-zA-Z0-9\.\-_]+\/\.well-known\/browserid$/; var server = http.createServer(function (req, res) { @@ -24,7 +27,7 @@ var server = http.createServer(function (req, res) { return; } - forward(url, req, res, function(err) { + forward.forward(url, req, res, function(err) { if (err) { res.writeHead(400); res.end('Oops: ' + err.toString()); diff --git a/lib/configuration.js b/lib/configuration.js index 8ba0fabc93cc20943e9df3bc9d10cda75c97c627..c414472c78173816225ce90a4ee205e79d0a3618 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -187,7 +187,11 @@ var conf = module.exports = convict({ env: 'DBWRITER_URL' }, process_type: 'string', - email_to_console: 'boolean = false' + email_to_console: 'boolean = false', + declaration_of_support_timeout_ms: { + doc: "The amount of time we wait for a server to respond with a declaration of support, before concluding that they are not a primary. Only relevant when our local proxy is in use, not in production or staging", + format: 'integer = 15000' + } }); // At the time this file is required, we'll determine the "process name" for this proc diff --git a/lib/http_forward.js b/lib/http_forward.js index 5277aa95643a31659a9a45085902e5b31cc37065..d88cbd85de18c4f97f85fbb563fc0929dc05c30d 100644 --- a/lib/http_forward.js +++ b/lib/http_forward.js @@ -9,7 +9,21 @@ https = require('https'), logger = require('./logging.js').logger, querystring = require('querystring'); -module.exports = function(dest, req, res, cb) { +var global_forward_timeout = undefined; + +exports.setTimeout = function(to) { + if (typeof to != 'number') throw "setTimeout expects a numeric argument"; + global_forward_timeout = to; +}; + +exports.forward = function(dest, req, res, cb) { + var _cb = cb; + var requestTimeout = undefined; + cb = function() { + if (requestTimeout) clearTimeout(requestTimeout); + if (_cb) _cb.apply(null, arguments); + } + function cleanupReq() { if (preq) { preq.removeAllListeners(); @@ -56,6 +70,10 @@ module.exports = function(dest, req, res, cb) { cb(e); }); + if (global_forward_timeout) { + requestTimeout = setTimeout(function() { preq.destroy(); }, global_forward_timeout); + } + if (req.headers['content-type']) { preq.setHeader('Content-Type', req.headers['content-type']); } diff --git a/lib/primary.js b/lib/primary.js index 55861ece63a6614d02107bf4b4927705d3031e83..5e7f481d43acbc617408affb594cadad418190d4 100644 --- a/lib/primary.js +++ b/lib/primary.js @@ -186,7 +186,7 @@ exports.checkSupport = function(domain, cb, delegates) { if (typeof domain !== 'string' || !domain.length) { return process.nextTick(function() { cb("invalid domain"); }); } - getWellKnown(domain, delegates, function (err, body, domain, delegates) { + getWellKnown(domain, delegates, function (err, body, domain, cbdelegates) { if (err) { logger.debug(err); return cb(err); @@ -196,7 +196,7 @@ exports.checkSupport = function(domain, cb, delegates) { } try { - var r = parseWellKnownBody(body, domain, delegates, function (err, r) { + var r = parseWellKnownBody(body, domain, cbdelegates, function (err, r) { if (err) { logger.debug(err); cb(err); @@ -226,6 +226,18 @@ exports.getPublicKey = function(domain, cb) { }); }; +// Does emailDomain actual delegate to the issuingDomain? +exports.delegatesAuthority = function (emailDomain, issuingDomain, cb) { + exports.checkSupport(emailDomain, function(err, urls, publicKey) { + // Check http or https://{issuingDomain}/some/sign_in_path + if (! err && urls && urls.auth && + urls.auth.indexOf('://' + issuingDomain + '/') !== -1) { + cb(true); + } + cb(false); + }); +} + // verify an assertion generated to authenticate to browserid exports.verifyAssertion = function(assertion, cb) { if (config.get('disable_primary_support')) { diff --git a/lib/verifier/certassertion.js b/lib/verifier/certassertion.js index b437fe4f9abbc5ecbcb38b66ae71e21557ceda50..babd1eae4e55096bad33b0016b243c876ff2a521 100644 --- a/lib/verifier/certassertion.js +++ b/lib/verifier/certassertion.js @@ -134,20 +134,30 @@ function verify(assertion, audience, successCB, errorCB) { return errorCB("audience mismatch: " + err); } - // verify that the issuer is the same as the email domain - // NOTE: for "delegation of authority" support we'll need to make this check - // more sophisticated + var token_verify = function (tok, pk, principal, ultimateIssuer) { + if (tok.verify(pk)) { + return successCB(principal.email, tok.audience, tok.expires, ultimateIssuer); + } else { + return errorCB("verification failure"); + } + } + + // verify that the issuer is the same as the email domain or + // that the email's domain delegated authority to the issuer var domainFromEmail = principal.email.replace(/^.*@/, ''); + if (ultimateIssuer != HOSTNAME && ultimateIssuer !== domainFromEmail) { - return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '" - + domainFromEmail + "'"); - } - - if (tok.verify(pk)) { - successCB(principal.email, tok.audience, tok.expires, ultimateIssuer); + primary.delegatesAuthority(domainFromEmail, ultimateIssuer, function (delegated) { + if (delegated) { + return token_verify(tok, pk, principal, ultimateIssuer); + } else { + return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '" + + domainFromEmail + "'"); + } + }); } else { - errorCB("verification failure"); + return token_verify(tok, pk, principal, ultimateIssuer); } }, errorCB); }; diff --git a/lib/wsapi.js b/lib/wsapi.js index c76c7e3e23bf501492316c40eb4dcf1b3a4c4c0a..7b736425feb5113c86a8f8d9ca91fddfe3204e87 100644 --- a/lib/wsapi.js +++ b/lib/wsapi.js @@ -22,7 +22,7 @@ secrets = require('./secrets'), config = require('./configuration'), logger = require('./logging.js').logger, httputils = require('./httputils.js'), -forward = require('./http_forward.js'), +forward = require('./http_forward.js').forward, url = require('url'), fs = require('fs'), path = require('path'), diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js index c1d734bdb4269fae0b2ec62d9688b9d3e6ade1c4..778bd4291953da0cc9af22988a039bd80e67cab3 100644 --- a/lib/wsapi/address_info.js +++ b/lib/wsapi/address_info.js @@ -31,7 +31,7 @@ exports.process = function(req, res) { return httputils.badRequest(res, "invalid email address"); } - primary.checkSupport(m[1], function(err, urls, publicKey) { + primary.checkSupport(m[1], function(err, urls, publicKey, delegates) { if (err) { logger.warn('error checking "' + m[1] + '" for primary support: ' + err); return httputils.serverError(res, "can't check email address"); diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js index 777d61223b6732ab620c66325baa9975eac891f5..0f0c81ef3f8d7336db02f0ad90a6719fcc086957 100644 --- a/lib/wsapi/cert_key.js +++ b/lib/wsapi/cert_key.js @@ -6,7 +6,7 @@ const db = require('../db.js'), httputils = require('../httputils'), logger = require('../logging.js').logger, -forward = require('../http_forward.js'), +forward = require('../http_forward.js').forward, config = require('../configuration.js'), urlparse = require('urlparse'), wsapi = require('../wsapi.js');