2020-04-22 14:19:25 +00:00
|
|
|
const { Controller } = require('libflitter')
|
2020-05-17 04:55:08 +00:00
|
|
|
const zxcvbn = require('zxcvbn')
|
2020-04-22 14:19:25 +00:00
|
|
|
|
|
|
|
class AuthController extends Controller {
|
|
|
|
static get services() {
|
2020-05-17 04:55:08 +00:00
|
|
|
return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs', 'utility']
|
2020-05-12 01:26:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async get_users(req, res, next) {
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
const users = await User.find()
|
|
|
|
const data = []
|
|
|
|
|
|
|
|
for ( const user of users ) {
|
|
|
|
if ( !req.user.can(`auth:user:${user.id}:view`) && req.user.id !== user.id) continue
|
|
|
|
data.push(await user.to_api())
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.api(data)
|
|
|
|
}
|
|
|
|
|
2020-05-17 04:55:08 +00:00
|
|
|
async get_groups(req, res, next) {
|
|
|
|
const Group = this.models.get('auth:Group')
|
|
|
|
const groups = await Group.find({active: true})
|
|
|
|
const data = []
|
|
|
|
|
|
|
|
for ( const group of groups ) {
|
|
|
|
if ( !req.user.can(`auth:group:${group.id}:view`) ) continue
|
|
|
|
data.push(await group.to_api())
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.api(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-05-12 01:26:09 +00:00
|
|
|
async get_roles(req, res, next) {
|
|
|
|
const role_config = this.configs.get('auth.roles')
|
|
|
|
const data = []
|
|
|
|
for ( const role_name in role_config ) {
|
|
|
|
if ( !role_config.hasOwnProperty(role_name) ) continue
|
|
|
|
data.push({
|
|
|
|
role: role_name,
|
|
|
|
permissions: role_config[role_name],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.api(data)
|
2020-04-22 14:19:25 +00:00
|
|
|
}
|
|
|
|
|
2020-05-17 04:55:08 +00:00
|
|
|
async get_group(req, res, next) {
|
|
|
|
const Group = this.models.get('auth:Group')
|
|
|
|
const group = await Group.findById(req.params.id)
|
|
|
|
|
|
|
|
if ( !group || !group.active )
|
|
|
|
return res.status(404)
|
|
|
|
.message('Group not found with that ID.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.user.can(`auth:group:${group.id}:view`) )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
return res.api(await group.to_api())
|
|
|
|
}
|
|
|
|
|
|
|
|
async get_user(req, res, next) {
|
|
|
|
if ( req.params.id === 'me' )
|
|
|
|
return res.api(await req.user.to_api())
|
|
|
|
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
const user = await User.findById(req.params.id)
|
|
|
|
|
|
|
|
if ( !user )
|
|
|
|
return res.status(404)
|
|
|
|
.message('User not found with that ID.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.user.can(`auth:user:${user.id}:view`) )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
return res.api(await user.to_api())
|
|
|
|
}
|
|
|
|
|
|
|
|
async create_group(req, res, next) {
|
|
|
|
if ( !req.user.can(`auth:group:create`) )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.body.name )
|
|
|
|
return res.status(400)
|
|
|
|
.message('Missing required field: name')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
const Group = this.models.get('auth:Group')
|
|
|
|
|
|
|
|
// Make sure the name is free
|
|
|
|
const existing_group = await Group.findOne({ name: req.body.name })
|
|
|
|
if ( existing_group )
|
|
|
|
return res.status(400)
|
|
|
|
.message('A group with that name already exists.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
const group = new Group({ name: req.body.name })
|
|
|
|
|
|
|
|
// Validate user ids
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
if ( req.body.user_ids ) {
|
|
|
|
const parsed = typeof req.body.user_ids === 'string' ? this.utility.infer(req.body.user_ids) : req.body.user_ids
|
|
|
|
const user_ids = Array.isArray(parsed) ? parsed : [parsed]
|
|
|
|
for ( const user_id of user_ids ) {
|
|
|
|
const user = await User.findById(user_id)
|
|
|
|
if ( !user )
|
|
|
|
return res.status(400)
|
|
|
|
.message('Invalid user_id.')
|
|
|
|
.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
group.user_ids = user_ids
|
|
|
|
}
|
|
|
|
|
|
|
|
await group.save()
|
|
|
|
return res.api(await group.to_api())
|
|
|
|
}
|
|
|
|
|
|
|
|
async create_user(req, res, next) {
|
|
|
|
if ( !req.user.can('auth:user:create') )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
const required_fields = ['uid', 'first_name', 'last_name', 'email', 'password']
|
|
|
|
for ( const field of required_fields ) {
|
|
|
|
if ( !req.body[field] )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`Missing required field: ${field}`)
|
|
|
|
.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure uid & email are unique
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
const unique_fields = ['uid', 'email']
|
|
|
|
for ( const field of unique_fields ) {
|
|
|
|
const filter = {}
|
|
|
|
filter[field] = req.body[field]
|
|
|
|
const existing_user = await User.findOne(filter)
|
|
|
|
if ( existing_user )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`A user already exists with that ${field}`)
|
|
|
|
.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify password complexity
|
|
|
|
const min_score = 3
|
|
|
|
const result = zxcvbn(req.body.password)
|
|
|
|
if ( result.score < min_score )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`Password does not meet the minimum complexity score of ${min_score}.`)
|
|
|
|
.api()
|
|
|
|
|
|
|
|
const user = new User({
|
|
|
|
uid: req.body.uid,
|
|
|
|
email: req.body.email,
|
|
|
|
first_name: req.body.first_name,
|
|
|
|
last_name: req.body.last_name,
|
|
|
|
})
|
|
|
|
|
|
|
|
if ( req.body.tagline )
|
|
|
|
user.tagline = req.body.tagline
|
|
|
|
|
|
|
|
await user.reset_password(req.body.password, 'create')
|
|
|
|
await user.save()
|
|
|
|
return res.api(await user.to_api())
|
|
|
|
}
|
|
|
|
|
|
|
|
async update_group(req, res, next) {
|
|
|
|
const Group = this.models.get('auth:Group')
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
|
|
|
|
const group = await Group.findById(req.params.id)
|
|
|
|
if ( !group )
|
|
|
|
return res.status(404)
|
|
|
|
.message('Group not found with that ID.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.user.can(`auth:group:${group.id}:update`) )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.body.name )
|
|
|
|
return res.status(400)
|
|
|
|
.message('Missing required field: name')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
// Make sure the group name is unique
|
|
|
|
const existing_group = await Group.findOne({ name: req.body.name })
|
|
|
|
if ( existing_group && existing_group.id !== group.id )
|
|
|
|
return res.status(400)
|
|
|
|
.message('A group with that name already exists.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
// Validate user_ids
|
|
|
|
if ( req.body.user_ids ) {
|
|
|
|
const parsed = typeof req.body.user_ids === 'string' ? this.utility.infer(req.body.user_ids) : req.body.user_ids
|
|
|
|
const user_ids = Array.isArray(parsed) ? parsed : [parsed]
|
|
|
|
for ( const user_id of user_ids ) {
|
|
|
|
const user = await User.findById(user_id)
|
|
|
|
if ( !user )
|
|
|
|
return res.status(400)
|
|
|
|
.message('Invalid user_id.')
|
|
|
|
.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
group.user_ids = user_ids
|
|
|
|
} else {
|
|
|
|
group.user_ids = []
|
|
|
|
}
|
|
|
|
|
|
|
|
group.name = req.body.name
|
|
|
|
await group.save()
|
|
|
|
return res.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
async update_user(req, res, next) {
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
const user = await User.findById(req.params.id)
|
|
|
|
|
|
|
|
if ( !user )
|
|
|
|
return res.status(404)
|
|
|
|
.message('User not found with that ID.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.user.can(`auth:user:${user.id}:update`) )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
const required_fields = ['uid', 'first_name', 'last_name', 'email']
|
|
|
|
for ( const field of required_fields ) {
|
|
|
|
if ( !req.body[field] )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`Missing required field: ${field}`)
|
|
|
|
.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure the uid/email are unique
|
|
|
|
const unique_fields = ['uid', 'email']
|
|
|
|
for ( const field of unique_fields ) {
|
|
|
|
const filter = {}
|
|
|
|
filter[field] = req.body[field]
|
|
|
|
const existing_user = await User.findOne(filter)
|
|
|
|
if ( existing_user && existing_user.id !== user.id )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`A user already exists with that ${field}`)
|
|
|
|
.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify password complexity
|
|
|
|
if ( req.body.password ) {
|
|
|
|
const min_score = 3
|
|
|
|
const result = zxcvbn(req.body.password)
|
|
|
|
if (result.score < min_score)
|
|
|
|
return res.status(400)
|
|
|
|
.message(`Password does not meet the minimum complexity score of ${min_score}.`)
|
|
|
|
.api()
|
|
|
|
|
|
|
|
await user.reset_password(req.body.password, 'api')
|
|
|
|
}
|
|
|
|
|
|
|
|
user.first_name = req.body.first_name
|
|
|
|
user.last_name = req.body.last_name
|
|
|
|
user.uid = req.body.uid
|
|
|
|
user.email = req.body.email
|
|
|
|
|
|
|
|
if ( req.body.tagline )
|
|
|
|
user.tagline = req.body.tagline
|
|
|
|
else
|
|
|
|
user.tagline = ''
|
|
|
|
|
|
|
|
await user.save()
|
|
|
|
return res.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete_group(req, res, next) {
|
|
|
|
const Group = this.models.get('auth:Group')
|
|
|
|
const group = await Group.findById(req.params.id)
|
|
|
|
|
|
|
|
if ( !group )
|
|
|
|
return res.status(404)
|
|
|
|
.message('Group not found with that ID.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.user.can(`auth:group:${group.id}:delete`) )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
group.active = false
|
|
|
|
await group.save()
|
|
|
|
return res.api()
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete_user(req, res, next) {
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
const user = await User.findById(req.params.id)
|
|
|
|
|
|
|
|
if ( !user )
|
|
|
|
return res.status(404)
|
|
|
|
.message('User not found with that ID.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
if ( !req.user.can(`auth:user:${user.id}:delete`) )
|
|
|
|
return res.status(401)
|
|
|
|
.message('Insufficient permissions.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
// check if the user is an LDAP client. if so, delete the client
|
|
|
|
const Client = this.models.get('ldap:Client')
|
|
|
|
const matching_client = await Client.findOne({ user_id: user.id })
|
|
|
|
if ( matching_client ) {
|
|
|
|
matching_client.active = false
|
|
|
|
await matching_client.save()
|
|
|
|
}
|
|
|
|
|
|
|
|
user.active = false
|
|
|
|
await user.kickout()
|
|
|
|
await user.save()
|
|
|
|
return res.api()
|
|
|
|
}
|
|
|
|
|
2020-04-22 14:19:25 +00:00
|
|
|
async validate_username(req, res, next) {
|
|
|
|
let is_valid = true
|
|
|
|
|
|
|
|
if ( !req.body.username ) is_valid = false
|
|
|
|
|
|
|
|
if ( is_valid ) {
|
|
|
|
const User = this.models.get('auth:User')
|
|
|
|
const user = await User.findOne({uid: req.body.username})
|
|
|
|
if ( !user || !user.can_login ) is_valid = false
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.api({ is_valid })
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO XSRF Token
|
|
|
|
/*
|
|
|
|
* Request Params:
|
|
|
|
* - username
|
|
|
|
* - password
|
|
|
|
* - [create_session = false]
|
|
|
|
*/
|
|
|
|
async attempt(req, res, next) {
|
|
|
|
const flitter = this.auth.get_provider('flitter')
|
|
|
|
|
|
|
|
const errors = await flitter.validate_login(req.body)
|
|
|
|
if ( errors && errors.length > 0 )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`Unable to complete authentication: one or more errors occurred`)
|
|
|
|
.api({ errors })
|
|
|
|
|
|
|
|
const login_args = await flitter.get_login_args(req.body)
|
|
|
|
const user = await flitter.login.apply(flitter, login_args)
|
|
|
|
|
|
|
|
if ( !user )
|
|
|
|
return res.status(200)
|
|
|
|
.message(`Invalid username or password.`)
|
|
|
|
.api({
|
|
|
|
message: `Invalid username or password.`,
|
|
|
|
success: false,
|
|
|
|
})
|
|
|
|
|
2020-05-17 04:55:08 +00:00
|
|
|
// Make sure the user can sign in.
|
|
|
|
// Sign-in is NOT allowed for LDAP clients
|
|
|
|
const Client = this.models.get('ldap:Client')
|
|
|
|
const client = await Client.findOne({ user_id: user.id })
|
|
|
|
if ( client )
|
|
|
|
return res.status(200)
|
|
|
|
.message(`Invalid username or password.`)
|
|
|
|
.api({
|
|
|
|
message: `Invalid username or password.`,
|
|
|
|
success: false,
|
|
|
|
})
|
|
|
|
|
2020-04-22 14:19:25 +00:00
|
|
|
if ( req.body.create_session )
|
|
|
|
await flitter.session(req, user)
|
|
|
|
|
|
|
|
let destination = this.configs.get('auth.default_login_route')
|
2020-04-22 21:56:39 +00:00
|
|
|
if ( req.session.auth.flow ) {
|
2020-04-22 14:19:25 +00:00
|
|
|
destination = req.session.auth.flow
|
2020-04-22 21:56:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( user.mfa_enabled && !req.session.mfa_remember ) {
|
|
|
|
req.session.auth.in_dmz = true
|
|
|
|
destination = '/auth/mfa/challenge'
|
2020-04-22 14:19:25 +00:00
|
|
|
}
|
|
|
|
|
2020-05-04 01:16:54 +00:00
|
|
|
if ( req.session?.auth?.message )
|
|
|
|
delete req.session.auth.message
|
|
|
|
|
|
|
|
// If we're doing a trust flow, check the grant
|
|
|
|
if ( req.body.grant_code && req.trust.has_flow() ) {
|
|
|
|
if ( req.trust.check_grant(req.body.grant_code) ) {
|
|
|
|
req.trust.grant(req.trust.flow_scope())
|
|
|
|
|
|
|
|
// Trust re-verification is granted,
|
|
|
|
// but the user might still need to verify MFA
|
|
|
|
const next = req.trust.end()
|
|
|
|
if ( req.session.auth.in_dmz ) {
|
|
|
|
req.session.auth.flow = next
|
|
|
|
} else {
|
|
|
|
destination = next
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return res.status(401)
|
|
|
|
.message(`Unable to grant trust. Grant token is invalid.`)
|
|
|
|
.api()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-22 14:19:25 +00:00
|
|
|
return res.api({
|
|
|
|
success: true,
|
|
|
|
session_created: !!req.body.create_session,
|
|
|
|
next: destination,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-04-22 21:56:39 +00:00
|
|
|
async generate_mfa_key(req, res, next) {
|
|
|
|
if ( req.user.mfa_enabled )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`MFA already configured for user. Cannot fetch key.`)
|
|
|
|
.api()
|
|
|
|
|
|
|
|
const MFAToken = this.models.get('auth:MFAToken')
|
|
|
|
const secret = await this.MFA.secret(req.user)
|
|
|
|
|
|
|
|
req.user.mfa_token = new MFAToken({
|
|
|
|
secret: secret.base32,
|
|
|
|
otpauth_url: secret.otpauth_url
|
|
|
|
}, req.user)
|
|
|
|
await req.user.save()
|
|
|
|
|
|
|
|
return res.api({
|
|
|
|
success: true,
|
|
|
|
secret: secret.base32,
|
|
|
|
otpauth_url: secret.otpauth_url,
|
|
|
|
qr_code: await this.MFA.qr_code(secret)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async attempt_mfa(req, res, next) {
|
|
|
|
if ( !req.user.mfa_token )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`The user does not have MFA configured.`)
|
|
|
|
.api()
|
|
|
|
|
|
|
|
const code = req.body.verify_code
|
|
|
|
const token = req.user.mfa_token
|
|
|
|
const is_valid = token.verify(code)
|
|
|
|
|
|
|
|
let next_destination = undefined
|
|
|
|
if ( is_valid ) {
|
|
|
|
req.session.auth.in_dmz = false
|
|
|
|
next_destination = req.session.auth.flow || this.configs.get('auth.default_login_route')
|
|
|
|
delete req.session.auth.flow
|
|
|
|
}
|
|
|
|
|
|
|
|
req.session.mfa_remember = true
|
|
|
|
|
|
|
|
return res.api({
|
|
|
|
success: true,
|
|
|
|
verify_code: code,
|
|
|
|
is_valid,
|
|
|
|
next_destination,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async enable_mfa(req, res, next) {
|
|
|
|
if ( !req.user.mfa_token )
|
|
|
|
return res.status(400)
|
|
|
|
.message(`The user does not have an MFA token configured.`)
|
|
|
|
.api()
|
|
|
|
|
|
|
|
req.user.mfa_enabled = true
|
2020-05-04 01:16:54 +00:00
|
|
|
req.user.mfa_enable_date = new Date
|
2020-04-22 21:56:39 +00:00
|
|
|
req.user.save()
|
|
|
|
|
2020-05-04 01:16:54 +00:00
|
|
|
// invalidate existing tokens and other logins
|
2020-04-22 21:56:39 +00:00
|
|
|
const flitter = await this.auth.get_provider('flitter')
|
|
|
|
await flitter.logout(req)
|
2020-05-04 01:16:54 +00:00
|
|
|
await req.user.kickout()
|
2020-04-22 21:56:39 +00:00
|
|
|
|
|
|
|
return res.api({success: true, mfa_enabled: req.user.mfa_enabled})
|
|
|
|
}
|
|
|
|
|
2020-05-04 01:16:54 +00:00
|
|
|
async disable_mfa(req, res, next) {
|
|
|
|
if ( !req.user.mfa_enabled )
|
|
|
|
return res.status(400)
|
|
|
|
.message('The user does not have MFA enabled.')
|
|
|
|
.api()
|
|
|
|
|
|
|
|
req.user.mfa_enabled = false
|
|
|
|
delete req.user.mfa_enable_date
|
|
|
|
delete req.user.mfa_token
|
|
|
|
req.user.app_passwords = []
|
|
|
|
await req.user.save()
|
|
|
|
|
|
|
|
// invalidate existing login tokens and logins
|
|
|
|
const flitter = await this.auth.get_provider('flitter')
|
|
|
|
await flitter.logout(req)
|
|
|
|
await req.user.kickout()
|
|
|
|
|
|
|
|
return res.api({success: true, mfa_enabled: req.user.mfa_enabled})
|
|
|
|
}
|
|
|
|
|
|
|
|
async get_mfa_enable_date(req, res, next) {
|
|
|
|
if ( !req.user.mfa_enabled )
|
|
|
|
return res.api({ mfa_enabled: false })
|
|
|
|
|
|
|
|
return res.api({ mfa_enabled: true, mfa_enable_date: req.user.mfa_enable_date })
|
|
|
|
}
|
2020-04-22 14:19:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = exports = AuthController
|