UNPKG

realm-object-server

Version:

Realm Object Server

424 lines 20.7 kB
"use strict"; 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