├── .npmrc ├── .git-blame-ignore-revs ├── .gitignore ├── eslint.config.cjs ├── LICENSE.txt ├── package.json ├── README.md ├── index.js └── test └── tests.js /.npmrc: -------------------------------------------------------------------------------- 1 | @clear:registry=https://npm.pkg.github.com/clear 2 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Added formatting 2 | 7b466c77bfa31ac5e2b08fd19e7c382b01c9e63b 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://gitignore.io 2 | 3 | ### Node ### 4 | lib-cov 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.gz 12 | 13 | pids 14 | logs 15 | results 16 | 17 | npm-debug.log 18 | node_modules 19 | 20 | pnpm-lock.yaml 21 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const clearConfig = require("@clear/style/eslint/js"); 2 | 3 | module.exports = [ 4 | // Test support and config (adds global var definitions and test-specific rules) 5 | mochaPlugin.configs.flat.recommended, 6 | { 7 | rules: { 8 | "mocha/prefer-arrow-callback": "off", 9 | "mocha/no-mocha-arrows": "off", 10 | }, 11 | }, 12 | 13 | // Add additional globals for mocha and node support 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | 19 | should: false, 20 | }, 21 | }, 22 | }, 23 | 24 | // Base config 25 | ...clearConfig, 26 | ]; 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-brute-mongo", 3 | "version": "1.0.0", 4 | "description": "MongoDB store for express-brute.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "lint": "eslint .", 9 | "format": "eslint --fix ." 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/auth0/express-brute-mongo.git" 14 | }, 15 | "author": "José F. Romaniello (http://joseoncode.com)", 16 | "license": "BSD", 17 | "prettier": "@clear/style/prettier", 18 | "devDependencies": { 19 | "@clear/style": "^0.3.0", 20 | "eslint": "^9.0.0", 21 | "eslint-plugin-mocha": "^10.5.0", 22 | "expect.js": "~0.3.1", 23 | "globals": "^15.11.0", 24 | "mocha": "~10.7.3", 25 | "moment": "~2.30.1", 26 | "mongo-getdb": "~5.0.1", 27 | "prettier": "^3.3.3" 28 | }, 29 | "dependencies": { 30 | "express-brute": "~1.0.1", 31 | "xtend": "~2.1.1" 32 | }, 33 | "peerDependencies": { 34 | "express": "4.x" 35 | }, 36 | "pnpm": { 37 | "overrides": { 38 | "underscore@<1.12.1": ">1.12.1" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MongoDB store adapter for the [express-brute](https://github.com/AdamPflug/express-brute). 2 | 3 | ## Installation 4 | 5 | ``` 6 | npm install express-brute-mongo 7 | ``` 8 | 9 | ## Usage 10 | 11 | ```javascript 12 | var ExpressBrute = require('express-brute'), 13 | var MongoStore = require('express-brute-mongo'); 14 | var MongoClient = require('mongodb').MongoClient; 15 | 16 | var store = new MongoStore(function (ready) { 17 | MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) { 18 | if (err) throw err; 19 | ready(db.collection('bruteforce-store')); 20 | }); 21 | }); 22 | 23 | var bruteforce = new ExpressBrute(store); 24 | 25 | app.post('/auth', 26 | bruteforce.prevent, // error 403 if we hit this route too often 27 | function (req, res, next) { 28 | res.send('Success!'); 29 | } 30 | ); 31 | ``` 32 | 33 | ## Expire documents 34 | 35 | Create an index with `expireAfterSeconds: 0` in mongo as follows: 36 | 37 | ``` 38 | db.my_api_limits_coll.ensureIndex({expires: 1}, {expireAfterSeconds: 0}); 39 | ``` 40 | 41 | ## Issue Reporting 42 | 43 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 44 | 45 | ## Author 46 | 47 | [Auth0](auth0.com) 48 | 49 | ## License 50 | 51 | This project is licensed under the MIT license. See the [LICENSE](LICENSE.txt) file for more info. 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var AbstractClientStore = require("express-brute/lib/AbstractClientStore"); 2 | var xtend = require("xtend"); 3 | var moment = require("moment"); 4 | 5 | var MongoStore = (module.exports = function (getCollection, options) { 6 | AbstractClientStore.apply(this, arguments); 7 | this.options = xtend({}, MongoStore.defaults, options); 8 | var self = this; 9 | getCollection(function (collection) { 10 | self._collection = collection; 11 | }); 12 | }); 13 | 14 | MongoStore.prototype = Object.create(AbstractClientStore.prototype); 15 | 16 | MongoStore.prototype.set = function (key, value, lifetime, callback) { 17 | var _id = this.options.prefix + key; 18 | var expiration = lifetime ? moment().add(lifetime, "seconds").toDate() : undefined; 19 | 20 | this._collection.replaceOne( 21 | { 22 | _id: _id, 23 | }, 24 | { 25 | _id: _id, 26 | data: value, 27 | expires: expiration, 28 | }, 29 | { 30 | upsert: true, 31 | }, 32 | function () { 33 | if (callback) callback.apply(this, arguments); 34 | }, 35 | ); 36 | }; 37 | 38 | MongoStore.prototype.get = function (key, callback) { 39 | var _id = this.options.prefix + key; 40 | var collection = this._collection; 41 | 42 | collection.findOne({ _id: _id }, function (err, doc) { 43 | if (err) { 44 | typeof callback == "function" && callback(err, null); 45 | } else { 46 | var data; 47 | if (doc && doc.expires < new Date()) { 48 | collection.remove({ _id: _id }, { w: 0 }); 49 | return callback(); 50 | } 51 | if (doc) { 52 | data = doc.data; 53 | data.lastRequest = new Date(data.lastRequest); 54 | data.firstRequest = new Date(data.firstRequest); 55 | } 56 | typeof callback == "function" && callback(err, data); 57 | } 58 | }); 59 | }; 60 | 61 | MongoStore.prototype.reset = function (key, callback) { 62 | var _id = this.options.prefix + key; 63 | this._collection.remove({ _id: _id }, function () { 64 | typeof callback == "function" && callback.apply(this, arguments); 65 | }); 66 | }; 67 | 68 | MongoStore.defaults = { 69 | prefix: "", 70 | }; 71 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | var expect = require("expect.js"); 2 | 3 | var MongoStore = require("../"); 4 | var getDb = require("mongo-getdb"); 5 | getDb.init("default", "mongodb://localhost/test_brute_express_mongo"); 6 | 7 | var mongoStore, collection; 8 | 9 | describe("MongoStore", function () { 10 | beforeEach(function (done) { 11 | mongoStore = new MongoStore(function (callback) { 12 | getDb(function (db) { 13 | collection = db.collection("api_limits"); 14 | collection.remove({}, function () { 15 | callback(collection); 16 | done(); 17 | }); 18 | }); 19 | }); 20 | }); 21 | 22 | it("should be able to set a value", function (done) { 23 | mongoStore.set("foo", { bar: 123 }, 1000, function (err) { 24 | if (err) return done(err); 25 | collection.findOne({ _id: "foo" }, function (err, limit) { 26 | if (err) return done(err); 27 | expect(limit.data).have.property("bar"); 28 | expect(limit.expires).to.be.a(Date); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it("should be able to get a value", function (done) { 35 | mongoStore.set("foo", { bar: 123 }, 1000, function (err) { 36 | if (err) return done(err); 37 | mongoStore.get("foo", function (err, data) { 38 | if (err) return done(err); 39 | expect(data).have.property("bar"); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | it("should return undef if expired", function (done) { 46 | mongoStore.set("foo", { bar: 123 }, 0, function (err) { 47 | if (err) return done(err); 48 | setTimeout(function () { 49 | mongoStore.get("foo", function (err, data) { 50 | if (err) return done(err); 51 | expect(data).to.be(undefined); 52 | done(); 53 | }); 54 | }, 200); 55 | }); 56 | }); 57 | 58 | it("should delete the doc if expired", function (done) { 59 | mongoStore.set("foo", { bar: 123 }, 0, function (err) { 60 | if (err) return done(err); 61 | setTimeout(function () { 62 | mongoStore.get("foo", function (_err, _data) { 63 | setTimeout(function () { 64 | collection.findOne({ _id: "foo" }, function (err, d) { 65 | expect(d).to.be(null); 66 | done(); 67 | }); 68 | }, 100); 69 | }); 70 | }, 100); 71 | }); 72 | }); 73 | 74 | it("should be able to reset", function (done) { 75 | mongoStore.set("foo", { bar: 123 }, 1000, function (err) { 76 | if (err) return done(err); 77 | mongoStore.reset("foo", function (err) { 78 | if (err) return done(err); 79 | 80 | collection.findOne({ _id: "foo" }, function (err, limit) { 81 | if (err) return done(err); 82 | expect(limit).to.be(null); 83 | done(); 84 | }); 85 | }); 86 | }); 87 | }); 88 | }); 89 | --------------------------------------------------------------------------------