Skip to content

Instantly share code, notes, and snippets.

@animir
Last active November 10, 2023 16:57
Show Gist options
  • Save animir/295912fc5a1d600f295af8410604e25b to your computer and use it in GitHub Desktop.
Save animir/295912fc5a1d600f295af8410604e25b to your computer and use it in GitHub Desktop.
Node.js rate-limiter-flexible. Brute-force protection - Block source of requests by IP.
const http = require('http');
const express = require('express');
const Redis = require('ioredis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = new Redis({ enableOfflineQueue: false });
const maxWrongAttemptsByIPperMinute = 5;
const maxWrongAttemptsByIPperDay = 100;
const limiterFastBruteByIP = new RateLimiterRedis({
redis: redisClient,
keyPrefix: 'login_fail_ip_per_minute',
points: maxWrongAttemptsByIPperMinute,
duration: 30,
blockDuration: 60 * 10, // Block for 10 minutes, if 5 wrong attempts per 30 seconds
});
const limiterSlowBruteByIP = new RateLimiterRedis({
redis: redisClient,
keyPrefix: 'login_fail_ip_per_day',
points: maxWrongAttemptsByIPperDay,
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24, // Block for 1 day, if 100 wrong attempts per day
});
async function loginRoute(req, res) {
const ipAddr = req.connection.remoteAddress;
const [resFastByIP, resSlowByIP] = await Promise.all([
limiterFastBruteByIP.get(ipAddr),
limiterSlowBruteByIP.get(ipAddr),
]);
let retrySecs = 0;
// Check if IP is already blocked
if (resSlowByIP !== null && resSlowByIP.consumedPoints > maxWrongAttemptsByIPperDay) {
retrySecs = Math.round(resSlowByIP.msBeforeNext / 1000) || 1;
} else if (resFastByIP !== null && resFastByIP.consumedPoints > maxWrongAttemptsByIPperMinute) {
retrySecs = Math.round(resFastByIP.msBeforeNext / 1000) || 1;
}
if (retrySecs > 0) {
res.set('Retry-After', String(retrySecs));
res.status(429).send('Too Many Requests');
} else {
const user = authorise(req.body.email, req.body.password);
if (!user.isLoggedIn) {
// Consume 1 point from limiters on wrong attempt and block if limits reached
try {
await Promise.all([
limiterFastBruteByIP.consume(ipAddr),
limiterSlowBruteByIP.consume(ipAddr),
]);
res.status(400).end('email or password is wrong');
} catch (rlRejected) {
if (rlRejected instanceof Error) {
throw rlRejected;
} else {
res.set('Retry-After', String(Math.round(rlRejected.msBeforeNext / 1000)) || 1);
res.status(429).send('Too Many Requests');
}
}
} else {
res.end('authorized');
}
}
}
const app = express();
app.post('/login', async (req, res) => {
try {
await loginRoute(req, res);
} catch (err) {
res.status(500).end();
}
});
const server = http.createServer(app);
server.listen(3000);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment