UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

463 lines (445 loc) 14.7 kB
// Generated by CoffeeScript 2.5.1 (function() { //######################################################################## // This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. // License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details //######################################################################## /* Password reset and change functionality. */ var PW_RESET_ENDPOINT, PW_RESET_KEY, async, auth, base_path, defaults, email, is_valid_password, message, misc, required; async = require('async'); misc = require('smc-util/misc'); message = require('smc-util/message'); // message protocol between front-end and back-end email = require('./email'); ({defaults, required} = misc); ({is_valid_password} = require('./client/create-account')); auth = require('./auth'); base_path = require('smc-util-node/base-path').default; exports.PW_RESET_ENDPOINT = PW_RESET_ENDPOINT = '/auth/password_reset'; exports.PW_RESET_KEY = PW_RESET_KEY = 'token'; exports.forgot_password = function(opts) { var id, locals; opts = defaults(opts, { mesg: required, database: required, ip_address: required, cb: required }); /* Send an email message to the given email address with a code that can be used to reset the password for a certain account. Anti-spam/DOS throttling policies: * a given email address can be sent at most 30 password resets per hour * a given ip address can send at most 100 password reset request per minute * a given ip can send at most 250 per hour */ if (opts.mesg.event !== 'forgot_password') { opts.cb(`Incorrect message event type: ${opts.mesg.event}`); return; } // This is an easy check to save work and also avoid empty email_address, which causes trouble below if (!misc.is_valid_email_address(opts.mesg.email_address)) { opts.cb("Invalid email address."); return; } opts.mesg.email_address = misc.lower_email_address(opts.mesg.email_address); id = null; locals = { settings: void 0 }; return async.series([ function(cb) { // Record this password reset attempt in our database return opts.database.record_password_reset_attempt({ email_address: opts.mesg.email_address, ip_address: opts.ip_address, cb: cb }); }, function(cb) { // POLICY 1: We limit the number of password resets that an email address can receive return opts.database.count_password_reset_attempts({ email_address: opts.mesg.email_address, age_s: 60 * 60, // 1 hour cb: function(err, count) { if (err) { return cb(err); } else if (count >= 31) { return cb("Too many password resets for this email per hour; try again later."); } else { return cb(); } } }); }, function(cb) { // POLICY 2: a given ip address can send at most 10 password reset requests per minute return opts.database.count_password_reset_attempts({ ip_address: opts.ip_address, age_s: 60, // 1 minute cb: function(err, count) { if (err) { return cb(err); } else if (count > 10) { return cb("Too many password resets per minute; try again later."); } else { return cb(); } } }); }, function(cb) { // POLICY 3: a given ip can send at most 60 per hour return opts.database.count_password_reset_attempts({ ip_address: opts.ip_address, age_s: 60 * 60, // 1 hour cb: function(err, count) { if (err) { return cb(err); } else if (count > 60) { return cb("Too many password resets per hour; try again later."); } else { return cb(); } } }); }, function(cb) { return opts.database.account_exists({ email_address: opts.mesg.email_address, cb: function(err, exists) { if (err) { return cb(err); } else if (!exists) { return cb(`No account with e-mail address ${opts.mesg.email_address}`); } else { return cb(); } } }); }, function(cb) { // We now know that there is an account with this email address. // put entry in the password_reset uuid:value table with ttl of // 1 hour, and send an email return opts.database.set_password_reset({ email_address: opts.mesg.email_address, ttl: 60 * 60, cb: function(err, _id) { id = _id; return cb(err); } }); }, (cb) => { return opts.database.get_server_settings_cached({ cb: (err, settings) => { if (err) { return cb(err); } else { locals.settings = settings; return cb(); } } }); }, function(cb) { var DOMAIN_URL, HELP_EMAIL, RESET_URL, SITE_NAME, body, dns, path, ref, ref1, theme; // send an email to opts.mesg.email_address that has a password reset link theme = require('smc-util/theme'); dns = locals.settings.dns || theme.DNS; DOMAIN_URL = `https://${dns}`; HELP_EMAIL = (ref = locals.settings.help_email) != null ? ref : theme.HELP_EMAIL; SITE_NAME = (ref1 = locals.settings.site_name) != null ? ref1 : theme.SITE_NAME; path = require('path').join(base_path, PW_RESET_ENDPOINT); RESET_URL = `${DOMAIN_URL}${path}?${PW_RESET_KEY}=${id}`; body = `<div>Hello,</div> <div>&nbsp;</div> <div> Somebody just requested to change the password of your ${SITE_NAME} account. If you requested this password change, please click this link:</div> <div>&nbsp;</div> <div style="text-align: center; font-size: 120%;"> <b><a href="${RESET_URL}">${RESET_URL}</a></b> </div> <div>&nbsp;</div> <div>If you don't want to change your password, ignore this message.</div> <div>&nbsp;</div> <div>In case of problems, email <a href="mailto:${HELP_EMAIL}">${HELP_EMAIL}</a> immediately! <div>&nbsp;</div>`; return email.send_email({ subject: `${SITE_NAME} Password Reset`, body: body, from: `CoCalc Help <${HELP_EMAIL}>`, to: opts.mesg.email_address, category: "password_reset", settings: locals.settings, cb: cb }); } ], opts.cb); }; exports.reset_forgot_password = function(opts) { var account_id, db, email_address; opts = defaults(opts, { mesg: required, database: required, cb: required }); if (opts.mesg.event !== 'reset_forgot_password') { opts.cb(`incorrect message event type: ${opts.mesg.event}`); return; } email_address = account_id = db = null; return async.series([ function(cb) { var reason, valid; // Verify password is valid and compute its hash. [valid, reason] = is_valid_password(opts.mesg.new_password); if (!valid) { cb(reason); return; } // Check that request is still valid return opts.database.get_password_reset({ id: opts.mesg.reset_code, cb: function(err, x) { if (err) { return cb(err); } else if (!x) { return cb("Password reset request is no longer valid."); } else { email_address = x; return cb(); } } }); }, function(cb) { // Get the account_id. return opts.database.get_account({ email_address: email_address, columns: ['account_id'], cb: function(err, account) { account_id = account != null ? account.account_id : void 0; return cb(err); } }); }, function(cb) { // Make the change return opts.database.change_password({ account_id: account_id, password_hash: auth.password_hash(opts.mesg.new_password), cb: function(err, account) { if (err) { return cb(err); } else { // only allow successful use of this reset token once return opts.database.delete_password_reset({ id: opts.mesg.reset_code, cb: cb }); } } }); } ], opts.cb); }; exports.change_password = function(opts) { var account; opts = defaults(opts, { mesg: required, account_id: required, // user they are auth'd as database: required, ip_address: required, cb: required }); account = null; return async.series([ function(cb) { // get account and validate the password (if they have one) return opts.database.get_account({ account_id: opts.account_id, columns: ['password_hash'], cb: function(error, result) { if (error) { cb({ other: error }); return; } account = result; return auth.is_password_correct({ database: opts.database, account_id: opts.account_id, password: opts.mesg.old_password, password_hash: account.password_hash, allow_empty_password: true, cb: function(err, is_correct) { if (err) { return cb(err); } else { if (!is_correct) { err = "invalid old password"; opts.database.log({ event: 'change_password', value: { email_address: opts.mesg.email_address, client_ip_address: opts.ip_address, message: err } }); return cb(err); } else { return cb(); } } } }); } }); }, function(cb) { var reason, valid; // check that new password is valid [valid, reason] = is_valid_password(opts.mesg.new_password); if (!valid) { return cb({ new_password: reason }); } else { return cb(); } }, function(cb) { // record current password hash (just in case?) and that we // are changing password and set new password opts.database.log({ event: "change_password", value: { account_id: opts.account_id, client_ip_address: opts.ip_address, previous_password_hash: account.password_hash } }); return opts.database.change_password({ account_id: opts.account_id, password_hash: auth.password_hash(opts.mesg.new_password), cb: cb }); } ], opts.cb); }; exports.change_email_address = function(opts) { var dbg; opts = defaults(opts, { mesg: required, database: required, account_id: required, ip_address: required, logger: void 0, cb: required }); if (opts.logger != null) { dbg = function(...m) { var ref; return (ref = opts.logger) != null ? ref.debug(`change_email_address(${opts.mesg.account_id}): `, ...m) : void 0; }; dbg(); } else { dbg = function() {}; } opts.mesg.new_email_address = misc.lower_email_address(opts.mesg.new_email_address); if (!misc.is_valid_email_address(opts.mesg.new_email_address)) { dbg("invalid email address"); opts.cb('email_invalid'); return; } if (opts.mesg.account_id !== opts.account_id) { opts.cb("account_id in mesg is not what user is signed in as"); return; } return async.series([ function(cb) { return auth.is_password_correct({ database: opts.database, account_id: opts.mesg.account_id, password: opts.mesg.password, allow_empty_password: true, // in case account created using a linked passport only cb: function(err, is_correct) { if (err) { return cb(`Error checking password -- please try again in a minute -- ${err}.`); } else if (!is_correct) { return cb("invalid_password"); } else { return cb(); } } }); }, function(cb) { // Record current email address (just in case?) and that we are // changing email address to the new one. This will make it // easy to implement a "change your email address back" feature // if I need to at some point. dbg("log change to db"); opts.database.log({ event: 'change_email_address', value: { client_ip_address: opts.ip_address, new_email_address: opts.mesg.new_email_address } }); dbg("actually make change in db"); return opts.database.change_email_address({ account_id: opts.mesg.account_id, email_address: opts.mesg.new_email_address, cb: cb }); }, function(cb) { // If they just changed email to an address that has some actions, carry those out... // TODO: move to hook this only after validation of the email address? // TODO: NO -- instead this should get completely removed and these actions // should be replaced by special URL's (e.g., a URL that when visited // makes it so you get added to a project, or a code you enter on the page). // That would be way more secure *and* flexible. return opts.database.do_account_creation_actions({ email_address: opts.mesg.new_email_address, account_id: opts.mesg.account_id, cb: cb }); } ], opts.cb); }; }).call(this); //# sourceMappingURL=password.js.map