diff --git a/lib/consts.js b/lib/consts.js index 7db1f8cf..fdb034c5 100644 --- a/lib/consts.js +++ b/lib/consts.js @@ -38,9 +38,14 @@ module.exports = { BCRYPT_ROUNDS: 12, // how many authentication failures per user to allow before blocking until the end of the auth window - AUTH_FAILURES: 6, + USER_AUTH_FAILURES: 12, // authentication window in seconds, starts counting from first invalid authentication - AUTH_WINDOW: 60, + USER_AUTH_WINDOW: 120, + + // how many authentication failures per ip to allow before blocking until the end of the auth window + IP_AUTH_FAILURES: 10, + // authentication window in seconds, starts counting from first invalid authentication + IP_AUTH_WINDOW: 300, // how many TOTP failures per user to allow before blocking until the end of the auth window TOTP_FAILURES: 6, diff --git a/lib/user-handler.js b/lib/user-handler.js index 35e36f4a..ecc47c63 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -242,6 +242,68 @@ class UserHandler { }); } + /** + * rateLimitIP + * if ip is not available will always return success object + * @param {Object} meta + * @param {String} meta.ip request remote ip address + * @param {Integer} count + * @param {Function} callback + */ + rateLimitIP (meta, count, callback) { + if (meta.ip) { + this.counters.ttlcounter('auth_ip:' + meta.ip, count, consts.IP_AUTH_FAILURES, consts.IP_AUTH_WINDOW, callback); + } + return callback(null, {success: true}); + } + + /** + * rateLimitUser + * @param {String} tokenID user identifier + * @param {Integer} count + * @param {Function} callback + */ + rateLimitUser (tokenID, count, callback) { + this.counters.ttlcounter('auth_user:' + tokenID, count, consts.USER_AUTH_FAILURES, consts.USER_AUTH_WINDOW, callback); + } + + /** + * rateLimitReleaseUser + * @param {String} tokenID user identifier + * @param {Integer} count + * @param {Function} callback + */ + rateLimitReleaseUser (tokenID, callback) { + this.redis.del('auth_user:' + tokenID, callback); + } + + /** + * rateLimit + * @param {String} tokenID user identifier + * @param {Object} meta + * @param {String} meta.ip request remote ip address + * @param {Integer} count + * @param {Function} callback + */ + rateLimit (tokenID, meta, count, callback) { + this.rateLimitIP(meta, count, (err, ipRes) => { + if (err) { + return callback(err); + } + + this.rateLimitUser(tokenID, count, (err, userRes) => { + if (err) { + return callback(err); + } + if (!ipRes.success) { + return callback(null, ipRes); + } + return callback(null, userRes); + }); + }); + } + + /** * Authenticate user * @@ -261,6 +323,16 @@ class UserHandler { return callback(null, false); } + this.rateLimitIP(meta, 0, (err, res) => { + if (err) { + err.code = 'InternalDatabaseError'; + return callback(err); + } + + if (!res.success) { + return rateLimitResponse(res, callback); + } + this.checkAddress(username, (err, query) => { if (err) { return callback(err); @@ -301,8 +373,8 @@ class UserHandler { return this.logAuthEvent(null, meta, () => { // rate limit failed authentication attempts against non-existent users as well let ustring = (query.unameview || query._id || '').toString(); - let rlkey = 'auth:' + ustring + (meta.ip ? ':' + meta.ip : ''); - this.counters.ttlcounter(rlkey, 1, consts.AUTH_FAILURES, consts.AUTH_WINDOW, (err, res) => { + + this.rateLimit(ustring, meta, 1, (err, res) => { if (err) { err.code = 'InternalDatabaseError'; return callback(err); @@ -315,8 +387,7 @@ class UserHandler { }); } - let rlkey = 'auth:' + userData._id.toString() + (meta.ip ? ':' + meta.ip : ''); - this.counters.ttlcounter(rlkey, 0, consts.AUTH_FAILURES, consts.AUTH_WINDOW, (err, res) => { + this.rateLimitUser(userData._id, 0, (err, res) => { if (err) { err.code = 'InternalDatabaseError'; return callback(err); @@ -342,13 +413,13 @@ class UserHandler { let authSuccess = (...args) => { // clear rate limit counter on success - this.redis.del(rlkey, () => false); + this.rateLimitReleaseUser(userData._id, () => false); callback(...args); }; let authFail = (...args) => { // increment rate limit counter on failure - this.counters.ttlcounter(rlkey, 1, consts.AUTH_FAILURES, consts.AUTH_WINDOW, () => { + this.rateLimit(userData._id, meta, 1, () => { callback(...args); }); }; @@ -514,7 +585,7 @@ class UserHandler { if (err) { // don't really care } - this.redis.del(rlkey, () => false); + this.rateLimitReleaseUser(userData._id, () => false); this.users.collection('asps').findOneAndUpdate( { _id: asp._id @@ -546,6 +617,8 @@ class UserHandler { } ); }); + }); + } /** @@ -595,7 +668,7 @@ class UserHandler { // FIXME: use IP in rlkey let rlkey = 'outh:' + userData._id.toString(); - this.counters.ttlcounter(rlkey, 0, consts.AUTH_FAILURES, consts.AUTH_WINDOW, (err, res) => { + this.counters.ttlcounter(rlkey, 0, consts.USER_AUTH_FAILURES, consts.USER_AUTH_WINDOW, (err, res) => { if (err) { err.code = 'InternalDatabaseError'; return callback(err); @@ -612,7 +685,7 @@ class UserHandler { let authFail = (...args) => { // increment rate limit counter on failure - this.counters.ttlcounter(rlkey, 1, consts.AUTH_FAILURES, consts.AUTH_WINDOW, () => { + this.counters.ttlcounter(rlkey, 1, consts.USER_AUTH_FAILURES, consts.USER_AUTH_WINDOW, () => { callback(...args); }); }; @@ -1575,8 +1648,8 @@ class UserHandler { } checkTotp(user, data, callback) { - let rlkey = 'totp:' + user.toString() + (data.ip ? ':' + data.ip : ''); - this.counters.ttlcounter(rlkey, 0, consts.AUTH_FAILURES, consts.AUTH_WINDOW * 3, (err, res) => { + let userRlKey = 'totp:' + user; + this.rateLimit(userRlKey, data, 0, (err, res) => { // NOT Sure why this used "consts.USER_AUTH_WINDOW * 3" if (err) { err.code = 'InternalDatabaseError'; return callback(err); @@ -1587,13 +1660,13 @@ class UserHandler { let authSuccess = (...args) => { // clear rate limit counter on success - this.redis.del(rlkey, () => false); + this.rateLimitReleaseUser(userRlKey, () => false); callback(...args); }; let authFail = (...args) => { // increment rate limit counter on failure - this.counters.ttlcounter(rlkey, 1, consts.TOTP_FAILURES, consts.TOTP_WINDOW, () => { + this.rateLimit(userRlKey, data, 1, () => { callback(...args); }); };