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

Commit

Permalink
feat(security): record event names and ip addresses for important events
Browse files Browse the repository at this point in the history
  • Loading branch information
seanmonstar committed Sep 20, 2016
1 parent 9fb1f71 commit 05485b4
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 10 deletions.
7 changes: 7 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,13 @@ var conf = convict({
],
env: 'SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX'
}
},
securityHistory: {
enabled: {
doc: 'enable security history',
default: true,
env: 'SECURITY_HISTORY_ENABLED'
}
}
})

Expand Down
18 changes: 18 additions & 0 deletions lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,24 @@ module.exports = function (
return this.pool.del('/verificationReminders', reminderData)
}

DB.prototype.securityEvent = function (event) {
log.trace({
op: 'DB.securityEvent',
securityEvent: event
})

return this.pool.post('/securityEvents', event)
}

DB.prototype.securityEvents = function (params) {
log.trace({
op: 'DB.securityEvents',
params: params
})

return this.pool.get('/securityEvents', params)
}

return DB
}

Expand Down
114 changes: 113 additions & 1 deletion lib/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ var PUSH_PAYLOADS_SCHEMA_PATH = '../../docs/pushpayloads.schema.json'
// Currently only for metrics purposes, not enforced.
var MAX_ACTIVE_SESSIONS = 200

var MS_ONE_DAY = 1000 * 60 * 60 * 24
var MS_ONE_WEEK = MS_ONE_DAY * 7
var MS_ONE_MONTH = MS_ONE_DAY * 30

var path = require('path')
var ajv = require('ajv')()
var fs = require('fs')
Expand Down Expand Up @@ -51,6 +55,8 @@ module.exports = function (
defaultLanguage: config.i18n.defaultLanguage
})

var securityHistoryEnabled = config.securityHistory && config.securityHistory.enabled

var routes = [
{
method: 'POST',
Expand Down Expand Up @@ -103,6 +109,7 @@ module.exports = function (
.then(createSessionToken)
.then(sendVerifyCode)
.then(createKeyFetchToken)
.then(recordSecurityEvent)
.then(createResponse)
.done(reply, reply)

Expand Down Expand Up @@ -307,6 +314,18 @@ module.exports = function (
}
}

function recordSecurityEvent() {
if (securityHistoryEnabled) {
// don't block response recording db event
db.securityEvent({
name: 'account.create',
uid: account.uid,
ipAddr: request.app.clientAddress,
sessionTokenId: sessionToken.tokenId
})
}
}

function createResponse () {
var response = {
uid: account.uid.toString('hex'),
Expand Down Expand Up @@ -377,13 +396,15 @@ module.exports = function (

customs.check(request, email, 'accountLogin')
.then(readEmailRecord)
.then(checkSecurityHistory)
.then(checkNumberOfActiveSessions)
.then(createSessionToken)
.then(createKeyFetchToken)
.then(emitSyncLoginEvent)
.then(sendVerifyAccountEmail)
.then(sendNewDeviceLoginNotification)
.then(sendVerifyLoginEmail)
.then(recordSecurityEvent)
.then(createResponse)
.done(reply, reply)

Expand Down Expand Up @@ -444,6 +465,72 @@ module.exports = function (
)
}

function checkSecurityHistory () {
if (!securityHistoryEnabled) {
return
}
return db.securityEvents({
uid: emailRecord.uid,
ipAddr: request.app.clientAddress
})
.then(
function (events) {
// if we've seen this address for this user before, we
// can skip signin confirmation
//
// for now, just log that we *could* have done so
if (events.length > 0) {
var latest = 0
var verified = false

events.forEach(function(ev) {
if (ev.verified) {
verified = true
if (ev.createdAt > latest) {
latest = ev.createdAt
}
}
})
if (verified) {
var since = Date.now() - latest
var recency
if (since < MS_ONE_DAY) {
recency = 'day'
} else if (since < MS_ONE_WEEK) {
recency = 'week'
} else if (since < MS_ONE_MONTH) {
recency = 'month'
} else {
recency = 'old'
}

log.info({
op: 'Account.history.verified',
uid: emailRecord.uid.toString('hex'),
events: events.length,
recency: recency
})
} else {
log.info({
op: 'Account.history.unverified',
uid: emailRecord.uid.toString('hex'),
events: events.length
})
}
}
},
function (err) {
// for now, security events are purely for metrics
// so errors shouldn't stop the login attempt
log.error({
op: 'Account.history.error',
err: err,
uid: emailRecord.uid.toString('hex')
})
}
)
}

function checkNumberOfActiveSessions () {
return db.sessions(emailRecord.uid)
.then(
Expand Down Expand Up @@ -619,6 +706,18 @@ module.exports = function (
}
}

function recordSecurityEvent() {
if (securityHistoryEnabled) {
// don't block response recording db event
db.securityEvent({
name: 'account.login',
uid: emailRecord.uid,
ipAddr: request.app.clientAddress,
sessionTokenId: sessionToken && sessionToken.tokenId
})
}
}

function createResponse () {
var response = {
uid: sessionToken.uid.toString('hex'),
Expand All @@ -644,7 +743,6 @@ module.exports = function (
response.verificationMethod = 'email'
response.verificationReason = 'login'
}

return P.resolve(response)
}
}
Expand Down Expand Up @@ -1458,6 +1556,7 @@ module.exports = function (
.then(resetAccountData)
.then(createSessionToken)
.then(createKeyFetchToken)
.then(recordSecurityEvent)
.then(createResponse)
.done(reply, reply)

Expand Down Expand Up @@ -1572,13 +1671,26 @@ module.exports = function (
}
}

function recordSecurityEvent() {
if (securityHistoryEnabled) {
// don't block response recording db event
db.securityEvent({
name: 'account.reset',
uid: account.uid,
ipAddr: request.app.clientAddress,
sessionTokenId: sessionToken && sessionToken.tokenId
})
}
}

function createResponse () {
// If no sessionToken, this could be a legacy client
// attempting to reset an account password, return legacy response.
if (!hasSessionToken) {
return {}
}


var response = {
uid: sessionToken.uid.toString('hex'),
sessionToken: sessionToken.data.toString('hex'),
Expand Down
2 changes: 1 addition & 1 deletion lib/routes/utils/request_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @returns {boolean}
*/
function wantsKeys (request) {
return request.query.keys === 'true'
return request.query && request.query.keys === 'true'
}

/**
Expand Down
7 changes: 6 additions & 1 deletion npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 05485b4

Please sign in to comment.