realm-object-server
Version:
424 lines • 20.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const assert = require("assert");
const crypto = require("crypto");
const moment = require("moment");
const nodemailer = require("nodemailer");
const CircularBuffer = require("circular-buffer");
const AuthProvider_1 = require("../AuthProvider");
const errors = require("../../errors");
const shared_1 = require("../../shared");
const PasswordRealm_1 = require("./PasswordRealm");
const emails_1 = require("./emails");
class PasswordAuthProvider extends AuthProvider_1.AuthProvider {
constructor(config = {}) {
super();
this.name = "password";
this.autoCreateAdminUser = config.autoCreateAdminUser || false;
this.saltLength = config.saltLength || 32;
this.iterations = config.iterations || 10000;
this.keyLength = config.keyLength || 512;
this.digest = config.digest || "sha512";
this.hashBuffer = new CircularBuffer(config.hashBufferSize || 25);
this.config = config;
}
start() {
const _super = Object.create(null, {
start: { get: () => super.start }
});
return __awaiter(this, void 0, void 0, function* () {
yield _super.start.call(this);
this.passwordRealm = yield this.service.server.openRealm(PasswordRealm_1.PasswordRealm);
if (this.autoCreateAdminUser) {
try {
const username = "realm-admin";
const foundUser = this.service.getUserByProviderId(this.name, username);
if (!foundUser) {
const password = process.env.ROS_DEFAULT_PASSWORD || "";
yield this.attemptToRegister(username, password, true);
this.service.logger.info(`Autocreated admin user: ${username}`);
}
}
catch (err) {
this.service.logger.error(`Failed to autocreate admin user: ${err}`);
}
}
if (this.config.emailHandler) {
this.emailHandler = this.config.emailHandler;
}
else if (this.config.emailHandlerConfig) {
try {
this.emailHandler = new BasicEmailHandler(this.config.emailHandlerConfig, this.service.logger);
}
catch (err) {
this.service.logger.error(`Failed to create emailHandler for config: ${JSON.stringify(this.config.emailHandlerConfig)}. Error: ${err}`);
}
}
});
}
stop() {
return __awaiter(this, void 0, void 0, function* () {
if (this.passwordRealm) {
this.passwordRealm.close();
}
});
}
authenticateOrCreateUser(body) {
return __awaiter(this, void 0, void 0, function* () {
let username;
let password;
let register;
if (body["user_info"]) {
username = body["data"];
password = body["user_info"]["password"];
register = body["user_info"]["register"];
}
else {
username = body["username"];
password = body["password"];
register = body["register"];
}
if (!username) {
throw new errors.realm.MissingParameters("username");
}
if (password === undefined || password === null) {
throw new errors.realm.MissingParameters("password");
}
if (register === undefined) {
const foundUser = this.service.getUserByProviderId("password", username);
register = !foundUser;
}
if (register) {
return this.attemptToRegister(username, password, false);
}
return this.attemptToLogin(username, password);
});
}
update(user, data) {
return __awaiter(this, void 0, void 0, function* () {
const newPassword = data.new_password;
if (!newPassword) {
throw new errors.realm.MissingParameters("new_password");
}
yield this.updatePassword(newPassword, user.userId);
return {};
});
}
updateProviderAccount(providerId, user, data, isAuthenticated, userAgent, remoteIp) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.emailHandler) {
throw new errors.realm.NotSupported({
detail: "The Password provider is not configured with an emailHandler."
});
}
switch (data.action) {
case "reset_password":
if (!this.emailHandler.resetPassword) {
throw new errors.realm.NotSupported({
detail: "The Password provider's emailHandler doesn't implement resetPassword."
});
}
const token = crypto.randomBytes(32).toString("hex");
if (user) {
yield shared_1.writeAsync(this.passwordRealm, () => {
this.passwordRealm.create("PasswordResetRequest", {
token,
expires: moment().add(12, "hour").toDate(),
userId: user.userId
});
});
this.emailHandler.resetPassword(providerId, token, userAgent, remoteIp)
.catch((err) => {
this.service.logger.error("Could not send reset email:", err);
});
}
return {};
case "complete_reset":
if (!this.emailHandler.resetPassword) {
throw new errors.realm.NotSupported({
detail: "The Password provider's emailHandler doesn't implement resetPassword."
});
}
const missingParams = [];
if (data.new_password === undefined || data.new_password === null) {
missingParams.push("new_password");
}
if (!data.token) {
missingParams.push("token");
}
if (missingParams.length > 0) {
throw new errors.realm.MissingParameters(...missingParams);
}
const existing = this.passwordRealm.objectForPrimaryKey("PasswordResetRequest", data.token);
if (!existing || existing.consumed || existing.expires < new Date()) {
throw new errors.realm.AccessDenied({
detail: "The password reset token is invalid or has expired."
});
}
yield shared_1.writeAsync(this.passwordRealm, () => {
existing.consumed = new Date();
});
yield this.updatePassword(data.new_password, existing.userId);
return {};
case "request_email_confirmation":
if (!this.emailHandler.confirmEmail) {
throw new errors.realm.NotSupported({
detail: "The Password provider's emailHandler doesn't implement confirmEmail."
});
}
if (user) {
yield this.requestEmailConfirmation(user, providerId);
}
return {};
case "confirm_email":
if (!this.emailHandler.confirmEmail) {
throw new errors.realm.NotSupported({
detail: "The Password provider's emailHandler doesn't implement confirmEmail."
});
}
if (!data.token) {
throw new errors.realm.MissingParameters("token");
}
const confirmationRequest = this.passwordRealm.objectForPrimaryKey("EmailConfirmationRequest", data.token);
if (!confirmationRequest || confirmationRequest.consumed || confirmationRequest.expires < new Date()) {
throw new errors.realm.AccessDenied({
detail: "The email confirmation token is invalid or has expired."
});
}
const passwordHash = this.passwordRealm.objectForPrimaryKey("PasswordSaltHash", confirmationRequest.userId);
yield shared_1.writeAsync(this.passwordRealm, () => {
passwordHash.isEmailConfirmed = true;
confirmationRequest.consumed = new Date();
});
yield this.service.updateUserMetadata(confirmationRequest.userId, {
isEmailConfirmed: "true"
});
return {};
default:
throw new errors.realm.InvalidParameters({
name: "action",
reason: `The action ${data.action} is not supported.`
});
}
});
}
deleteUser(userId) {
return __awaiter(this, void 0, void 0, function* () {
let userDeleted = false;
yield shared_1.writeAsync(this.passwordRealm, () => {
const foundPassword = this.passwordRealm.objectForPrimaryKey("PasswordSaltHash", userId);
if (foundPassword) {
userDeleted = true;
this.passwordRealm.delete(foundPassword);
}
});
return userDeleted;
});
}
enhanceLog(body) {
let username;
let register;
const userInfo = body.user_info;
if (userInfo) {
username = body.data;
register = userInfo.register;
}
else {
username = body.username;
register = body.register;
}
return `username: '${username}', register: ${register}`;
}
updatePassword(newPassword, userId) {
return __awaiter(this, void 0, void 0, function* () {
const salt = yield this.createSalt(this.saltLength);
const hash = yield this.hashPassword(newPassword, salt, this.iterations, this.keyLength, this.digest);
yield shared_1.writeAsync(this.passwordRealm, () => {
const existingHash = this.passwordRealm.objectForPrimaryKey("PasswordSaltHash", userId);
this.passwordRealm.create("PasswordSaltHash", {
salt: salt.toString("base64"),
iterations: this.iterations,
keyLength: this.keyLength,
digest: this.digest,
hash: hash.toString("base64"),
userId: userId,
isEmailConfirmed: (existingHash && existingHash.isEmailConfirmed) || false
}, true);
});
});
}
requestEmailConfirmation(user, email) {
return __awaiter(this, void 0, void 0, function* () {
const token = crypto.randomBytes(32).toString("hex");
yield shared_1.writeAsync(this.passwordRealm, () => {
this.passwordRealm.create("EmailConfirmationRequest", {
token,
expires: moment().add(1, "month").toDate(),
userId: user.userId,
email
});
});
this.emailHandler.confirmEmail(email, token)
.catch((err) => {
this.service.logger.error("Could not send confirmation email:", err);
});
});
}
hashPassword(clearTextPassword, salt, iterations, keyLength, digest) {
return __awaiter(this, void 0, void 0, function* () {
const { result, time } = yield shared_1.measureTime(() => shared_1.Promisify(crypto.pbkdf2.bind(crypto), clearTextPassword, salt, iterations, keyLength, digest));
this.hashBuffer.enq(time);
return result;
});
}
createSalt(saltLength) {
return __awaiter(this, void 0, void 0, function* () {
return shared_1.Promisify(crypto.randomBytes.bind(crypto), saltLength);
});
}
comparePassword(candidatePassword, salt, iterations, keyLength, digest, hash) {
return __awaiter(this, void 0, void 0, function* () {
const hashedCandidatePassword = yield this.hashPassword(candidatePassword, salt, iterations, keyLength, digest);
return hashedCandidatePassword.equals(hash);
});
}
attemptToRegister(username, password, isAdmin) {
return __awaiter(this, void 0, void 0, function* () {
const foundUser = this.service.getUserByProviderId(this.name, username);
if (foundUser) {
this.service.logger.detail(`[PasswordAuth] Register called, but the user already exists: '${username}'`);
throw new errors.realm.InvalidCredentials();
}
const user = yield this.service.createOrUpdateUser(username, this.name, isAdmin);
yield this.updatePassword(password, user.userId);
if (this.emailHandler && this.emailHandler.confirmEmail) {
yield this.requestEmailConfirmation(user, username);
}
return user;
});
}
attemptToLogin(username, password) {
return __awaiter(this, void 0, void 0, function* () {
const foundUser = this.service.getUserByProviderId(this.name, username);
if (!foundUser) {
yield this.fakeHash(password);
this.service.logger.detail(`[PasswordAuth] Login called, but the user doesn't exists: '${username}'`);
throw new errors.realm.InvalidCredentials();
}
const foundPassword = this.passwordRealm.objectForPrimaryKey("PasswordSaltHash", foundUser.userId);
if (!foundPassword) {
yield this.fakeHash(password);
this.service.logger.detail(`[PasswordAuth] Login called, but the password doesn't exist: '${username}'`);
throw new errors.realm.InvalidCredentials();
}
const passwordMatch = yield this.comparePassword(password, Buffer.from(foundPassword.salt, "base64"), foundPassword.iterations, foundPassword.keyLength, foundPassword.digest, Buffer.from(foundPassword.hash, "base64"));
if (!passwordMatch) {
this.service.logger.detail(`[PasswordAuth] Login called, but the password doesn't match: '${username}'`);
throw new errors.realm.InvalidCredentials();
}
if (foundPassword.isEmailConfirmed !== foundUser.isEmailConfirmed()) {
yield this.service.updateUserMetadata(foundUser, {
isEmailConfirmed: foundPassword.isEmailConfirmed ? "true" : "false"
});
}
return foundUser;
});
}
fakeHash(password) {
return __awaiter(this, void 0, void 0, function* () {
const timings = this.hashBuffer.toarray();
if (timings.length === 0) {
const salt = yield this.createSalt(this.saltLength);
yield this.hashPassword(password, salt, this.iterations, this.keyLength, this.digest);
}
else {
const toDelay = shared_1.gaussianRandom(timings);
yield shared_1.delay(toDelay);
}
});
}
}
exports.PasswordAuthProvider = PasswordAuthProvider;
class BasicEmailHandler {
constructor(config, logger) {
this.transporter = nodemailer.createTransport(config.connectionString);
this.from = config.from;
this.logger = logger;
this.resetActionConfig = config.resetActionConfig || {
subject: BasicEmailHandler.defaultResetSubject,
textTemplate: BasicEmailHandler.defaultResetTextTemplate,
htmlTemplate: BasicEmailHandler.defaultResetHtmlTemplate
};
this.confirmActionConfig = config.confirmActionConfig || {
subject: BasicEmailHandler.defaultConfirmSubject,
textTemplate: BasicEmailHandler.defaultConfirmTextTemplate,
htmlTemplate: BasicEmailHandler.defaultConfirmHtmlTemplate
};
this.transporter.verify().catch((err) => {
this.logger.error(`Email configuration for the Password provider is invalid: ${err}`);
});
const trimRegex = /\/+$/g;
this.baseUrl = config.baseUrl && config.baseUrl.replace(trimRegex, "");
this.validate();
}
resetPassword(email, token, userAgent, remoteIp) {
return __awaiter(this, void 0, void 0, function* () {
const templateVars = { TOKEN: token, IP: remoteIp, BASE_URL: this.baseUrl || "[missing baseUrl]" };
const message = {
from: this.from,
to: email,
subject: this.resetActionConfig.subject,
text: this.renderEmail(this.resetActionConfig.textTemplate, templateVars),
html: this.renderEmail(this.resetActionConfig.htmlTemplate, templateVars),
};
const response = yield this.transporter.sendMail(message);
this.logger.detail(`Password reset email sent for ${email}: ${JSON.stringify(response)}`);
});
}
confirmEmail(email, token) {
return __awaiter(this, void 0, void 0, function* () {
const templateVars = { TOKEN: token, BASE_URL: this.baseUrl || "[missing baseUrl]" };
const message = {
from: this.from,
to: email,
subject: this.confirmActionConfig.subject,
text: this.renderEmail(this.confirmActionConfig.textTemplate, templateVars),
html: this.renderEmail(this.confirmActionConfig.htmlTemplate, templateVars),
};
const response = yield this.transporter.sendMail(message);
this.logger.detail(`Confirm email sent for ${email}: ${JSON.stringify(response)}`);
});
}
renderEmail(template, vars) {
let result = template;
for (const key in vars) {
const pattern = new RegExp(`%${key}%`, "g");
result = result.replace(pattern, vars[key]);
}
return result;
}
validate() {
if (!this.baseUrl) {
const key = "%BASE_URL%";
assert(this.resetActionConfig.textTemplate.indexOf(key) === -1, `Reset password text template uses ${key} but baseUrl is not configured`);
assert(this.resetActionConfig.htmlTemplate.indexOf(key) === -1, `Reset password html template uses ${key} but baseUrl is not configured`);
assert(this.confirmActionConfig.textTemplate.indexOf(key) === -1, `Confirm email text template uses ${key} but baseUrl is not configured`);
assert(this.confirmActionConfig.htmlTemplate.indexOf(key) === -1, `Confirm email html template uses ${key} but baseUrl is not configured`);
}
}
}
BasicEmailHandler.defaultResetSubject = "Password reset requested";
BasicEmailHandler.defaultResetTextTemplate = emails_1.emails.resetPassword.text;
BasicEmailHandler.defaultResetHtmlTemplate = emails_1.emails.resetPassword.html;
BasicEmailHandler.defaultConfirmSubject = "Confirm your email";
BasicEmailHandler.defaultConfirmTextTemplate = emails_1.emails.confirmEmail.text;
BasicEmailHandler.defaultConfirmHtmlTemplate = emails_1.emails.confirmEmail.html;
//# sourceMappingURL=PasswordAuthProvider.js.map