/
verify.js
205 lines (185 loc) · 6.72 KB
/
verify.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/* 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;