Skip to content
Permalink
8b51f42e3c
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
205 lines (185 sloc) 6.72 KB
/* 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/. */
const
http = require("http"),
https = require("https"),
url = require("url"),
jwcrypto = require("jwcrypto"),
urlparse = require('urlparse'),
compareAudiences = require('./compare-audiences.js'),
util = require('util');
require("jwcrypto/lib/algs/ds");
require("jwcrypto/lib/algs/rs");
// a list of reserved claims from jwt's spec and including
// browserid reserved (but "private") claims.
const jwtRegisteredClaimNames = [
'iss',
'sub',
'aud',
'exp',
'nbf',
'iat',
'jti',
'public-key',
'principal'
];
// given a payload (from assertion or certificate), extract
// "extra" claims embedded therein. To conform with JWT, these are
// assumed to be un-recognized top level properties. A historical exception
// is 'principal'. If principal is an object, all claims other than 'email'
// will be extracted and returned as if they were proper top level jwt
// extensions.
function extractExtraClaims(claims) {
var extraClaims = {};
Object.keys(claims).forEach(function(key) {
if (jwtRegisteredClaimNames.indexOf(key) === -1) {
extraClaims[key] = claims[key];
}
});
// now extract unknown fields from 'principal' object as if they
// were proper top level extensions
if (typeof claims.principal === 'object') {
Object.keys(claims.principal).forEach(function(key) {
// only overlay non-reserved, non 'email', embedded claims
// that do not exist at the top level.
if (jwtRegisteredClaimNames.indexOf(key) === -1 &&
key !== 'email' &&
!extraClaims[key]) {
extraClaims[key] = claims.principal[key];
}
});
}
return Object.keys(extraClaims).length ? extraClaims : null;
}
function extractDomainFromEmail(email) {
return (/\@(.*)$/).exec(email)[1].toLowerCase();
}
// XXX document me!
// XXX allow variation of paramters on a per-call basis
function verify(browserid, args, cb) {
if (arguments.length !== 3) {
throw "wrong number of arguments";
}
if (!args.assertion) {
throw "missing required 'assertion' argument";
}
if (!args.audience) {
throw "missing required 'audience' argument";
}
var assertion = args.assertion;
var audience = args.audience;
var ultimateIssuer;
// we will manually unpack the certificate (IdP issued) and assertion
// (UA generated). jwcrypto's API's are insufficient to allow us to relay
// claims made by each back to the user, which limits extensibility.
var idpClaims, uaClaims;
// first we must determine the principal email that this assertion vouches for.
// BrowserID support document lookup requires that support documents are fetched
// with the domain of the principal email to allow IdP's to serve dynamic
// support documents.
var principalDomain = null;
try {
var email = null;
var bundle = jwcrypto.cert.unbundle(assertion);
// idp's claims come from the last certificate in the chain.
idpClaims = jwcrypto.extractComponents(bundle.certs[bundle.certs.length - 1]).payload;
uaClaims = jwcrypto.extractComponents(bundle.signedAssertion).payload;
if (idpClaims.principal) {
email = idpClaims.principal.email;
}
if (!email) {
email = idpClaims.sub;
}
principalDomain = extractDomainFromEmail(email);
} catch(e) {
// if we fail to extract principle domain, we will rely on subsequent verification
// logic to determine whether this is an assertion *without* an email, and if it
// can be trusted...
}
// XXX: make verification time configurable. Allow the caller to pass in the
// time for which the assertion should be checked for validity, and in the absence
// of that, let's use current javascript time.
//
// This facilitates testing.
jwcrypto.cert.verifyBundle(
assertion,
args.now || new Date(),
function(issuer, next) {
// update issuer with each issuer in the chain, so the
// returned issuer will be the last cert in the chain
ultimateIssuer = issuer;
// let's go fetch the public key for this issuer
browserid.lookup({
domain: issuer,
principalDomain: principalDomain
} , function(err, details) {
if (err) {
return cb(err);
}
next(null, details.publicKey);
});
}, function(err, certParamsArray, payload, assertionParams) {
if (err) {
return cb(err);
}
// for now, to be extra safe, we don't allow cert chains
if (certParamsArray.length > 1) {
return cb("certificate chaining is not yet allowed");
}
// audience must match!
err = compareAudiences(assertionParams.audience, audience);
if (err) {
return cb("audience mismatch: " + err);
}
// build up a response object
var obj = {
audience: assertionParams.audience,
expires: assertionParams.expiresAt,
issuer: ultimateIssuer
};
if (idpClaims && idpClaims.principal && idpClaims.principal.email) {
obj.email = idpClaims.principal.email;
}
// extract extra idp claims
var extClaims = extractExtraClaims(idpClaims);
if (extClaims) {
obj.idpClaims = extClaims;
}
// extract extra ua claims
extClaims = extractExtraClaims(uaClaims);
if (extClaims) {
obj.uaClaims = extClaims;
}
// If the caller has expressed trust in a set of issuers, then we need not verify
// that those issuers can speak for the principal.
if (args.trustedIssuers && args.trustedIssuers.indexOf(ultimateIssuer) !== -1) {
cb(null, obj);
}
// otherwise, if there is an email embedded in the assertion, we must lookup the
// expected issuer (by the BrowserID protocol) for that email domain.
else if (principalDomain) {
browserid.lookup({
domain: principalDomain,
principalDomain: principalDomain
}, function(err, details) {
var expectedIssuer = args.fallback;
if (!err && details.authoritativeDomain) {
expectedIssuer = details.authoritativeDomain;
}
if (expectedIssuer !== ultimateIssuer) {
cb(util.format("untrusted issuer, expected '%s', got '%s'",
expectedIssuer, ultimateIssuer));
} else {
cb(null, obj);
}
});
}
// otherwise, if there is an email embedded in the assertion, we must lookup the
// expected issuer (by the BrowserID protocol) for that email domain.
else {
cb("untrusted assertion, doesn't contain an email, and issuer is untrusted ");
}
});
}
module.exports = verify;