Add foreign IP login notifications

This commit is contained in:
garrettmills 2020-07-12 16:05:59 -05:00
parent 8dd3accfc4
commit d29e6f057a
No known key found for this signature in database
GPG Key ID: 6ACD58D6ADACFC6E
7 changed files with 106 additions and 5 deletions

View File

@ -3,4 +3,3 @@
- show app documentation somewhere besides final page of setup - show app documentation somewhere besides final page of setup
- Logins as jobs? - Logins as jobs?
- OpenID Connect - oidc-provider - OpenID Connect - oidc-provider
- Login from new IP notifications

View File

@ -4,7 +4,7 @@ const email_validator = require('email-validator')
class AuthController extends Controller { class AuthController extends Controller {
static get services() { static get services() {
return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs', 'utility'] return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs', 'utility', 'activity']
} }
async get_auth_user(req, res, next) { async get_auth_user(req, res, next) {
@ -168,7 +168,6 @@ class AuthController extends Controller {
return res.api(data) return res.api(data)
} }
async get_roles(req, res, next) { async get_roles(req, res, next) {
const role_config = this.configs.get('auth.roles') const role_config = this.configs.get('auth.roles')
const data = [] const data = []
@ -603,6 +602,9 @@ class AuthController extends Controller {
} }
} }
// Create a login tracking activity
await this.activity.login(req)
return res.api({ return res.api({
success: true, success: true,
session_created: !!req.body.create_session, session_created: !!req.body.create_session,

View File

@ -0,0 +1,37 @@
const { Job } = require('flitter-jobs')
class ForeignIPLoginAlertJob extends Job {
static get services() { return [...super.services, 'models', 'jobs', 'output', 'configs'] }
async execute(job) {
const { data } = job
const { user_id, ip } = data
try {
const User = this.models.get('auth:User')
const user = await User.findById(user_id)
if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
this.output.info('Sending foreign IP login alert to user.')
await this.jobs.queue('mailer').add('EMail', {
to: user.email,
subject: `Security Alert | ${this.configs.get('app.name')}`,
email_params: {
header_text: 'Login From New IP',
body_paragraphs: [
`We've detected a login to your ${this.configs.get('app.name')} account from a new IP address (${ip}).`,
'If this was you, no further action is required. If this was not you, please log into your account and reset your password.',
'Also, consider enabling multi-factor authentication to protect your account.',
],
button_text: 'Account Settings',
button_link: `${this.configs.get('app.url')}dash/profile`,
}
})
} catch (e) {
this.output.error(e)
}
}
}
module.exports = exports = ForeignIPLoginAlertJob

View File

@ -0,0 +1,15 @@
const { Model } = require('flitter-orm')
class ActivityModel extends Model {
static get schema() {
return {
user_id: String,
session_id: String,
action: String,
timestamp: { type: Date, default: () => new Date },
metadata: Object,
}
}
}
module.exports = exports = ActivityModel

View File

@ -0,0 +1,45 @@
const { Service } = require('flitter-di')
class ActivityService extends Service {
static get services() { return ['models', 'jobs'] }
model() {
return this.models.get('Activity')
}
async login(req) {
const Activity = this.model()
const activity = new Activity({
user_id: req.session.auth.user_id,
session_id: req.session.id,
action: 'login',
metadata: {
ip: req.ip
}
})
// If this is a new IP login, send an e-mail alert
const foreign_ip = await this.foreign_login_ip(req.session.auth.user_id, req.ip)
if ( foreign_ip ) {
await this.jobs.queue('notifications').add('ForeignIPLoginAlert', {
ip: req.ip,
user_id: req.session.auth.user_id,
})
}
await activity.save()
}
async foreign_login_ip(user_id, ip) {
const Activity = this.model()
const existing_ip = await Activity.findOne({
user_id,
action: 'login',
'metadata.ip': ip,
})
return !existing_ip
}
}
module.exports = exports = ActivityService

View File

@ -10,6 +10,8 @@ class SettingsUnit extends Unit {
} }
async go(app) { async go(app) {
app.express.set('trust proxy', true)
const Setting = this.models.get('Setting') const Setting = this.models.get('Setting')
const default_settings = this.configs.get('setting.settings') const default_settings = this.configs.get('setting.settings')
for ( const key in default_settings ) { for ( const key in default_settings ) {

View File

@ -14,6 +14,7 @@ const jobs_config = {
queues: [ queues: [
'mailer', 'mailer',
'password_resets', 'password_resets',
'notifications',
], ],
// Mapping of worker name => worker config // Mapping of worker name => worker config
@ -22,7 +23,7 @@ const jobs_config = {
// The name of the worker is "main" // The name of the worker is "main"
main: { main: {
// This worker will process these queues // This worker will process these queues
queues: ['mailer', 'password_resets'], queues: ['mailer', 'password_resets', 'notifications'],
}, },
// You can have many workers, and multiple workers can // You can have many workers, and multiple workers can