Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.

Commit 05485b4

Browse files
committed
feat(security): record event names and ip addresses for important events
1 parent 9fb1f71 commit 05485b4

File tree

7 files changed

+280
-10
lines changed

7 files changed

+280
-10
lines changed

config/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,13 @@ var conf = convict({
465465
],
466466
env: 'SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX'
467467
}
468+
},
469+
securityHistory: {
470+
enabled: {
471+
doc: 'enable security history',
472+
default: true,
473+
env: 'SECURITY_HISTORY_ENABLED'
474+
}
468475
}
469476
})
470477

lib/db.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,24 @@ module.exports = function (
772772
return this.pool.del('/verificationReminders', reminderData)
773773
}
774774

775+
DB.prototype.securityEvent = function (event) {
776+
log.trace({
777+
op: 'DB.securityEvent',
778+
securityEvent: event
779+
})
780+
781+
return this.pool.post('/securityEvents', event)
782+
}
783+
784+
DB.prototype.securityEvents = function (params) {
785+
log.trace({
786+
op: 'DB.securityEvents',
787+
params: params
788+
})
789+
790+
return this.pool.get('/securityEvents', params)
791+
}
792+
775793
return DB
776794
}
777795

lib/routes/account.js

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ var PUSH_PAYLOADS_SCHEMA_PATH = '../../docs/pushpayloads.schema.json'
1313
// Currently only for metrics purposes, not enforced.
1414
var MAX_ACTIVE_SESSIONS = 200
1515

16+
var MS_ONE_DAY = 1000 * 60 * 60 * 24
17+
var MS_ONE_WEEK = MS_ONE_DAY * 7
18+
var MS_ONE_MONTH = MS_ONE_DAY * 30
19+
1620
var path = require('path')
1721
var ajv = require('ajv')()
1822
var fs = require('fs')
@@ -51,6 +55,8 @@ module.exports = function (
5155
defaultLanguage: config.i18n.defaultLanguage
5256
})
5357

