Kaballas's picture
initialize project structure with essential configurations and components
56b6519
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;