smc-hub
Version:
CoCalc: Backend webserver component
463 lines (445 loc) • 14.7 kB
JavaScript
// 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> </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> </div>
<div style="text-align: center; font-size: 120%;">
<b><a href="${RESET_URL}">${RESET_URL}</a></b>
</div>
<div> </div>
<div>If you don't want to change your password, ignore this message.</div>
<div> </div>
<div>In case of problems, email
<a href="mailto:${HELP_EMAIL}">${HELP_EMAIL}</a> immediately!
<div> </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