58+
var securityHistoryEnabled = config.securityHistory && config.securityHistory.enabled
59+
5460
var routes = [
5561
{
5662
method: 'POST',
@@ -103,6 +109,7 @@ module.exports = function (
103109
.then(createSessionToken)
104110
.then(sendVerifyCode)
105111
.then(createKeyFetchToken)
112+
.then(recordSecurityEvent)
106113
.then(createResponse)
107114
.done(reply, reply)
108115

@@ -307,6 +314,18 @@ module.exports = function (
307314
}
308315
}
309316

317+
function recordSecurityEvent() {
318+
if (securityHistoryEnabled) {
319+
// don't block response recording db event
320+
db.securityEvent({
321+
name: 'account.create',
322+
uid: account.uid,
323+
ipAddr: request.app.clientAddress,
324+
sessionTokenId: sessionToken.tokenId
325+
})
326+
}
327+
}
328+
310329
function createResponse () {
311330
var response = {
312331
uid: account.uid.toString('hex'),
@@ -377,13 +396,15 @@ module.exports = function (
377396

378397
customs.check(request, email, 'accountLogin')
379398
.then(readEmailRecord)
399+
.then(checkSecurityHistory)
380400
.then(checkNumberOfActiveSessions)
381401
.then(createSessionToken)
382402
.then(createKeyFetchToken)
383403
.then(emitSyncLoginEvent)
384404
.then(sendVerifyAccountEmail)
385405
.then(sendNewDeviceLoginNotification)
386406
.then(sendVerifyLoginEmail)
407+
.then(recordSecurityEvent)
387408
.then(createResponse)
388409
.done(reply, reply)
389410

@@ -444,6 +465,72 @@ module.exports = function (
444465
)
445466
}
446467

468+
function checkSecurityHistory () {
469+
if (!securityHistoryEnabled) {
470+
return
471+
}
472+
return db.securityEvents({
473+
uid: emailRecord.uid,
474+
ipAddr: request.app.clientAddress
475+
})
476+
.then(
477+
function (events) {
478+
// if we've seen this address for this user before, we
479+
// can skip signin confirmation
480+
//
481+
// for now, just log that we *could* have done so
482+
if (events.length > 0) {
483+
var latest = 0
484+
var verified = false
485+
486+
events.forEach(function(ev) {
487+
if (ev.verified) {
488+
verified = true
489+
if (ev.createdAt > latest) {
490+
latest = ev.createdAt
491+
}
492+
}
493+
})
494+
if (verified) {
495+
var since = Date.now() - latest
496+
var recency
497+
if (since < MS_ONE_DAY) {
498+
recency = 'day'
499+
} else if (since < MS_ONE_WEEK) {
500+
recency = 'week'
501+
} else if (since < MS_ONE_MONTH) {
502+
recency = 'month'
503+
} else {
504+
recency = 'old'
505+
}
506+
507+
log.info({
508+
op: 'Account.history.verified',
509+
uid: emailRecord.uid.toString('hex'),
510+
events: events.length,
511+
recency: recency
512+
})
513+
} else {
514+
log.info({
515+
op: 'Account.history.unverified',
516+
uid: emailRecord.uid.toString('hex'),
517+
events: events.length
518+
})
519+
}
520+
}
521+
},
522+
function (err) {
523+
// for now, security events are purely for metrics
524+
// so errors shouldn't stop the login attempt
525+
log.error({
526+
op: 'Account.history.error',
527+
err: err,
528+
uid: emailRecord.uid.toString('hex')
529+
})
530+
}
531+
)
532+
}
533+
447534
function checkNumberOfActiveSessions () {
448535
return db.sessions(emailRecord.uid)
449536
.then(
@@ -619,6 +706,18 @@ module.exports = function (
619706
}
620707
}
621708

709+
function recordSecurityEvent() {
710+
if (securityHistoryEnabled) {
711+
// don't block response recording db event
712+
db.securityEvent({
713+
name: 'account.login',
714+
uid: emailRecord.uid,
715+
ipAddr: request.app.clientAddress,
716+
sessionTokenId: sessionToken && sessionToken.tokenId
717+
})
718+
}
719+
}
720+
622721
function createResponse () {
623722
var response = {
624723
uid: sessionToken.uid.toString('hex'),
@@ -644,7 +743,6 @@ module.exports = function (
644743
response.verificationMethod = 'email'
645744
response.verificationReason = 'login'
646745
}
647-
648746
return P.resolve(response)
649747
}
650748
}
@@ -1458,6 +1556,7 @@ module.exports = function (
14581556
.then(resetAccountData)
14591557
.then(createSessionToken)
14601558
.then(createKeyFetchToken)
1559+
.then(recordSecurityEvent)
14611560
.then(createResponse)
14621561
.done(reply, reply)
14631562

@@ -1572,13 +1671,26 @@ module.exports = function (
15721671
}
15731672
}
15741673

1674+
function recordSecurityEvent() {
1675+
if (securityHistoryEnabled) {
1676+
// don't block response recording db event
1677+
db.securityEvent({
1678+
name: 'account.reset',
1679+
uid: account.uid,
1680+
ipAddr: request.app.clientAddress,
1681+
sessionTokenId: sessionToken && sessionToken.tokenId
1682+
})
1683+
}
1684+
}
1685+
15751686
function createResponse () {
15761687
// If no sessionToken, this could be a legacy client
15771688
// attempting to reset an account password, return legacy response.
15781689
if (!hasSessionToken) {
15791690
return {}
15801691
}
15811692

1693+
15821694
var response = {
15831695
uid: sessionToken.uid.toString('hex'),
15841696
sessionToken: sessionToken.data.toString('hex'),

lib/routes/utils/request_helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @returns {boolean}
1010
*/
1111
function wantsKeys (request) {
12-
return request.query.keys === 'true'
12+
return request.query && request.query.keys === 'true'
1313
}
1414

1515
/**

npm-shrinkwrap.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)