From 60777f516586686107529c816330abeb048c07a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Ruz=20Nieto?= <40019177+aruznieto@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:02:50 +0200 Subject: [PATCH 1/5] feat:rateLimiter --- backend/package.json | 1 + backend/routes/auth.js | 10 +++++++++- yarn.lock | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index ed4077b..75cf1b5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "express-bearer-token": "^2.4.0", + "express-rate-limit": "^7.1.1", "helmet": "^5.1.1", "lodash": "^4.17.21", "lowdb": "^1.0.0", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 890b8e1..0b3bbb8 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,8 +1,15 @@ import express from "express"; +import rateLimit from "express-rate-limit" const router = express.Router(); import * as auth from "../services/auth.js"; +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 requests per windowMs + message: "Too many login attempts, please try again in 15 minutes.", +}); + router.get("/login", async function (req, res) { if (process.env.ZU_DISABLE_AUTH === "true") { res.send({ enabled: false }); @@ -11,9 +18,10 @@ router.get("/login", async function (req, res) { } }); -router.post("/login", async function (req, res) { +router.post("/login", loginLimiter, async function (req, res) { if (req.body.username && req.body.password) { auth.authorize(req.body.username, req.body.password, function (err, user) { + console.log(err.message) if (user) { res.send({ token: user["token"] }); } else { diff --git a/yarn.lock b/yarn.lock index 402ea2d..4ca548d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2020,6 +2020,7 @@ __metadata: eslint-plugin-unicorn: "npm:^48.0.1" express: "npm:^4.18.2" express-bearer-token: "npm:^2.4.0" + express-rate-limit: "npm:^7.1.1" helmet: "npm:^5.1.1" lodash: "npm:^4.17.21" lowdb: "npm:^1.0.0" @@ -4005,6 +4006,15 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^7.1.1": + version: 7.1.1 + resolution: "express-rate-limit@npm:7.1.1" + peerDependencies: + express: ^4 || ^5 + checksum: 28fc48e25e52b269a37a9e223fcd2a234022466645737c182eec2d2a72fdfdb42a396738a062924ee37ff56719957ae0ef092cfde510ba34b63d554ce094f6ba + languageName: node + linkType: hard + "express@npm:^4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" From 7dd6f3729b54009276bd328a7577685b84b017f2 Mon Sep 17 00:00:00 2001 From: Andres Date: Sun, 15 Oct 2023 10:41:19 +0200 Subject: [PATCH 2/5] feat: login-limiter --- .gitignore | 3 ++- backend/routes/auth.js | 8 +++++--- backend/services/auth.js | 4 ++-- .../components/LogIn/components/LogInUser/LogInUser.jsx | 7 +++++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 0a35440..8ff8d26 100644 --- a/.gitignore +++ b/.gitignore @@ -161,4 +161,5 @@ sketch # and uncomment the following lines # .pnp.* -# End of https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node +.yarn/cache/* \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 0b3bbb8..3712878 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,5 +1,5 @@ import express from "express"; -import rateLimit from "express-rate-limit" +import rateLimit from "express-rate-limit"; const router = express.Router(); import * as auth from "../services/auth.js"; @@ -7,7 +7,10 @@ import * as auth from "../services/auth.js"; const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // limit each IP to 5 requests per windowMs - message: "Too many login attempts, please try again in 15 minutes.", + message: { + status: 429, + error: "Too many login attempts, please try again in 15 minutes.", + }, }); router.get("/login", async function (req, res) { @@ -21,7 +24,6 @@ router.get("/login", async function (req, res) { router.post("/login", loginLimiter, async function (req, res) { if (req.body.username && req.body.password) { auth.authorize(req.body.username, req.body.password, function (err, user) { - console.log(err.message) if (user) { res.send({ token: user["token"] }); } else { diff --git a/backend/services/auth.js b/backend/services/auth.js index f908037..c7e1d7c 100644 --- a/backend/services/auth.js +++ b/backend/services/auth.js @@ -8,12 +8,12 @@ export async function authorize(username, password, callback) { throw err; } const user = users.find({ username: username }); - if (!user.value()) return callback(new Error("Cannot find user")); + if (!user.value()) return callback(new Error("Invalid username or password")); // If return "user not found" someone can do a user listing const verified = await verifyHash(password, user.value()["password_hash"]); if (verified) { return callback(null, user.value()); } else { - return callback(new Error("Invalid password")); + return callback(new Error("Invalid username or password")); } } diff --git a/frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx b/frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx index 38256fd..0658d3c 100644 --- a/frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx +++ b/frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx @@ -17,6 +17,8 @@ function LogInUser() { const [open, setOpen] = useState(false); const [snackbarOpen, setSnackbarOpen] = useState(false); + const [error, setError] = useState(""); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -65,7 +67,8 @@ function LogInUser() { .catch(function (error) { setPassword(""); setSnackbarOpen(true); - console.error(error); + setError(error.response.data.error); + // console.error(error.response.data.error); }); }; @@ -114,7 +117,7 @@ function LogInUser() { vertical: "top", horizontal: "center", }} - message="Invalid username or password" + message={error} /> ); From 569384038ae0327365e2d9f5f9ccf6b80d2ddca1 Mon Sep 17 00:00:00 2001 From: Andres Date: Sun, 15 Oct 2023 18:12:17 +0200 Subject: [PATCH 3/5] feat: rate-limiter --- README.md | 2 ++ backend/routes/auth.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59e9513..ceed218 100755 --- a/README.md +++ b/README.md @@ -168,6 +168,8 @@ Advanced manual setups are also supported. Check the following environment varia | ZU_DISABLE_AUTH | unset | If set to true, automatically log in all users. This is useful if ZeroUI is protected by an authentication proxy. Note that when this value is changed, the localStorage of instances of logged-in panels should be cleared | | ZU_LAST_SEEN_FETCH | `true`| Enables [Last Seen feature](https://github.com/dec0dOS/zero-ui/issues/40) | | ZU_LAST_SEEN_SCHEDULE | `*/5 * * * *` | Last Seen cron-like schedule | +| ZT_BAN_TIME | 30 | The duration of the user's ban. (in minutes). You should write it on `environment` zerotier service | +| ZT_TRIES_TO_BAN | 50 | User/password combination attemps before ban. You should write it on `environment` zerotier service | ZeroUI could be deployed as a regular nodejs web application, but it requires a ZeroTier controller that is installed with the `zerotier-one` package. For more info about the network controller, you could read [here](https://github.com/zerotier/ZeroTierOne/tree/master/controller/#readme). diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 3712878..7cc1a80 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -5,8 +5,8 @@ const router = express.Router(); import * as auth from "../services/auth.js"; const loginLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // limit each IP to 5 requests per windowMs + windowMs: (Number(process.env.ZT_BAN_TIME) || 30) * 60 * 1000, // 30 minutes + max: Number(process.env.ZT_TRIES_TO_BAN) || 50, // limit each IP to 50 requests per windowMs message: { status: 429, error: "Too many login attempts, please try again in 15 minutes.", From d65e6fb71a0a57864c29e8e0d6592814307f04c1 Mon Sep 17 00:00:00 2001 From: Andres Date: Sun, 15 Oct 2023 18:33:17 +0200 Subject: [PATCH 4/5] feat: rate-limiter --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ceed218..5522daa 100755 --- a/README.md +++ b/README.md @@ -168,8 +168,8 @@ Advanced manual setups are also supported. Check the following environment varia | ZU_DISABLE_AUTH | unset | If set to true, automatically log in all users. This is useful if ZeroUI is protected by an authentication proxy. Note that when this value is changed, the localStorage of instances of logged-in panels should be cleared | | ZU_LAST_SEEN_FETCH | `true`| Enables [Last Seen feature](https://github.com/dec0dOS/zero-ui/issues/40) | | ZU_LAST_SEEN_SCHEDULE | `*/5 * * * *` | Last Seen cron-like schedule | -| ZT_BAN_TIME | 30 | The duration of the user's ban. (in minutes). You should write it on `environment` zerotier service | -| ZT_TRIES_TO_BAN | 50 | User/password combination attemps before ban. You should write it on `environment` zerotier service | +| ZT_BAN_TIME | 30 | The duration of the user's ban. (in minutes). | +| ZT_TRIES_TO_BAN | 50 | User/password combination attemps before ban. | ZeroUI could be deployed as a regular nodejs web application, but it requires a ZeroTier controller that is installed with the `zerotier-one` package. For more info about the network controller, you could read [here](https://github.com/zerotier/ZeroTierOne/tree/master/controller/#readme). From 41f12ad2f32f3ae7d66acb2e9e4fa412fe523f06 Mon Sep 17 00:00:00 2001 From: dec0dOS Date: Sun, 15 Oct 2023 21:41:52 +0100 Subject: [PATCH 5/5] chore: small changes --- README.md | 4 ++-- backend/routes/auth.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5522daa..228587a 100755 --- a/README.md +++ b/README.md @@ -168,8 +168,8 @@ Advanced manual setups are also supported. Check the following environment varia | ZU_DISABLE_AUTH | unset | If set to true, automatically log in all users. This is useful if ZeroUI is protected by an authentication proxy. Note that when this value is changed, the localStorage of instances of logged-in panels should be cleared | | ZU_LAST_SEEN_FETCH | `true`| Enables [Last Seen feature](https://github.com/dec0dOS/zero-ui/issues/40) | | ZU_LAST_SEEN_SCHEDULE | `*/5 * * * *` | Last Seen cron-like schedule | -| ZT_BAN_TIME | 30 | The duration of the user's ban. (in minutes). | -| ZT_TRIES_TO_BAN | 50 | User/password combination attemps before ban. | +| ZU_LOGIN_LIMIT_WINDOW | 30 | The duration of the IP ban in minutes | +| ZT_LOGIN_LIMIT_ATTEMPTS | 50 | Login attemps before ban | ZeroUI could be deployed as a regular nodejs web application, but it requires a ZeroTier controller that is installed with the `zerotier-one` package. For more info about the network controller, you could read [here](https://github.com/zerotier/ZeroTierOne/tree/master/controller/#readme). diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7cc1a80..f1d2368 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -5,8 +5,8 @@ const router = express.Router(); import * as auth from "../services/auth.js"; const loginLimiter = rateLimit({ - windowMs: (Number(process.env.ZT_BAN_TIME) || 30) * 60 * 1000, // 30 minutes - max: Number(process.env.ZT_TRIES_TO_BAN) || 50, // limit each IP to 50 requests per windowMs + windowMs: (Number(process.env.ZU_LOGIN_LIMIT_WINDOW) || 30) * 60 * 1000, // 30 minutes + max: Number(process.env.ZT_LOGIN_LIMIT_ATTEMPTS) || 50, // limit each IP to 50 requests per windowMs message: { status: 429, error: "Too many login attempts, please try again in 15 minutes.",