I'm working on an api that includes sending emails for password resets and email confirmations. Along with functions like "user sign up".
I'm trying to include an action limiter that allows users to perform these actions a limited amount of times within a given time frame to prevent malicious use.
At first I thought using IP addresses would be fine because even malicious users run out of ip address eventually (at least that im aware) but then I realized this might block users who are in a large building and would possibly inconvenience VPN users.
What is the best way to uniquely identify a user who is not signed in in order to limit their actions on certain functions? Is this possible? How does FAANG handle this?
Here's an example I wrote in nodejs if anybody has any feedback and/or ideas on how to make this more unique I'd be all ears.
const db = require('../../common/database');
// const ActionLimiterEnum = require('../../enums/action-limiter').actionLimiterEnum;
const NumberUtil = require('../../utils/number');
const ObjectUtil = require('../../utils/object');
// !==========================================================================================!
// This module has been put on hold until I can think of a way to uniquely identify users
// Major problem about this is that it may deny large groups of people whom use the same ip
// VPNS and/or large buildings
//
// Could cause more problems than it solves
// !==========================================================================================!
// Simple action limiter for how often a user can perform actions
// Needs to be saved to a database and not an instance because there may be multiple instances and/or they may be reset
// Object of action or "signIn"
async function actionLimiter(action,ip){
const d = {err: {code:0,message:""},res:{}}; let r,sql,vars;
r = await checkLimit(action,ip);
if(r.err.code) return r;
r = await incrementLimit(action,ip);
if(r.err.code) return r;
return d;
}
async function checkLimit(action,ip){
const d = {err: {code:0,message:""},res:{}}; let r,sql,vars;
if(action === "signIn"){
r = await checkLimit(ActionLimiterEnum.signInShortTerm,ip); if(r.err.code) return r;
r = await checkLimit(ActionLimiterEnum.signInMidTerm,ip); if(r.err.code) return r;
r = await checkLimit(ActionLimiterEnum.signInLongTerm,ip); if(r.err.code) return r;
return d;
}
const numberIp = NumberUtil.ipToNumber(ip);
var deleteDate = new Date();
deleteDate.setMilliseconds(deleteDate.getMilliseconds() - action.time);
sql = "DELETE FROM m_admin_action_limiter WHERE action_id = ? AND created_date <= ?";
vars = [action.id,deleteDate];
r = await db.query(sql,vars);
if(r.err.code) return r;
sql = "SELECT * FROM m_admin_action_limiter WHERE action_id = ? AND ip = ?";
vars = [action.id,numberIp];
r = await db.query(sql,vars);
if(r.err.code) return r;
if(r.res.length){
const results = ObjectUtil.toCamelCaseKeys(r.res[0]);
if(results.actionCount >= action.maxCount){
d.err.code = 1;
d.err.message = "Sorry this ip has performed this action too often please try again later. ";
switch(action.id){
case ActionLimiterEnum.signInShortTerm.id:
case ActionLimiterEnum.signInMidTerm.id:
case ActionLimiterEnum.signInLongTerm.id:
d.err.message += "If you're having trouble remembering your password you can reset it via email. ";
break;
default:
break;
}
d.err.actionLimited = true;
return d;
}
}
return d;
}
async function incrementLimit(action,ip){
const d = {err: {code:0,message:""},res:{}}; let r,sql,vars;
if(action === "signIn"){
r = await incrementLimit(ActionLimiterEnum.signInShortTerm,ip); if(r.err.code) return r;
r = await incrementLimit(ActionLimiterEnum.signInMidTerm,ip); if(r.err.code) return r;
r = await incrementLimit(ActionLimiterEnum.signInLongTerm,ip); if(r.err.code) return r;
return d;
}
const numberIp = NumberUtil.ipToNumber(ip);
const timenow = new Date();
sql = "SELECT admin_action_limiter_id FROM m_admin_action_limiter WHERE action_id = ? AND ip = ?";
vars = [action.id,numberIp];
r = await db.query(sql,vars);
if(r.err.code) return r;
if(r.res.length){
// update
const id = r.res[0]['admin_action_limiter_id']
sql = "UPDATE m_admin_action_limiter SET action_count = action_count + 1 WHERE admin_action_limiter_id = ?";
vars = [id];
r = await db.query(sql,vars);
if(r.err.code) return r;
}else{
// insert
sql = "INSERT INTO m_admin_action_limiter (action_id,ip,action_count,created_date) VALUES(?,?,?,?)";
vars = [action.id,numberIp,1,timenow];
r = await db.query(sql,vars);
if(r.err.code) return r;
}
return d;
}
module.exports = {
actionLimiter,
checkLimit,
incrementLimit,
Enum: ActionLimiterEnum,
};
// SQL
// -- -----------------------------------------------------
// -- Table `m_admin_action_limiter`
// -- -----------------------------------------------------
// CREATE TABLE IF NOT EXISTS `m_admin_action_limiter`(
// `admin_action_limiter_id` int(11) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
// `action_id` int(2) unsigned NOT NULL,
// `ip` int(11) unsigned NOT NULL,
// `action_count` unsigned int(11) DEFAULT 1,
// `created_date` DATETIME NOT NULL
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// ALTER TABLE `m_admin_action_limiter`
// ADD CONSTRAINT m_admin_action_limiter_unique UNIQUE (`action_id`,`ip`);
// CREATE INDEX `created_date_index` ON `m_admin_action_limiter` (`created_date`);
// Enums
// time: (days * hours * minutes * seconds * milliseconds)
// time - amount of times they can try within the alotted count
// const actionLimiterEnum = {
// signInShortTerm: {
// id: 1,
// time: (1 * 1 * 60 * 60 * 1000), // 1 hour
// maxCount: 24,
// },
// signInMidTerm: {
// id: 2,
// time: (7 * 24 * 60 * 60 * 1000), // 7 days
// maxCount: 150,
// },
// signInLongTerm: {
// id: 3,
// time: (120 * 24 * 60 * 60 * 1000), // 120 days
// maxCount: 840,
// },
// authToken: {
// id: 4,
// time: (1 * 24 * 60 * 60 * 1000), // 1 day
// maxCount: 16,
// },
// createAccount: {
// id: 5,
// time: (90 * 24 * 60 * 60 * 1000), // 90 days
// maxCount: 3,
// },
// passwordCheck: {
// id: 6,
// time: (7 * 24 * 60 * 60 * 1000), // 1 week
// maxCount: 150,
// },
// }
// module.exports = {
// actionLimiterEnum,
// };