@cocalc/hub
Version:
CoCalc: Backend webserver component
437 lines (414 loc) • 14 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
//########################################################################
/*
User sign in
Throttling policy: It basically like this, except we reset the counters
each minute and hour, so a crafty attacker could get twice as many tries by finding the
reset interval and hitting us right before and after. This is an acceptable tradeoff
for making the data structure trivial.
* POLICY 1: A given email address is allowed at most 3 failed login attempts per minute.
* POLICY 2: A given email address is allowed at most 30 failed login attempts per hour.
* POLICY 3: A given ip address is allowed at most 10 failed login attempts per minute.
* POLICY 4: A given ip address is allowed at most 50 failed login attempts per hour.
*/
var Bottleneck, _sign_in, _sign_in_using_auth_token, apiKeyAction, async, auth, bottleneck_opts, defaults, limit_group, message, misc, process_env_int, record_sign_in_fail, required, sign_in_check, throttle;
async = require('async');
message = require('@cocalc/util/message');
misc = require('@cocalc/util/misc');
({required, defaults} = misc);
auth = require('./auth');
({process_env_int} = require("@cocalc/backend/misc"));
throttle = require("@cocalc/server/auth/throttle");
Bottleneck = require("bottleneck");
apiKeyAction = require("@cocalc/server/api/manage").default;
bottleneck_opts = {
minTime: process_env_int('THROTTLE_SIGN_IN_MINTIME', 100),
maxConcurrent: process_env_int('THROTTLE_SIGN_IN_CONCURRENT', 10)
};
limit_group = new Bottleneck.Group(bottleneck_opts);
record_sign_in_fail = function(opts) {
var email, ip, logger;
({email, ip, logger} = defaults(opts, {
email: required,
ip: required,
logger: void 0
}));
throttle.recordFail(email, ip);
return typeof logger === "function" ? logger(`WARNING: record_sign_in_fail(${email}, ${ip})`) : void 0;
};
sign_in_check = function(opts) {
var auth_token, email, ip;
({email, ip, auth_token} = defaults(opts, {
email: required,
ip: required,
auth_token: void 0
}));
return throttle.signInCheck(email, ip, auth_token);
};
exports.sign_in = function(opts) {
var done_cb, key;
opts = defaults(opts, {
client: required,
mesg: required,
logger: void 0,
database: required,
host: void 0,
port: void 0,
cb: void 0
});
key = opts.client.ip_address;
done_cb = function() {
var ref;
return (ref = opts.logger) != null ? ref.debug(`sign_in(group='${key}'): done`) : void 0;
};
return limit_group.key(key).submit(_sign_in, opts, done_cb);
};
_sign_in = async function(opts, done) {
var account, client, dbg, m, mesg, sign_in_error, signed_in_mesg, tm;
({client, mesg} = opts);
if (opts.logger != null) {
dbg = function(m) {
return opts.logger.debug(`_sign_in(${mesg.email_address}): ${m}`);
};
dbg();
} else {
dbg = function() {};
}
tm = misc.walltime();
sign_in_error = function(error) {
dbg(`sign_in_error -- ${error}`);
exports.record_sign_in({
database: opts.database,
ip_address: client.ip_address,
successful: false,
email_address: mesg.email_address,
account_id: typeof account !== "undefined" && account !== null ? account.account_id : void 0
});
client.push_to_client(message.sign_in_failed({
id: mesg.id,
email_address: mesg.email_address,
reason: error
}));
return typeof opts.cb === "function" ? opts.cb(error) : void 0;
};
if (!mesg.email_address) {
sign_in_error("Empty email address.");
return;
}
if (!mesg.password) {
sign_in_error("Empty password.");
return;
}
mesg.email_address = misc.lower_email_address(mesg.email_address);
m = (await sign_in_check({
email: mesg.email_address,
ip: client.ip_address,
auth_token: false
}));
if (m) {
sign_in_error(`sign_in_check failure: ${m}`);
return;
}
signed_in_mesg = void 0;
account = void 0;
return async.series([
function(cb) {
dbg("get account and check credentials");
// NOTE: Despite people complaining, we do give away info about whether
// the e-mail address is for a valid user or not.
// There is no security in not doing this, since the same information
// can be determined via the invite collaborators feature.
return opts.database.get_account({
email_address: mesg.email_address,
columns: ['password_hash',
'account_id',
'passports',
'banned'],
cb: function(err,
_account) {
if (err) {
cb(err);
return;
}
account = _account;
dbg(`account = ${JSON.stringify(account)}`);
if (account.banned) {
dbg("banned account!");
cb("This account is BANNED. Contact help@cocalc.com if you believe this is a mistake.");
return;
}
return cb();
}
});
},
function(cb) {
dbg("got account; now checking if password is correct...");
return auth.is_password_correct({
database: opts.database,
account_id: account.account_id,
password: mesg.password,
password_hash: account.password_hash,
cb: function(err,
is_correct) {
if (err) {
cb(`Error checking correctness of password -- ${err}`);
return;
}
if (!is_correct) {
if (!account.password_hash) {
return cb(`The account ${mesg.email_address} exists but doesn't have a password. Either set your password by clicking 'Forgot Password?' or log in using ${misc.keys(account.passports).join(', ')}. If that doesn't work, email help@cocalc.com and we will sort this out.`);
} else {
return cb(`Incorrect password for ${mesg.email_address}. You can reset your password by clicking the 'Forgot Password?' link. If that doesn't work, email help@cocalc.com and we will sort this out.`);
}
} else {
return cb();
}
}
});
},
// remember me
function(cb) {
if (!mesg.remember_me) {
// do not set cookie if already set (and message known)
cb();
return;
}
dbg("remember_me -- setting the remember_me cookie");
signed_in_mesg = message.signed_in({
id: mesg.id,
account_id: account.account_id,
email_address: mesg.email_address,
remember_me: false,
hub: opts.host + ':' + opts.port
});
return client.remember_me({
account_id: signed_in_mesg.account_id,
cb: cb
});
},
async function(cb) {
var err;
if (!mesg.get_api_key) {
cb();
return;
}
dbg("get_api_key -- also get_api_key");
try {
signed_in_mesg.api_key = (await apiKeyAction({
account_id: account.account_id,
password: mesg.password,
action: 'get'
}));
return cb();
} catch (error1) {
err = error1;
return cb(err);
}
},
async function(cb) {
var err;
if (!mesg.get_api_key || signed_in_mesg.api_key) {
cb();
return;
}
dbg("get_api_key -- must generate key since don't already have it");
try {
signed_in_mesg.api_key = (await apiKeyAction({
account_id: account.account_id,
password: mesg.password,
action: 'regenerate'
}));
return cb();
} catch (error1) {
err = error1;
return cb(err);
}
}
], function(err) {
if (err) {
dbg(`send error to user (in ${misc.walltime(tm)}seconds) -- ${err}`);
sign_in_error(err);
if (typeof opts.cb === "function") {
opts.cb(err);
}
} else {
dbg(`user got signed in fine (in ${misc.walltime(tm)}seconds) -- sending them a message`);
client.signed_in(signed_in_mesg);
client.push_to_client(signed_in_mesg);
if (typeof opts.cb === "function") {
opts.cb();
}
}
// final callback for bottleneck group
return done();
});
};
exports.sign_in_using_auth_token = function(opts) {
var done_cb, key;
opts = defaults(opts, {
client: required,
mesg: required,
logger: void 0,
database: required,
host: void 0,
port: void 0,
cb: void 0
});
key = opts.client.ip_address;
done_cb = function() {
var ref;
return (ref = opts.logger) != null ? ref.debug(`sign_in_using_auth_token(group='${key}'): done`) : void 0;
};
return limit_group.key(key).submit(_sign_in_using_auth_token, opts, done_cb);
};
_sign_in_using_auth_token = async function(opts, done) {
var account, account_id, client, dbg, m, mesg, ref, sign_in_error, signed_in_mesg, tm;
({client, mesg} = opts);
if (opts.logger != null) {
dbg = function(m) {
return opts.logger.debug(`_sign_in_using_auth_token(${mesg.email_address}): ${m}`);
};
dbg();
} else {
dbg = function() {};
}
tm = misc.walltime();
sign_in_error = function(error) {
dbg(`sign_in_using_auth_token_error -- ${error}`);
exports.record_sign_in({
database: opts.database,
ip_address: client.ip_address,
successful: false,
email_address: mesg.auth_token, // yes, we abuse the email_address field
account_id: typeof account !== "undefined" && account !== null ? account.account_id : void 0
});
client.push_to_client(message.error({
id: mesg.id,
error: error
}));
return typeof opts.cb === "function" ? opts.cb(error) : void 0;
};
if (!mesg.auth_token) {
sign_in_error("missing auth_token.");
return;
}
if (((ref = mesg.auth_token) != null ? ref.length : void 0) !== 24) {
sign_in_error("auth_token must be exactly 24 characters long");
return;
}
m = (await sign_in_check({
email: mesg.auth_token,
ip: client.ip_address,
auth_token: true
}));
if (m) {
sign_in_error(`sign_in_check fail(ip=${client.ip_address}): ${m}`);
return;
}
signed_in_mesg = void 0;
account = account_id = void 0;
return async.series([
function(cb) {
dbg("get account and check credentials");
return opts.database.get_auth_token_account_id({
auth_token: mesg.auth_token,
cb: function(err,
_account_id) {
if (!err && !_account_id) {
err = 'auth_token is not valid';
}
account_id = _account_id;
return cb(err);
}
});
},
function(cb) {
dbg("successly got account_id; now getting more information about the account");
return opts.database.get_account({
account_id: account_id,
columns: ['email_address',
'lti_id'],
cb: function(err,
_account) {
if (!!_account.lti_id) {
if (_account.email_address == null) {
_account.email_address = '';
}
}
account = _account;
return cb(err);
}
});
},
// remember me
function(cb) {
dbg("remember_me -- setting the remember_me cookie");
signed_in_mesg = message.signed_in({
id: mesg.id,
account_id: account_id,
email_address: account.email_address,
lti_id: account.lti_id,
remember_me: false,
hub: opts.host + ':' + opts.port
});
return client.remember_me({
account_id: signed_in_mesg.account_id,
lti_id: signed_in_mesg.lti_id,
ttl: 12 * 3600,
cb: cb
});
}
], function(err) {
if (err) {
dbg(`send error to user (in ${misc.walltime(tm)}seconds) -- ${err}`);
sign_in_error(err);
if (typeof opts.cb === "function") {
opts.cb(err);
}
} else {
dbg(`user got signed in fine (in ${misc.walltime(tm)}seconds) -- sending them a message`);
client.signed_in(signed_in_mesg);
client.push_to_client(signed_in_mesg);
if (typeof opts.cb === "function") {
opts.cb();
}
}
// final callback for bottleneck group
return done();
});
};
// Record to the database a failed and/or successful login attempt.
exports.record_sign_in = function(opts) {
var data, ref;
opts = defaults(opts, {
ip_address: required,
successful: required,
database: required,
email_address: void 0,
account_id: void 0,
remember_me: false
});
if (!opts.successful) {
return record_sign_in_fail({
email: opts.email_address,
ip: opts.ip_address
});
} else {
data = {
ip_address: opts.ip_address,
email_address: (ref = opts.email_address) != null ? ref : null,
remember_me: opts.remember_me,
account_id: opts.account_id
};
return opts.database.log({
event: 'successful_sign_in',
value: data
});
}
};
}).call(this);
//# sourceMappingURL=sign-in.js.map