const { Controller } = require('libflitter') const zxcvbn = require('zxcvbn') const email_validator = require('email-validator') class AuthController extends Controller { static get services() { return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs', 'utility'] } async get_auth_user(req, res, next) { if ( req.user ) { return res.api({ authenticated: true, uid: req.user.uid, }) } return res.api({ authenticated: false }) } async get_traps(req, res, next) { const trap_config = this.configs.get('traps') const data = [{ name: '(None)', trap: '', redirect_to: '/' }] for ( const name in trap_config.types ) { if ( !trap_config.types.hasOwnProperty(name) ) continue data.push({ name: name.replace(/_/g, ' ') .split(' ') .map(x => x.charAt(0).toUpperCase() + x.substr(1)) .join(' '), trap: name, redirect_to: trap_config.types[name].redirect_to }) } return res.api(data) } async registration(req, res, next) { const User = this.models.get('auth:User') const required_fields = ['first_name', 'last_name', 'uid', 'email'] const unique_fields = ['uid', 'email'] for ( const field of required_fields ) { if ( !req.body[field] ) return res.status(400) .message(`Missing required field: ${field}`) .api() } if ( !req.body.uid.match(/^([A-Z]|[a-z]|[0-9]|_|-|\.)+$/) ) return res.status(400) .message('Invalid field: uid (should be alphanumeric with "_", "-", and "." allowed)') .api() if ( !email_validator.validate(req.body.email) ) return res.status(400) .message('Invalid field: email') .api() for ( const field of unique_fields ) { const params = {} params[field] = req.body[field] const match_user = await User.findOne(params) if ( match_user ) return res.status(400) .message(`A user already exists with that ${field}.`) .api() } const user = new User({ first_name: req.body.first_name, last_name: req.body.last_name, uid: req.body.uid, email: req.body.email, trap: 'password_reset', // Force user to reset password }) user.promote('base_user') await user.save() // Log in the user automatically await this.auth.get_provider().session(req, user) return res.api(await user.to_api()) } async attempt_mfa_recovery(req, res, next) { if ( !req.user.mfa_enabled || !Array.isArray(req.user.mfa_token.recovery_codes) || req.user.mfa_token.recovery_codes.length < 1 ) return res.status(400) .message('Your user is not configured to use MFA, or has no recovery codes.') .api() if ( !req.body.code ) return res.status(400) .message('Missing required field: code') .api() const success = await req.user.mfa_token.attempt_recovery(req.body.code) if ( !success ) return res.api({ success }) if ( req.trap.has_trap('mfa_challenge') ) await req.trap.end() let 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, next_destination, remaining_codes: req.user.mfa_token.recovery_codes.filter(x => !x.used).length, }) } async validate_email(req, res, next) { let is_valid = !!req.body.email if ( is_valid ) { is_valid = email_validator.validate(req.body.email) } return res.api({ is_valid }) } 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) } 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) } 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) } 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 if ( req.body.trap ) { if ( !req.trap.trap_exists(req.body.trap) ) return res.status(400) .message('Invalid trap type.') .api() user.trap = req.body.trap } 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 = '' if ( req.body.trap ) { if ( !req.trap.trap_exists(req.body.trap) ) return res.status(400) .message('Invalid trap type.') .api() user.trap = req.body.trap } else user.trap = '' 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() } 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 }) } async user_exists(req, res, next) { const User = this.models.get('auth:User') if ( !req.body.username && !req.body.email ) return res.status(400) .message('Please provide one of: username, email') .api() const data = {} if ( req.body.username ) { const existing_user = await User.findOne({ uid: req.body.username, }) data.username_taken = !!existing_user } if ( req.body.email ) { const existing_user = await User.findOne({ email: req.body.email, }) data.email_taken = !!existing_user } return res.api(data) } // 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, }) // 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, }) if ( req.body.create_session ) await flitter.session(req, user) let destination = this.configs.get('auth.default_login_route') if ( req.session.auth.flow ) { destination = req.session.auth.flow } if ( user.mfa_enabled && !req.session.mfa_remember ) { await req.trap.begin('mfa_challenge', { session_only: true }) } 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.trap.has_trap('mfa_challenge') ) { req.session.auth.flow = next } else { destination = next } } else { return res.status(401) .message(`Unable to grant trust. Grant token is invalid.`) .api() } } return res.api({ success: true, session_created: !!req.body.create_session, next: destination, }) } async get_mfa_recovery(req, res, next) { if ( !req.user.mfa_enabled ) return res.status(400) .message('Your user does not have MFA enabled.') .api() const token = req.user.mfa_token if ( !Array.isArray(token.recovery_codes) || token.recovery_codes.length < 1 ) return res.api({ has_recovery: false }) return res.api({ has_recovery: true, generated: token.recovery_codes[0].generated, remaining_codes: token.recovery_codes.filter(x => !x.used).length, }) } async generate_mfa_recovery(req, res, next) { if ( !req.user.mfa_enabled ) return res.status(400) .message('Your user does not have MFA enabled.') .api() const token = req.user.mfa_token const codes = await token.generate_recovery() await req.user.save() return res.api({ codes, }) } 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 ) { if ( req.trap.has_trap('mfa_challenge') ) await req.trap.end() 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 req.user.mfa_enable_date = new Date req.user.save() // invalidate existing tokens and other 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 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 }) } } module.exports = exports = AuthController