This repository has been archived by the owner on Feb 20, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
/
routes.js
504 lines (431 loc) · 15.6 KB
/
routes.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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
/* 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
pinCode = require('./lib/pin_code'),
certify = require('./lib/certifier'),
config = require('./lib/configuration'),
crypto = require('./lib/crypto.js'),
emailer = require('./lib/email'),
logger = require('./lib/logging').logger,
passport = require('passport'),
request = require('request'),
statsd = require('./lib/statsd'),
session = require('./lib/session_context'),
util = require('util'),
valid_email = require('./lib/validation/email');
exports.init = function(app) {
var well_known_last_mod = new Date().getTime();
var baseUrl = util.format("https://%s", config.get('issuer'));
// see issue #169
// appropriate for all templates that do not change between server
// restarts (no user-specific data) - forces re-validation and causes
// caches to not conflate different locales to support instant locale
// switching.
function addHeadersToForceRevalidation(res) {
res.setHeader('Vary', 'Accept-Encoding, Accept-Language');
res.setHeader('Cache-Control', 'public, max-age=0');
}
// see issues #165 and #168
// appropriate for all resources that are user-specific - i.e. javascript
// or html files that have embedded user-specific data or errors in them.
// This header should prevent all browsers from caching these resources
// ever.
function addHeadersToPreventCaching(res) {
res.setHeader('Cache-Control', 'private, max-age=0, no-cache, no-store');
}
app.use(function(req, res, next) {
res.locals({
browserid_server: config.get('browserid_server'),
dev_mode: config.get('env'),
issuer: config.get('issuer'),
layout: false
});
next();
});
app.get('/authentication', function(req, res) {
addHeadersToForceRevalidation(res);
var start = new Date();
statsd.increment('routes.authentication.get');
session.initialBidUrl(req);
res.render('authentication');
statsd.timing('routes.authentication', new Date() - start);
});
// GET /proxy/:email
// Dispatch the user to an appropriate authentication library.
app.get('/proxy/:email', function(req, res, next) {
addHeadersToForceRevalidation(res);
var
start = new Date(),
domainInfo = config.get('domain_info');
statsd.increment('routes.proxy.get');
// Issue #18 - Verify user input for email
if (valid_email(req.params.email) === false) {
// Safari and Android stock browsers double encode the URI
// So let's double decode URI across the sky Issue #89
req.params.email = decodeURIComponent(req.params.email);
if (valid_email(req.params.email) === false) {
return res.send('Email is bad input', 400);
}
}
session.setClaimedEmail(req);
// TODO: Can I define this somewhere closer to the strategy itself?
var authOptions = {
windowslive: { scope: 'wl.emails' }
};
var domain = req.params.email.split('@')[1];
if (!domainInfo.hasOwnProperty(domain)) {
logger.error('User landed on /proxy/:email for an unsupported domain');
res.redirect(session.getErrorUrl(baseUrl, req));
} else {
var strategy = domainInfo[domain].strategy;
if (strategy) {
(passport.authenticate(strategy, authOptions[strategy]))(req,res,next);
}
}
statsd.timing('routes.proxy', new Date() - start);
});
// GET /sign_in
// After successful authentication double-check the email address and
// begin or abort BrowserID provisioning as appropriate.
//
// Note that per-service callback URLs are defined in each service
// library (passport_google, etc); those callbacks redirect here if the
// user successfully auths with the service.
app.get('/sign_in', function(req, res) {
addHeadersToPreventCaching(res);
var
start = new Date(),
current = session.getCurrentUser(req),
ctx = {
current_user: current ? current : null
};
statsd.increment('routes.sign_in.get');
if (!current) { logger.debug("No active session"); }
res.render('signin', ctx);
statsd.timing('routes.sign_in', new Date() - start);
});
// GET /provision
// Begin BrowserID provisioning.
app.get('/provision', function(req, res){
addHeadersToForceRevalidation(res);
var start = new Date();
statsd.increment('routes.provision.get');
res.render('provision');
statsd.timing('routes.provision', new Date() - start);
});
var cryptoError = function(res, start) {
statsd.increment('routes.provision.err.crypto');
res.writeHead(500);
res.end();
statsd.timing('routes.provision_post', new Date() - start);
return false;
};
// POST /provision
// Finish BrowserID provisioning by signing a user's public key.
app.post('/provision', function(req, res) {
addHeadersToPreventCaching(res);
var
start = new Date(),
current_user = session.getCurrentUser(req),
authed_email = req.body.authed_email;
if (!current_user || ! req.isAuthenticated()) {
res.writeHead(401);
statsd.increment('routes.provision.no_current_user');
return res.end();
}
if (!req.body.pubkey || !req.body.duration) {
res.writeHead(400);
statsd.increment('routes.provision.invalid_post');
return res.end();
}
statsd.increment('routes.provision_get.post');
// If user doesn't go through OpenID / OAuth flow, they may be switching
// between already active emails.
if (authed_email !== current_user) {
var active_emails = session.getActiveEmails(req);
if (active_emails[authed_email] === true) {
current_user = authed_email;
session.setCurrentUser(req, authed_email);
statsd.increment('routes.provision.email_flopped');
}
}
// PIN verification - Did the user prove they can use a different email
// address via PIN verification from the id_mismatch screen?
if (pinCode.wasValidated(authed_email, req)) {
current_user = authed_email;
session.setCurrentUser(req, authed_email);
statsd.increment('routes.provision.pin_based');
}
var certified_cb = function(err, cert) {
var certificate;
if (err) {
return cryptoError(res, start);
} else {
try {
var certResp = JSON.parse(cert);
if (certResp && certResp.success) {
// Kill Session Issue #62
req.session.reset();
res.json({ cert: certResp.certificate });
} else {
console.error('certifier expected success: true, but got ', cert);
return cryptoError(res, start);
}
} catch (e) {
console.error('Bad output from certifier');
if (e.stack) console.error(e.stack);
return cryptoError(res, start);
}
}
statsd.timing('routes.provision_post', new Date() - start);
};
certify(req.body.pubkey,
current_user,
req.body.duration,
certified_cb);
});
// GET /provision.js
// This script handles client-side provisioning logic.
app.get('/provision.js', function(req, res) {
// this is a fully dynamic javascript file. it must never be cached
// see issue #165
addHeadersToPreventCaching(res);
var
start = new Date(),
ctx = {
duration: config.get('certificate_duration'),
emails: [],
num_emails: 0
};
statsd.increment('routes.provision_js.get');
if (req.isAuthenticated()) {
ctx.emails = session.getActiveEmails(req);
ctx.num_emails = Object.keys(ctx.emails).length;
}
res.contentType('js');
res.render('provision_js', ctx);
statsd.timing('routes.provision_js', new Date() - start);
});
// GET /error
// Generic error page for when we're unable to log a user in.
app.get('/error', function(req, res) {
addHeadersToPreventCaching(res);
var start = new Date();
statsd.increment('routes.error.get');
res.render('error', {
claimed: session.getClaimedEmail(req)
});
statsd.timing('routes.error', new Date() - start);
});
// GET /id_mismatch
// Error page for when a user auths as an email address other than the
// intended one. E.g., a user told BigTent that they were foo@yahoo.com, but
// we got back an OpenID auth for bar@yahoo.com.
// TODO: Add load test activity
app.get('/id_mismatch', function(req, res) {
addHeadersToPreventCaching(res);
var
start = new Date(),
claimed = session.getClaimedEmail(req),
domain,
domainInfo = config.get('domain_info');
if (claimed && claimed.indexOf('@') !== -1) {
domain = claimed.split('@')[1];
} else if (req.query.email && req.query.email.indexOf('@') !== -1) {
claimed = req.query.email;
domain = claimed.split('@')[1];
} else {
claimed = "";
domain = "Unknown";
}
statsd.increment('routes.id_mismatch.get');
if (!domainInfo.hasOwnProperty(domain)) {
logger.error('User landed on /id_mismatch for an unsupported domain');
res.redirect(session.getErrorUrl(baseUrl, req));
} else {
res.render('id_mismatch', {
claimed: claimed,
mismatched: session.getMismatchEmail(req),
provider: domainInfo[domain].providerName,
providerURL: domainInfo[domain].providerURL
});
}
statsd.timing('routes.id_mismatch', new Date() - start);
});
// The user's claimed and OpenID (mismatched) emails didn't match.
// We'll send them an email verification with a PIN
// We'll put the PIN in a secure cookie
app.post('/pin_code_request', function(req, res) {
var start = new Date();
statsd.increment('routes.pin_code_request.post');
pinCode.generateSecret(req, function(err, pin){
if (err) {
logger.error(err);
res.status(400);
return res.send("Unable to generate secret");
}
var domain, domainInfo, providerName,
email = session.getClaimedEmail(req);
if (!email) {
logger.error("Session is missing claimed email");
res.status(400);
return res.send("Session is missing claimed email");
}
try {
domain = email.split('@')[1];
domainInfo = config.get('domain_info');
providerName = domainInfo[domain].providerName;
} catch (e) {
statsd.increment('routes.err.pin_code_request.bad_provider');
statsd.timing('routes.pin_code_request', new Date() - start);
res.status(500);
return res.send("Error preparing webmail provider name");
}
if (err) {
statsd.increment('routes.err.pin_code_request.error_gen_secret');
statsd.timing('routes.pin_code_request', new Date() - start);
res.status(400);
res.send(err);
} else {
var langContext = {
lang: req.lang,
locale: req.locale,
gettext: req.gettext,
ngettext: req.ngettext,
format: req.format
};
var ctx = {
pin_code: pin,
webmail: providerName
};
emailer.sendPinVerification(email, ctx, langContext);
statsd.timing('routes.pin_code_request', new Date() - start);
res.send('OK');
}
});
});
app.post('/pin_code_check', function(req, res) {
var errorMsg, redirectUrl, start = new Date();
statsd.increment('routes.pin_code_check.post');
pinCode.validateSecret(req, function(err, pinMatched) {
if (err) {
logger.error(err);
statsd.increment('routes.err.pin_code_check.validate_secret');
statsd.timing('routes.pin_code_check', new Date() - start);
res.status(401);
return res.send('There was a problem with your request.');
} else if (pinMatched) {
pinCode.markVerified(session.getClaimedEmail(req), req);
session.setCurrentUser(req, session.getClaimedEmail(req));
redirectUrl = session.getBidUrl(baseUrl, req);
session.clearClaimedEmail(req);
session.clearBidUrl(req);
} else {
statsd.increment('routes.err.pin_code_check.pin_mismatch');
errorMsg = req.gettext("Sorry, wrong PIN code");
}
var payload = {
pinMatched: pinMatched,
redirectUrl: redirectUrl,
error: errorMsg
};
statsd.timing('routes.pin_code_check', new Date() - start);
res.setHeader('Content-Type', 'application/json');
return res.send(JSON.stringify(payload));
});
});
// GET /cancel
// Handle the user cancelling the OpenID or OAuth flow.
app.get('/cancel', function(req, res) {
addHeadersToPreventCaching(res);
res.redirect(session.getCancelledUrl(baseUrl, req));
});
// GET /cancelled
// If the user cancelled login, raise a BrowserID authentication
// failure, which instructs the user agent to return to its provisioning
// flow and proceed with its failure case. Typically, this just
// re-starts the BrowserID flow for the user.
app.get('/cancelled', function(req, res) {
addHeadersToForceRevalidation(res);
var start = new Date();
statsd.increment('routes.cancelled.get');
res.render('cancelled');
statsd.timing('routes.cancelled', new Date() - start);
});
// GET /.well-known/browserid
// Declare support as a BrowserID Identity Provider.
app.get('/.well-known/browserid', function(req, res) {
addHeadersToForceRevalidation(res);
var
start = new Date(),
timeout = config.get('pub_key_ttl'),
serviceDisabled = config.get('disable_bigtent');
if (serviceDisabled) {
res.setHeader('Content-Type', 'application/json');
return res.send(JSON.stringify({disabled: true}));
}
statsd.increment('routes.wellknown.get');
if (req.headers['if-modified-since'] !== undefined) {
var since = new Date(req.headers['if-modified-since']);
if (isNaN(since.getTime())) {
logger.error('======== Bad date in If-Modified-Since header');
} else {
util.puts(since);
// Does the client already have the latest copy?
if (since >= well_known_last_mod) {
// TODO move above?
res.setHeader('Cache-Control', 'max-age=' + timeout);
return res.send(304);
}
}
}
crypto.pubKey(function(err, publicKey) {
// This should never happen
if (err) {
console.error("Route unable to load BigTent public key");
console.error(err);
throw new Error('Unabled to load BigTent public key');
}
var pk = JSON.stringify(publicKey);
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'max-age=' + timeout);
res.setHeader('Last-Modified',
new Date(well_known_last_mod).toUTCString());
res.render('well_known_browserid', {
public_key: pk
});
statsd.timing('routes.wellknown', new Date() - start);
});
});
app.get('/', function(req, res) {
addHeadersToForceRevalidation(res);
res.redirect('https://login.persona.org/');
});
// GET /__heartbeat__
// Report on whether or not this node is functioning as expected.
app.get('/__heartbeat__', function(req, res) {
addHeadersToPreventCaching(res);
var
url = util.format('http://%s:%s/__heartbeat__',
config.get('certifier_host'),
config.get('certifier_port')),
opts = {
url: url,
timeout: 500
};
request(url, function(err, heartResp, body) {
if (err ||
200 !== heartResp.statusCode ||
'ok certifier' !== body.trim()) {
res.writeHead(500);
res.write('certifier down');
res.end();
} else {
res.writeHead(200);
res.write('ok');
res.end();
}
});
});
};