var mongoose = require('mongoose'); var Schema = mongoose.Schema; var bcrypt = require('bcrypt'); var jwt = require('jsonwebtoken'); var auth = require('../lib/auth.js'); const { generateUUID } = require('../lib/utils.js'); var _ = require('lodash'); var QRCode = require('qrcode'); var OTPAuth = require('otpauth'); var UserSchema = new Schema( { username: { type: String, unique: true, required: true }, password: { type: String, required: true }, firstname: { type: String, required: true }, lastname: { type: String, required: true }, email: { type: String, required: false }, phone: { type: String, required: false }, role: { type: String, default: 'user' }, totpEnabled: { type: Boolean, default: false }, totpSecret: { type: String, default: '' }, enabled: { type: Boolean, default: true }, refreshTokens: [ { _id: false, sessionId: String, userAgent: String, token: String }, ], }, { timestamps: true }, ); var totpConfig = { issuer: 'AuditForge', label: '', algorithm: 'SHA1', digits: 6, period: 30, secret: '', }; //check TOTP token var checkTotpToken = function (token, secret) { if (!token) throw { fn: 'BadParameters', message: 'TOTP token required' }; if (token.length !== 6) throw { fn: 'BadParameters', message: 'Invalid TOTP token length' }; if (!secret) throw { fn: 'BadParameters', message: 'TOTP secret required' }; let newConfig = totpConfig; newConfig.secret = secret; let totp = new OTPAuth.TOTP(newConfig); let delta = totp.validate({ token: token, window: 5, }); //The token is valid in 2 windows in the past and the future, current window is 0. if (delta === null) { throw { fn: 'Unauthorized', message: 'Wrong TOTP token.' }; } else if (delta < -2 || delta > 2) { throw { fn: 'Unauthorized', message: 'TOTP token out of window.' }; } return true; }; /* *** Statics *** */ // Create user UserSchema.statics.create = function (user) { return new Promise((resolve, reject) => { var hash = bcrypt.hashSync(user.password, 10); user.password = hash; new User(user) .save() .then(function () { resolve(); }) .catch(function (err) { if (err.code === 11000) reject({ fn: 'BadParameters', message: 'Username already exists' }); else reject(err); }); }); }; // Get all users UserSchema.statics.getAll = function () { return new Promise((resolve, reject) => { var query = this.find(); query.select( 'username firstname lastname email phone role totpEnabled enabled', ); query .exec() .then(function (rows) { resolve(rows); }) .catch(function (err) { reject(err); }); }); }; // Get one user by its username UserSchema.statics.getByUsername = function (username) { return new Promise((resolve, reject) => { var query = this.findOne({ username: username }); query.select( 'username firstname lastname email phone role totpEnabled enabled', ); query .exec() .then(function (row) { if (row) resolve(row); else throw { fn: 'NotFound', message: 'User not found' }; }) .catch(function (err) { reject(err); }); }); }; // Update user with password verification (for updating my profile) UserSchema.statics.updateProfile = function (username, user) { return new Promise((resolve, reject) => { var query = this.findOne({ username: username }); var payload = {}; query .exec() .then(function (row) { if (!row) throw { fn: 'NotFound', message: 'User not found' }; else if (bcrypt.compareSync(user.password, row.password)) { if (user.username) row.username = user.username; if (user.firstname) row.firstname = user.firstname; if (user.lastname) row.lastname = user.lastname; if (!_.isNil(user.email)) row.email = user.email; if (!_.isNil(user.phone)) row.phone = user.phone; if (user.newPassword) row.password = bcrypt.hashSync(user.newPassword, 10); if (typeof user.totpEnabled == 'boolean') row.totpEnabled = user.totpEnabled; payload.id = row._id; payload.username = row.username; payload.role = row.role; payload.firstname = row.firstname; payload.lastname = row.lastname; payload.email = row.email; payload.phone = row.phone; payload.roles = auth.acl.getRoles(payload.role); return row.save(); } else throw { fn: 'Unauthorized', message: 'Current password is invalid' }; }) .then(function () { var token = jwt.sign(payload, auth.jwtSecret, { expiresIn: '15 minutes', }); resolve({ token: `JWT ${token}` }); }) .catch(function (err) { if (err.code === 11000) reject({ fn: 'BadParameters', message: 'Username already exists' }); else reject(err); }); }); }; // Update user (for admin usage) UserSchema.statics.updateUser = function (userId, user) { return new Promise((resolve, reject) => { if (user.password) user.password = bcrypt.hashSync(user.password, 10); var query = this.findOneAndUpdate({ _id: userId }, user); query .exec() .then(function (row) { if (row) resolve('User updated successfully'); else reject({ fn: 'NotFound', message: 'User not found' }); }) .catch(function (err) { if (err.code === 11000) reject({ fn: 'BadParameters', message: 'Username already exists' }); else reject(err); }); }); }; // Update refreshtoken UserSchema.statics.updateRefreshToken = function (refreshToken, userAgent) { return new Promise((resolve, reject) => { var token = ''; var newRefreshToken = ''; try { var decoded = jwt.verify(refreshToken, auth.jwtRefreshSecret); var userId = decoded.userId; var sessionId = decoded.sessionId; var expiration = decoded.exp; } catch (err) { if (err.name === 'TokenExpiredError') throw { fn: 'Unauthorized', message: 'Expired refreshToken' }; else throw { fn: 'Unauthorized', message: 'Invalid refreshToken' }; } var query = this.findById(userId); query .exec() .then(row => { if (row && row.enabled !== false) { // Check session exist and sessionId not null (if null then it is a login) if (sessionId !== null) { var sessionExist = row.refreshTokens.findIndex( e => e.sessionId === sessionId && e.token === refreshToken, ); if (sessionExist === -1) // Not found throw { fn: 'Unauthorized', message: 'Session not found' }; } // Generate new token var payload = {}; payload.id = row._id; payload.username = row.username; payload.role = row.role; payload.firstname = row.firstname; payload.lastname = row.lastname; payload.email = row.email; payload.phone = row.phone; payload.roles = auth.acl.getRoles(payload.role); token = jwt.sign(payload, auth.jwtSecret, { expiresIn: '15 minutes', }); // Remove expired sessions row.refreshTokens = row.refreshTokens.filter(e => { try { var decoded = jwt.verify(e.token, auth.jwtRefreshSecret); } catch (err) { var decoded = null; } return decoded !== null; }); // Update or add new refresh token var foundIndex = row.refreshTokens.findIndex( e => e.sessionId === sessionId, ); if (foundIndex === -1) { // Not found sessionId = generateUUID(); newRefreshToken = jwt.sign( { sessionId: sessionId, userId: userId }, auth.jwtRefreshSecret, { expiresIn: '7 days' }, ); row.refreshTokens.push({ sessionId: sessionId, userAgent: userAgent, token: newRefreshToken, }); } else { newRefreshToken = jwt.sign( { sessionId: sessionId, userId: userId, exp: expiration }, auth.jwtRefreshSecret, ); row.refreshTokens[foundIndex].token = newRefreshToken; } return row.save(); } else if (row) { reject({ fn: 'Unauthorized', message: 'Account disabled' }); } else reject({ fn: 'NotFound', message: 'Session not found' }); }) .then(() => { resolve({ token: token, refreshToken: newRefreshToken }); }) .catch(err => { if (err.code === 11000) reject({ fn: 'BadParameters', message: 'Username already exists' }); else reject(err); }); }); }; // Remove session UserSchema.statics.removeSession = function (userId, sessionId) { return new Promise((resolve, reject) => { var query = this.findById(userId); query .exec() .then(row => { if (row) { row.refreshTokens = row.refreshTokens.filter( e => e.sessionId !== sessionId, ); return row.save(); } else reject({ fn: 'NotFound', message: 'User not found' }); }) .then(() => { resolve('Session removed successfully'); }) .catch(err => { if (err.code === 11000) reject({ fn: 'BadParameters', message: 'Username already exists' }); else reject(err); }); }); }; // gen totp QRCode url UserSchema.statics.getTotpQrcode = function (username) { return new Promise((resolve, reject) => { let newConfig = totpConfig; newConfig.label = username; const secret = new OTPAuth.Secret({ size: 10, }).base32; newConfig.secret = secret; let totp = new OTPAuth.TOTP(newConfig); let totpUrl = totp.toString(); QRCode.toDataURL(totpUrl, function (err, url) { resolve({ totpQrCode: url, totpSecret: secret, }); }); }); }; // verify TOTP and Setup enabled status and secret code UserSchema.statics.setupTotp = function (token, secret, username) { return new Promise((resolve, reject) => { checkTotpToken(token, secret); var query = this.findOne({ username: username }); query .exec() .then(function (row) { if (!row) throw { errmsg: 'User not found' }; else if (row.totpEnabled === true) throw { errmsg: 'TOTP already enabled by this user' }; else { row.totpEnabled = true; row.totpSecret = secret; return row.save(); } }) .then(function () { resolve({ msg: true }); }) .catch(function (err) { reject(err); }); }); }; // verify TOTP and Cancel enabled status and secret code UserSchema.statics.cancelTotp = function (token, username) { return new Promise((resolve, reject) => { var query = this.findOne({ username: username }); query .exec() .then(function (row) { if (!row) throw { errmsg: 'User not found' }; else if (row.totpEnabled !== true) throw { errmsg: 'TOTP is not enabled yet' }; else { checkTotpToken(token, row.totpSecret); row.totpEnabled = false; row.totpSecret = ''; return row.save(); } }) .then(function () { resolve({ msg: 'TOTP is canceled.' }); }) .catch(function (err) { reject(err); }); }); }; /* *** Methods *** */ // Authenticate user with username and password, return JWT token UserSchema.methods.getToken = function (userAgent) { return new Promise((resolve, reject) => { var user = this; var query = User.findOne({ username: user.username }); query .exec() .then(function (row) { if (row && row.enabled === false) throw { fn: 'Unauthorized', message: 'Authentication Failed.' }; if (row && bcrypt.compareSync(user.password, row.password)) { if (row.totpEnabled && user.totpToken) checkTotpToken(user.totpToken, row.totpSecret); else if (row.totpEnabled) throw { fn: 'BadParameters', message: 'Missing TOTP token' }; var refreshToken = jwt.sign( { sessionId: null, userId: row._id }, auth.jwtRefreshSecret, ); return User.updateRefreshToken(refreshToken, userAgent); } else { if (!row) { // We compare two random strings to generate delay var randomHash = '$2b$10$' + [...Array(53)].map(() => Math.random().toString(36)[2]).join(''); bcrypt.compareSync(user.password, randomHash); } throw { fn: 'Unauthorized', message: 'Authentication Failed.' }; } }) .then(row => { resolve({ token: row.token, refreshToken: row.refreshToken }); }) .catch(function (err) { reject(err); }); }); }; var User = mongoose.model('User', UserSchema); module.exports = User;