Skip to content

Commit

Permalink
Add rate limiting for /password-reset endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
mahnuh committed Nov 21, 2023
1 parent cc9bd94 commit 68ea766
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ It's easy to add custom fields to user documents. When added to a `profile` fiel

## Brute force protection

To enable brute force protection for the `/login` route you just need to add `loginRateLimit: {}` to `security` in your `config`. Adding just the empty object uses following defaults that can be overriden as needed:
To enable brute force protection for the `/login` route you just need to add `loginRateLimit: {}` to `security` in your `config`. The same goes for the `/password-reset` route, where you just need to add `passwordResetRateLimit: {}` accordingly. Adding just the empty object uses following defaults that can be overriden as needed:

```ts
const config {
Expand Down Expand Up @@ -406,6 +406,7 @@ couch-auth uses [express-slow-down](https://www.npmjs.com/package/express-slow-d

### Important notes:
- You won't be able to override the keyGenerator option, as we use usernameField from the config.
- When activating rate limiting for the `/password-reset` route, `username` field is required in the request body!
- If you want to use Redis Store instead of Memory Store you currently need to use [rate-limit-redis@2x](https://github.com/wyattjoh/rate-limit-redis/tree/v2.1.0) for now [due to known issues](https://github.com/express-rate-limit/express-slow-down/issues/40#issuecomment-1548011953) with newer versions of rate-limit-redis.

## Advanced Configuration
Expand Down
44 changes: 43 additions & 1 deletion src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,50 @@ export default function (
}
);

if (!disabled.includes('password-reset'))
if (!disabled.includes('password-reset')) {
const speedLimiter = slowDown({
windowMs:
config.security.passwordResetRateLimit?.windowMs || 5 * 60 * 1000,
delayAfter: config.security.passwordResetRateLimit?.delayAfter || 3,
delayMs: config.security.passwordResetRateLimit
? config.security.passwordResetRateLimit.delayMs || 500
: 0,
maxDelayMs: config.security.passwordResetRateLimit?.maxDelayMs || 10000,
skipSuccessfulRequests:
config.security.passwordResetRateLimit?.skipSuccessfulRequests || true,
skipFailedRequests:
config.security.passwordResetRateLimit?.skipFailedRequests || false,
keyGenerator: function (req) {
const usernameField = config.local.usernameField || 'username';

return req.body[usernameField];
},
onLimitReached:
config.security.passwordResetRateLimit?.onLimitReached ||
function () {},
store: config.security.passwordResetRateLimit?.store || undefined,
headers: config.security.passwordResetRateLimit?.headers || false
});

router.post(
'/password-reset',
function (req: Request, res: Response, next: NextFunction) {
if (!config.security.passwordResetRateLimit) {
return next();
}

const usernameField = config.local.usernameField || 'username';

if (!req.body[usernameField]) {
return next({
error: 'username required',
status: 422
});
}

return next();
},
speedLimiter,
function (req: Request, res: Response, next: NextFunction) {
user.resetPassword(req.body, req).then(
function (currentUser) {
Expand Down Expand Up @@ -264,6 +305,7 @@ export default function (
);
}
);
}

if (!disabled.includes('password-change'))
router.post(
Expand Down
1 change: 1 addition & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface SecurityConfig {
*/
forwardErrors?: boolean;
loginRateLimit?: ExpressSlowDownOptions;
passwordResetRateLimit?: ExpressSlowDownOptions;
}

export interface LengthConstraint {
Expand Down

0 comments on commit 68ea766

Please sign in to comment.