Expand activity tracking and add PasswordResetAlert job
This commit is contained in:
parent
d29e6f057a
commit
143fccf179
@ -2,4 +2,5 @@
|
||||
- Localize all the things
|
||||
- show app documentation somewhere besides final page of setup
|
||||
- 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 codes = await token.generate_recovery()
|
||||
await req.user.save()
|
||||
await this.activity.mfa_recovery_created({ req })
|
||||
return res.api({
|
||||
codes,
|
||||
})
|
||||
@ -704,6 +705,8 @@ class AuthController extends Controller {
|
||||
req.user.mfa_enable_date = new Date
|
||||
req.user.save()
|
||||
|
||||
await this.activity.mfa_enable({ req })
|
||||
|
||||
// invalidate existing tokens and other logins
|
||||
const flitter = await this.auth.get_provider('flitter')
|
||||
await flitter.logout(req)
|
||||
@ -724,6 +727,8 @@ class AuthController extends Controller {
|
||||
req.user.app_passwords = []
|
||||
await req.user.save()
|
||||
|
||||
await this.activity.mfa_disable({ req })
|
||||
|
||||
// invalidate existing login tokens and logins
|
||||
const flitter = await this.auth.get_provider('flitter')
|
||||
await flitter.logout(req)
|
||||
|
@ -3,7 +3,7 @@ const zxcvbn = require('zxcvbn')
|
||||
|
||||
class PasswordController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'auth', 'jobs', 'models']
|
||||
return [...super.services, 'auth', 'jobs', 'models', 'activity']
|
||||
}
|
||||
|
||||
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)
|
||||
await req.user.save()
|
||||
await this.activity.app_password_created({ req, name: req.body.name })
|
||||
|
||||
return res.api({
|
||||
password,
|
||||
@ -86,6 +87,7 @@ class PasswordController extends Controller {
|
||||
// Create the password reset
|
||||
const reset = await req.user.reset_password(req.body.password)
|
||||
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()
|
||||
|
||||
// invalidate existing tokens and other logins
|
||||
|
@ -3,7 +3,7 @@ const uuid = require('uuid/v4')
|
||||
|
||||
class ReflectController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'routers', 'models']
|
||||
return [...super.services, 'routers', 'models', 'activity']
|
||||
}
|
||||
|
||||
async get_tokens(req, res, next) {
|
||||
@ -81,6 +81,7 @@ class ReflectController extends Controller {
|
||||
})
|
||||
|
||||
await token.save()
|
||||
await this.activity.api_token_created({ req, oauth_client_id: client.uuid })
|
||||
return res.api({
|
||||
id: token.id,
|
||||
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 {
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
return [...super.services, 'models', 'activity']
|
||||
}
|
||||
|
||||
async test(req, res, next, { check }) {
|
||||
@ -11,20 +11,34 @@ class PermissionMiddleware extends Middleware {
|
||||
// If the request was authorized using an OAuth2 bearer token,
|
||||
// make sure the associated client has permission to access this endpoint.
|
||||
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)
|
||||
.message('Insufficient permissions (OAuth2 Client).')
|
||||
.api()
|
||||
}
|
||||
}
|
||||
|
||||
const policy_denied = await Policy.check_user_denied(req.user, check)
|
||||
const policy_access = await Policy.check_user_access(req.user, check)
|
||||
|
||||
// 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)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
@ -8,15 +8,11 @@ class ActivityService extends Service {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
const activity = this.from_req(req)
|
||||
activity.action = 'login'
|
||||
activity.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)
|
||||
@ -30,6 +26,64 @@ class ActivityService extends Service {
|
||||
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) {
|
||||
const Activity = this.model()
|
||||
const existing_ip = await Activity.findOne({
|
||||
@ -40,6 +94,14 @@ class ActivityService extends Service {
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user