Expand activity tracking and add PasswordResetAlert job
This commit is contained in:
parent
d29e6f057a
commit
143fccf179
@ -2,4 +2,5 @@
|
|||||||
- Localize all the things
|
- Localize all the things
|
||||||
- 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
|
||||||
|
- system banner messages UI
|
@ -638,6 +638,7 @@ class AuthController extends Controller {
|
|||||||
const token = req.user.mfa_token
|
const token = req.user.mfa_token
|
||||||
const codes = await token.generate_recovery()
|
const codes = await token.generate_recovery()
|
||||||
await req.user.save()
|
await req.user.save()
|
||||||
|
await this.activity.mfa_recovery_created({ req })
|
||||||
return res.api({
|
return res.api({
|
||||||
codes,
|
codes,
|
||||||
})
|
})
|
||||||
@ -704,6 +705,8 @@ class AuthController extends Controller {
|
|||||||
req.user.mfa_enable_date = new Date
|
req.user.mfa_enable_date = new Date
|
||||||
req.user.save()
|
req.user.save()
|
||||||
|
|
||||||
|
await this.activity.mfa_enable({ req })
|
||||||
|
|
||||||
// invalidate existing tokens and other logins
|
// invalidate existing tokens and other logins
|
||||||
const flitter = await this.auth.get_provider('flitter')
|
const flitter = await this.auth.get_provider('flitter')
|
||||||
await flitter.logout(req)
|
await flitter.logout(req)
|
||||||
@ -724,6 +727,8 @@ class AuthController extends Controller {
|
|||||||
req.user.app_passwords = []
|
req.user.app_passwords = []
|
||||||
await req.user.save()
|
await req.user.save()
|
||||||
|
|
||||||
|
await this.activity.mfa_disable({ req })
|
||||||
|
|
||||||
// invalidate existing login tokens and logins
|
// invalidate existing login tokens and logins
|
||||||
const flitter = await this.auth.get_provider('flitter')
|
const flitter = await this.auth.get_provider('flitter')
|
||||||
await flitter.logout(req)
|
await flitter.logout(req)
|
||||||
|
@ -3,7 +3,7 @@ const zxcvbn = require('zxcvbn')
|
|||||||
|
|
||||||
class PasswordController extends Controller {
|
class PasswordController extends Controller {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'auth', 'jobs', 'models']
|
return [...super.services, 'auth', 'jobs', 'models', 'activity']
|
||||||
}
|
}
|
||||||
|
|
||||||
async get_resets(req, res, next) {
|
async get_resets(req, res, next) {
|
||||||
@ -35,6 +35,7 @@ class PasswordController extends Controller {
|
|||||||
|
|
||||||
const { password, record } = await req.user.app_password(req.body.name)
|
const { password, record } = await req.user.app_password(req.body.name)
|
||||||
await req.user.save()
|
await req.user.save()
|
||||||
|
await this.activity.app_password_created({ req, name: req.body.name })
|
||||||
|
|
||||||
return res.api({
|
return res.api({
|
||||||
password,
|
password,
|
||||||
@ -86,6 +87,7 @@ class PasswordController extends Controller {
|
|||||||
// Create the password reset
|
// Create the password reset
|
||||||
const reset = await req.user.reset_password(req.body.password)
|
const reset = await req.user.reset_password(req.body.password)
|
||||||
await req.user.save()
|
await req.user.save()
|
||||||
|
await this.activity.password_reset({ req, ip: req.ip })
|
||||||
if ( req.trap.has_trap() && req.trap.get_trap() === 'password_reset' ) await req.trap.end()
|
if ( req.trap.has_trap() && req.trap.get_trap() === 'password_reset' ) await req.trap.end()
|
||||||
|
|
||||||
// invalidate existing tokens and other logins
|
// invalidate existing tokens and other logins
|
||||||
|
@ -3,7 +3,7 @@ const uuid = require('uuid/v4')
|
|||||||
|
|
||||||
class ReflectController extends Controller {
|
class ReflectController extends Controller {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'routers', 'models']
|
return [...super.services, 'routers', 'models', 'activity']
|
||||||
}
|
}
|
||||||
|
|
||||||
async get_tokens(req, res, next) {
|
async get_tokens(req, res, next) {
|
||||||
@ -81,6 +81,7 @@ class ReflectController extends Controller {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await token.save()
|
await token.save()
|
||||||
|
await this.activity.api_token_created({ req, oauth_client_id: client.uuid })
|
||||||
return res.api({
|
return res.api({
|
||||||
id: token.id,
|
id: token.id,
|
||||||
token: token.accessToken,
|
token: token.accessToken,
|
||||||
|
36
app/jobs/PasswordResetAlert.job.js
Normal file
36
app/jobs/PasswordResetAlert.job.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const { Job } = require('flitter-jobs')
|
||||||
|
|
||||||
|
class PasswordResetAlertJob 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 password reset 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: 'Your Password Was Reset',
|
||||||
|
body_paragraphs: [
|
||||||
|
`The password to your ${this.configs.get('app.name')} account (${user.uid}) was recently reset from the IP ${ip}.`,
|
||||||
|
'If this was you, please disregard this email. Otherwise, please contact your administrator for assistance recovering your account.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.output.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = PasswordResetAlertJob
|
@ -2,7 +2,7 @@ const { Middleware } = require('libflitter')
|
|||||||
|
|
||||||
class PermissionMiddleware extends Middleware {
|
class PermissionMiddleware extends Middleware {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'models']
|
return [...super.services, 'models', 'activity']
|
||||||
}
|
}
|
||||||
|
|
||||||
async test(req, res, next, { check }) {
|
async test(req, res, next, { check }) {
|
||||||
@ -11,20 +11,34 @@ class PermissionMiddleware extends Middleware {
|
|||||||
// If the request was authorized using an OAuth2 bearer token,
|
// If the request was authorized using an OAuth2 bearer token,
|
||||||
// make sure the associated client has permission to access this endpoint.
|
// make sure the associated client has permission to access this endpoint.
|
||||||
if ( req?.oauth?.client ) {
|
if ( req?.oauth?.client ) {
|
||||||
if ( !req.oauth.client.can(check) )
|
if ( !req.oauth.client.can(check) ) {
|
||||||
|
const reason = 'oauth-permission-fail'
|
||||||
|
await this.activity.api_access_denial({
|
||||||
|
req,
|
||||||
|
reason,
|
||||||
|
check,
|
||||||
|
oauth_client_id: req.oauth.client.id,
|
||||||
|
})
|
||||||
|
|
||||||
return res.status(401)
|
return res.status(401)
|
||||||
.message('Insufficient permissions (OAuth2 Client).')
|
.message('Insufficient permissions (OAuth2 Client).')
|
||||||
.api()
|
.api()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const policy_denied = await Policy.check_user_denied(req.user, check)
|
const policy_denied = await Policy.check_user_denied(req.user, check)
|
||||||
const policy_access = await Policy.check_user_access(req.user, check)
|
const policy_access = await Policy.check_user_access(req.user, check)
|
||||||
|
|
||||||
// Make sure the user has permission
|
// Make sure the user has permission
|
||||||
if ( policy_denied || (!req.user.can(check) && !policy_access) )
|
if ( policy_denied || (!req.user.can(check) && !policy_access) ) {
|
||||||
|
// Record the failed API access
|
||||||
|
const reason = policy_denied ? 'iam-denial' : (!req.user.can(check) ? 'user-permission-fail' : 'iam-not-granted')
|
||||||
|
await this.activity.api_access_denial({ req, reason, check })
|
||||||
|
|
||||||
return res.status(401)
|
return res.status(401)
|
||||||
.message('Insufficient permissions.')
|
.message('Insufficient permissions.')
|
||||||
.api()
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,11 @@ class ActivityService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(req) {
|
async login(req) {
|
||||||
const Activity = this.model()
|
const activity = this.from_req(req)
|
||||||
const activity = new Activity({
|
activity.action = 'login'
|
||||||
user_id: req.session.auth.user_id,
|
activity.metadata = {
|
||||||
session_id: req.session.id,
|
ip: req.ip
|
||||||
action: 'login',
|
}
|
||||||
metadata: {
|
|
||||||
ip: req.ip
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// If this is a new IP login, send an e-mail alert
|
// 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)
|
const foreign_ip = await this.foreign_login_ip(req.session.auth.user_id, req.ip)
|
||||||
@ -30,6 +26,64 @@ class ActivityService extends Service {
|
|||||||
await activity.save()
|
await activity.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async api_access_denial({ req, reason, check, oauth_client_id = null }) {
|
||||||
|
const activity = this.from_req(req)
|
||||||
|
activity.action = 'api-access-denial'
|
||||||
|
activity.metadata = {
|
||||||
|
scope: check,
|
||||||
|
reason,
|
||||||
|
oauth_client_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
await activity.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async mfa_enable({ req }) {
|
||||||
|
const activity = this.from_req(req)
|
||||||
|
activity.action = 'mfa-enable'
|
||||||
|
await activity.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async mfa_disable({ req }) {
|
||||||
|
const activity = this.from_req(req)
|
||||||
|
activity.action = 'mfa-disable'
|
||||||
|
await activity.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async mfa_recovery_created({ req }) {
|
||||||
|
const activity = this.from_req(req)
|
||||||
|
activity.action = 'mfa-recovery-created'
|
||||||
|
await activity.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async app_password_created({ req, name }) {
|
||||||
|
const activity = this.from_req(req)
|
||||||
|
activity.action = 'app-password-created'
|
||||||
|
activity.metadata = { name }
|
||||||
|
await activity.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async password_reset({ req, ip }) {
|
||||||
|
const activity = this.from_req(req)
|
||||||
|
activity.action = 'password-reset'
|
||||||
|
activity.metadata = { ip }
|
||||||
|
await activity.save()
|
||||||
|
|
||||||
|
// Send an alert to the user
|
||||||
|
await this.jobs.queue('notifications').add('PasswordResetAlert', {
|
||||||
|
ip, user_id: req.session.auth.user_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async api_token_created({ req, oauth_client_id }) {
|
||||||
|
const activity = this.from_req(req)
|
||||||
|
activity.action = 'api-token-created'
|
||||||
|
activity.metadata = {
|
||||||
|
ip: req.ip,
|
||||||
|
oauth_client_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async foreign_login_ip(user_id, ip) {
|
async foreign_login_ip(user_id, ip) {
|
||||||
const Activity = this.model()
|
const Activity = this.model()
|
||||||
const existing_ip = await Activity.findOne({
|
const existing_ip = await Activity.findOne({
|
||||||
@ -40,6 +94,14 @@ class ActivityService extends Service {
|
|||||||
|
|
||||||
return !existing_ip
|
return !existing_ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
from_req(req) {
|
||||||
|
const Activity = this.model()
|
||||||
|
return new Activity({
|
||||||
|
user_id: req.session.auth.user_id,
|
||||||
|
session_id: req.session.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = ActivityService
|
module.exports = exports = ActivityService
|
||||||
|
Loading…
Reference in New Issue
Block a user