@hicoder/express-auth-server
Version:
Model Driver Development Stack - authentication and authorization server for mongoose and express based application. It can be enabled to work as authentication, user profile managment, and authorization management servers.
681 lines (598 loc) • 18.7 kB
JavaScript
const jwt = require('jsonwebtoken');
const createError = require('http-errors');
const mongoose = require('mongoose');
const { mddsCoreHistoryDef } = require('@hicoder/express-core');
const REFRESH_SECRETE = 'server refresh secret random';
const ACCESS_SECRETE = 'server secret random';
const EMAIL_RESET_SECRETE = 'server secret random for email reset';
const EMAIL_REGISTRATION_SECRETE = 'server secret random for email registration verification';
class AuthnController {
constructor(options) {
this.authSchemas = {};
this.mddsProperties = options || {};
}
registerAuth(schemaName, schema, DB_CONFIG, userFields, passwordField, profileFields, mraBE) {
let db_app_name, db_module_name;
if (DB_CONFIG) {
db_app_name = DB_CONFIG.APP_NAME;
db_module_name = DB_CONFIG.MODULE_NAME;
}
if (!db_app_name || !db_module_name) {
throw new Error(`APP Name and Module Name not provided for database. Please provide "DB_CONFIG" for your schema definition in module ${moduleName}.`);
}
db_app_name = db_app_name.toLowerCase();
db_module_name = db_module_name.toLowerCase();
this.authSchemas[schemaName] = {
schemaName,
userFields,
passwordField,
profileFields,
schema,
model: mongoose.model(schemaName, schema, `${db_app_name}_${db_module_name}_${schemaName.toLowerCase()}`), //model uses given name
};
if (mraBE.enableHistory) {
this.historySchema = {
moduleName: db_module_name,
schemaName,
model: mongoose.model("MddsCoreHistory", mddsCoreHistoryDef.schema, mddsCoreHistoryDef.mraBE.collection),
}
}
}
authLogin(req, res, next) {
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
if (!body || typeof body !== "object") return next(createError(400, "Bad request body."));
let userName;
let fieldName;
let userNames = auth.userFields.split(' ');
for (let n of userNames) {
if (body[n]) {
fieldName = n;
userName = body[n];
break;
}
}
if (body['resetToken']) {
return next(); // this is a password reset from email, no need to compare password. go to next handler.
}
let password;
if (body[auth.passwordField]) {
password = body[auth.passwordField];
}
if (!userName || !password) {
return next(createError(400, "Request is missing required information (user name, or password)."));
}
let query = {};
query[fieldName] = userName;
model.findOne(query, function(err, user) {
if (err) {
return next(err);
}
if (!user) {
return next(createError(403, "User does not exist."));
}
if (user.status === 'Disabled') {
return next(createError(403, "User is currently disabled."));
}
if (user.status === 'Pending') {
return next(createError(403, "User is currently pending for verification."));
}
// test a matching password
user.comparePassword(password, function(err, isMatch) {
if (err || !isMatch) return next(createError(403, "Password is incorrect."));
req.muser = {
"_id": user["_id"],
"userName": userName,
"fieldName": fieldName
};
return next(); //don't send response. need generate Token.
});
});
}
generateToken(req, res, next) {
const accessExpires = 60*60;
const refreshExpires = 60*60*12;
// role permission
let rolep = req.muser.rolep || {};
delete req.muser.rolep;
let accessToken = jwt.sign(
req.muser,
ACCESS_SECRETE,
{expiresIn: accessExpires}
);
let refreshToken = jwt.sign(
req.muser,
REFRESH_SECRETE,
{expiresIn: refreshExpires}
);
let r = {
accessToken,
refreshToken,
userName: req.muser.userName,
fieldName: req.muser.fieldName,
rolep,
expiresIn: refreshExpires,
}
return res.send(r);
}
verifyRefreshToken(req, res, next) {
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
if (!body || typeof body !== "object") return next(createError(400, "Bad request body."));
let token;
let queryKey = "refreshToken";
if (body[queryKey]) {
token = body[queryKey];
}
if (!token) {
return next(createError(400, "Bad request token."));
}
jwt.verify(token, REFRESH_SECRETE, function(err, decoded) {
if (err) {
//return next();
return next(createError(400, "Bad request token."));
}
if (!decoded) return next(createError(400, "Bad request token."));
req.muser = decoded;
return next(); //call authRefresh to check user in DB.
});
}
authRefresh(req, res, next) {
let { _id, userName, fieldName} = req.muser;
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let query = {};
query["_id"] = _id;
model.findOne(query, function(err, user) {
if (err) {
return next(err);
}
if (!user) {
return next(createError(403, "Bad user."));
}
req.muser = {"_id": user["_id"],
'userName': userName,
'fieldName': fieldName
};
return next(); //don't send response. need generate Token.
});
}
getProfile(req, res, next) {
let { _id, userName, fieldName} = req.muser;
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let query = {};
query["_id"] = _id;
model.findOne(query, function(err, user) {
if (err) {
return next(err);
}
if (!user) {
return next(createError(403, "Bad user."));
}
const profile = {"_id": user["_id"]};
let profileFields = [];
if (auth.profileFields) {
profileFields = auth.profileFields.split(' ');
}
for (const f of profileFields) {
profile[f] = user[f];
}
return res.send(profile);
});
}
updateProfile(req, res, next) {
let { _id, userName, fieldName} = req.muser;
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
if (body._id !== _id) {
return next(createError(403, "User id doesn't match."));
}
let query = {};
query["_id"] = _id;
model.findOne(query, (err, result) => {
if (err) {
return next(err);
}
const profile = {"_id": _id};
let profileFields = [];
if (auth.profileFields) {
profileFields = auth.profileFields.split(' ');
}
for (const f of profileFields) {
if (!(f in body)) {
//not in body means user deleted this field
// delete result[field]
result[f] = undefined;
} else {
result[f] = body[f];
profile[f] = body[f];
}
}
result.save((err, r) => {
if (err) {
return next(err);
}
if (this.historySchema) {
console.log("r: ", r);
this.historySchema.model.create(
{
oid: r._id,
module: this.historySchema.moduleName,
schema: this.historySchema.schemaName,
action: 'update',
user: r._id,
document: JSON.stringify(r),
}, (err, r) => {
// TODO: error handling
},
);
}
return res.send(profile);
});
});
}
authRegister(req, res, next) {
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
let userNameGiven = false;
let userNames = auth.userFields.split(' ');
for (let n of userNames) {
if (body[n]) {
userNameGiven = true;
break;
};
}
let password;
if (body[auth.passwordField]) {
password = body[auth.passwordField];
}
if (!userNameGiven || !password) {
return next(createError(400, "Bad register request: missing info."));
}
const registrationEmailVerification = this.mddsProperties.registerEmailVerification;
if (registrationEmailVerification) {
body.status = "Pending";
}
model.create(body, async (err, result) => {
if (err) {
if (err.name === 'MongoError' && err.code === 11000) {
// Duplicate
return next(createError(400, "Bad request body. User already exists."));
}
return next(err);
}
if (this.historySchema) {
this.historySchema.model.create(
{
oid: result._id,
module: this.historySchema.moduleName,
schema: this.historySchema.schemaName,
action: 'insert',
user: result._id,
document: JSON.stringify(result),
}, (err, r) => {
// TODO: error handling
},
);
}
if (registrationEmailVerification) {
let email = body['email'];
let userName = 'user';
try {
await this.sendRegVerificationEmail(userName, email);
} catch (err1) {
return next(err1);
}
}
const tempExpiresIn = 60 * 60; // 1hour
let temporaryToken = jwt.sign(
{
"_id": result["_id"],
},
ACCESS_SECRETE,
{expiresIn: tempExpiresIn}
);
const returnObj = {registrationEmailVerification, temporaryToken, expiresIn: tempExpiresIn};
return res.send(returnObj);
});
};
authVerifyReg(req, res, next) {
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
if (!body || !body.verificationToken) {
return next(createError(400, "Bad verification request: missing token info."));
}
const verificationToken = body.verificationToken;
const query = {};
try {
const decoded = jwt.verify(verificationToken, EMAIL_REGISTRATION_SECRETE);
query['email'] = decoded.email;
} catch (err) {
return next(createError(400, "Bad request: invalid registration token."));
}
model.findOne(query, function (err, result) {
if (err) {
return next(err);
}
if (result['status'] === 'Disabled') {
return next(createError(403, "User is currently disabled."));
}
if (result['status'] === 'Enabled') {
return res.send();
}
result['status'] = 'Enabled';
result.save(function (err1, r) {
if (err1) {
return next(err1);
}
return res.send();
});
});
}
// Not used yet.
authResendRegVerification(req, res, next) {
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
let email;
if (body['email']) {
email = body['email'];
}
if (!email) {
return next(createError(400, "Bad request: missing required information (email)."));
}
const query = {};
query['email'] = email;
model.findOne(query, async (err, result) => {
if (err) {
return next(err);
}
if (!result) {
return next(createError(400, "Bad request: user not registered."));
}
if (result.status !== 'Pending') {
return next(createError(400, "Bad request: user has been verified."));
}
try {
let userName = 'user';
await this.sendRegVerificationEmail(userName, email);
} catch (err1) {
return next(err1);
}
return res.send();
});
}
async sendRegVerificationEmail(userName, email) {
const { emailer, emailerObj } = this.mddsProperties || {};
if (!emailer) {
throw createError(503, 'Emailing service is not available');
}
let verificationToken = jwt.sign(
{email},
EMAIL_REGISTRATION_SECRETE,
{expiresIn: 60*60*24*30} // 30 days
);
const tag = 'registrationverification';
const obj = {
userName,
link: emailerObj.serverUrlRegVerification + verificationToken
};
const result = await emailer.sendEmailTemplate([email], tag, obj);
// result: {success: 1, fail: 0, pending: 0, errors: []}
if (result.fail === 1) {
throw result.errors[0] || new Error('Email send failed: unknown error.');
}
return;
}
changePass(req, res, next) {
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
let userName;
let fieldName;
let userNames = auth.userFields.split(' ');
for (let n of userNames) {
if (body[n]) {
fieldName = n;
userName = body[n];
break;
}
}
let resetToken;
if (body['resetToken']) {
resetToken = body['resetToken'];
}
let password;
if (body['newPassword']) {
password = body['newPassword'];
}
if (!password) {
return next(createError(400, "Bad request: missing required information (new password)."));
}
// this function is called, either passing the authLogin username/password check, of from a token;
if (!resetToken && !userName) {
return next(createError(400, "Bad request: missing required information (user info)."));
}
const query = {};
if (resetToken) {
try {
const decoded = jwt.verify(resetToken, EMAIL_RESET_SECRETE);
query['email'] = decoded.email;
} catch (err) {
return next(createError(400, "Bad request: invalid reset token."));
}
} else {
query[fieldName] = userName;
}
model.findOne(query, (err, result) => {
if (err) {
return next(err);
}
result[auth.passwordField] = password;
result.save( (err, r) => {
if (err) {
return next(err);
}
if (this.historySchema) {
this.historySchema.model.create(
{
oid: r._id,
module: this.historySchema.moduleName,
schema: this.historySchema.schemaName,
action: 'update',
user: r._id,
document: JSON.stringify(r),
}, (err, r) => {
// TODO: error handling
},
);
}
return res.send();
});
});
};
findPass(req, res, next) {
let authSchemaName = req.authSchemaName;
let auth = this.authSchemas[authSchemaName];
let model = auth.model;
if (!model) {
return next(createError(400, "Authentication server is not provisioned."));
}
let body = req.body;
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch(e) {
return next(createError(400, "Bad request body."));
}
}
let email;
if (body['email']) {
email = body['email'];
}
if (!email) {
return next(createError(400, "Bad request: missing required information (email)."));
}
const query = {};
query['email'] = email;
model.findOne(query, async (err, result) => {
if (err) {
return next(err);
}
if (!result) {
return next(createError(400, "Bad request: user not registered."));
}
let resetToken = jwt.sign(
{email},
EMAIL_RESET_SECRETE,
{expiresIn: 60*60*24}
);
let userName = 'user';
const { emailer, emailerObj } = this.mddsProperties || {};
if (!emailer) {
return next(createError(503, 'Emailing service is not available'));
}
const tag = 'resetpassword';
const obj = {
userName,
link: emailerObj.serverUrlPasswordReset + resetToken
};
try {
const result = await emailer.sendEmailTemplate([email], tag, obj);
// result: {success: 1, fail: 0, pending: 1, errors: []}
if (result.fail === 1) {
return next(result.errors[0] || new Error('Email send failed: unknown error.'));
}
} catch (err2) {
return next(err2);
}
return res.send();
});
};
}
module.exports = AuthnController;