Skip to content

Commit 7286006

Browse files
committed
fix(rate-limiting): refactor with tiered approach for finance apps
- Previous config was too strict (only 10 auth requests per 15 min) - New global limit: 120 requests per minute (handles sync bursts) - Auth-specific limiters per endpoint: - Login/Register: 15 failed attempts/15min (bruteforce protection) - Token Refresh: 20 requests/min (mobile background sync friendly) - Password Change: 5 attempts/hour - Account Deletion: 3 attempts/24h - Transaction/Recurring routes: 60 writes per minute - Token refresh now separate from login - Successful requests skipped for login/register - Using standardHeaders: draft-7 for modern headers Bump version to 0.8.3
1 parent 2b8ceaf commit 7286006

4 files changed

Lines changed: 97 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
All notable changes to this project are documented in this file.
44

5+
## [v0.8.3] - 2026-01-08
6+
7+
### fix
8+
9+
- **Rate Limiting**: Refactored rate limiting with tiered approach for finance apps
10+
- Previous config was too strict (only 10 auth requests per 15 min including token refresh!)
11+
- New global limit: 120 requests per minute (handles sync bursts)
12+
- Auth-specific limiters per endpoint:
13+
- Login/Register: 15 failed attempts per 15 min (bruteforce protection)
14+
- Token Refresh: 20 requests per minute (mobile background sync friendly)
15+
- Password Change: 5 attempts per hour
16+
- Account Deletion: 3 attempts per 24 hours
17+
- Transaction/Recurring routes: 60 writes per minute
18+
- Token refresh now separate from login - won't block legitimate refresh calls
19+
- Successful requests skipped for login/register (won't lock out legitimate users)
20+
- Using `standardHeaders: "draft-7"` for modern rate limit headers
21+
522
## [v0.8.2] - 2026-01-08
623

724
### fix

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "finx",
3-
"version": "0.8.2",
3+
"version": "0.8.3",
44
"description": "A modern personal finance management application",
55
"main": "server.js",
66
"scripts": {

routes/auth.js

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const express = require("express");
2+
const rateLimit = require("express-rate-limit");
23
const {
34
register,
45
login,
@@ -20,20 +21,68 @@ const {
2021

2122
const router = express.Router();
2223

24+
// ============================================================================
25+
// Auth-specific rate limiters
26+
// ============================================================================
27+
28+
// Login/Register limiter - bruteforce protection
29+
// Only counts failed requests to prevent lockout of legitimate users
30+
const loginLimiter = rateLimit({
31+
windowMs: 15 * 60 * 1000, // 15 minutes
32+
limit: 15, // 15 failed attempts per 15 min (1 per minute average)
33+
standardHeaders: "draft-7",
34+
legacyHeaders: false,
35+
skipSuccessfulRequests: true,
36+
message: { error: "Too many authentication attempts. Please try again in 15 minutes." },
37+
});
38+
39+
// Token refresh limiter - more generous for mobile app background sync
40+
// Token refresh is automatic and essential for app functionality
41+
const refreshLimiter = rateLimit({
42+
windowMs: 1 * 60 * 1000, // 1 minute
43+
limit: 20, // 20 refreshes per minute (handles multiple devices, retries, background sync)
44+
standardHeaders: "draft-7",
45+
legacyHeaders: false,
46+
message: { error: "Too many token refresh requests. Please wait before retrying." },
47+
});
48+
49+
// Password change limiter - strict to prevent abuse
50+
const passwordChangeLimiter = rateLimit({
51+
windowMs: 60 * 60 * 1000, // 1 hour
52+
limit: 5, // 5 attempts per hour
53+
standardHeaders: "draft-7",
54+
legacyHeaders: false,
55+
skipSuccessfulRequests: true,
56+
message: { error: "Too many password change attempts. Please try again in an hour." },
57+
});
58+
59+
// Account deletion limiter - very strict
60+
const deletionLimiter = rateLimit({
61+
windowMs: 24 * 60 * 60 * 1000, // 24 hours
62+
limit: 3, // 3 attempts per day
63+
standardHeaders: "draft-7",
64+
legacyHeaders: false,
65+
message: { error: "Too many account deletion attempts. Please try again tomorrow." },
66+
});
67+
68+
// ============================================================================
69+
// Routes
70+
// ============================================================================
71+
2372
// Register route - only enable if DISABLE_REGISTRATION is not set to true
2473
if (process.env.DISABLE_REGISTRATION !== "true") {
25-
router.post("/register", validateBody(registerSchema), register);
74+
router.post("/register", loginLimiter, validateBody(registerSchema), register);
2675
} else {
2776
router.post("/register", (req, res) => {
2877
res.status(403).json({ message: "Registration is disabled" });
2978
});
3079
}
3180

3281
// Login route
33-
router.post("/login", validateBody(loginSchema), login);
82+
router.post("/login", loginLimiter, validateBody(loginSchema), login);
3483

3584
// Refresh token route (no auth middleware - uses refresh token for authentication)
36-
router.post("/refresh", refreshToken);
85+
router.post("/refresh", refreshLimiter, refreshToken);
3786

3887
// Logout route (protected - needs valid access token or refresh token)
3988
router.post("/logout", auth, logout);
@@ -45,10 +94,10 @@ router.get("/me", auth, getCurrentUser);
4594
router.put("/me", auth, validateBody(updateProfileSchema), updateUser);
4695

4796
// Change password (protected route)
48-
router.post("/change-password", auth, validateBody(changePasswordSchema), changePassword);
97+
router.post("/change-password", auth, passwordChangeLimiter, validateBody(changePasswordSchema), changePassword);
4998

5099
// Delete account (protected route)
51-
router.delete("/me", auth, deleteAccount);
100+
router.delete("/me", auth, deletionLimiter, deleteAccount);
52101

53102
module.exports = router;
54103

server.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,33 @@ app.use(
8888
}),
8989
);
9090

91-
// General rate limiting
91+
// ============================================================================
92+
// Rate Limiting Configuration
93+
// Tiered approach for personal finance app with mobile sync support
94+
// Auth-specific limiters are defined in routes/auth.js per-endpoint
95+
// ============================================================================
96+
97+
// General API limiter - generous for data sync operations
98+
// Finance apps may fetch transactions, categories, sources, targets, goals, etc.
9299
const generalLimiter = rateLimit({
93-
windowMs: 15 * 60 * 1000,
94-
max: 1000,
95-
standardHeaders: true,
100+
windowMs: 1 * 60 * 1000, // 1 minute window
101+
limit: 120, // 120 requests per minute (2 req/sec sustained)
102+
standardHeaders: "draft-7",
96103
legacyHeaders: false,
104+
message: { error: "Too many requests. Please wait a moment before trying again." },
97105
});
98106

99-
// Strict rate limiting
100-
const strictLimiter = rateLimit({
101-
windowMs: 15 * 60 * 1000,
102-
max: 10,
103-
standardHeaders: true,
107+
// Transaction/write limiter - slightly stricter for write operations
108+
// Prevents accidental or malicious flooding of transaction/recurring creation
109+
const transactionWriteLimiter = rateLimit({
110+
windowMs: 1 * 60 * 1000, // 1 minute window
111+
limit: 60, // 60 writes per minute (1 per second sustained)
112+
standardHeaders: "draft-7",
104113
legacyHeaders: false,
105-
skipSuccessfulRequests: true,
114+
message: { error: "Too many write operations. Please slow down." },
106115
});
107116

117+
// Apply general limiter to all routes
108118
app.use(generalLimiter);
109119

110120
// Routes
@@ -128,10 +138,12 @@ app.get("/ready", async (req, res) => {
128138
});
129139

130140
// API Routes
131-
app.use("/api/auth", strictLimiter, require("./routes/auth"));
141+
// Auth routes need separate handling - some endpoints are sensitive, others less so
142+
const authRouter = require("./routes/auth");
143+
app.use("/api/auth", authRouter);
132144
app.use("/api/categories", require("./routes/category"));
133-
app.use("/api/transactions", require("./routes/transaction"));
134-
app.use("/api/recurring-transactions", require("./routes/recurring-transactions"));
145+
app.use("/api/transactions", transactionWriteLimiter, require("./routes/transaction"));
146+
app.use("/api/recurring-transactions", transactionWriteLimiter, require("./routes/recurring-transactions"));
135147
app.use("/api/sources", require("./routes/source"));
136148
app.use("/api/targets", require("./routes/target"));
137149
app.use("/api/sharing", require("./routes/sharing"));

0 commit comments

Comments
 (0)