Skip to content

Commit

Permalink
Add login brute force protection (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
mahnuh committed Jul 19, 2023
1 parent 04dfc89 commit af8524d
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 2 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ User authentication is often the hardest part of building any web app, especiall
- Link multiple authentication strategies to the same account for user convenience
- Provides seamless token access to both your CouchDB server (or Cloudant) and your private API
- Manages permissions on an unlimited number of private or shared user databases and seeds them with the correct design documents
- Enable slowing down requests to /login on errors to [prevent brute force attacks](#brute-force-protection)

## How It Works

Expand Down Expand Up @@ -373,6 +374,40 @@ 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:

```ts
const config {

...

security: {

...

loginRateLimit: {
windowMs: 5 * 60 * 1000,
delayAfter: 3,
delayMs: 500
maxDelayMs: 10000,
skipSuccessfulRequests: true,
skipFailedRequests: false,
onLimitReached: function () {},
store: undefined, // if undefined uses Memory Store by default
headers: false
}
}
}
```

couch-auth uses [express-slow-down](https://www.npmjs.com/package/express-slow-down) under the hood, feel free to check the docs to dig deeper into configuration options.

### Important notes:
- You won't be able to override the keyGenerator option, as we use usernameField from the config.
- 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

Take a look at `config.example.js` or `src/types/config.d.ts` for a complete tour of all available configuration options. You'll find a lot of cool hidden features there that aren't documented here.
Expand Down
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@
"@sl-nx/couch-pwd": "2.0.0",
"@sl-nx/sofa-model": "^1.0.3",
"@types/express": "^4.17.11",
"@types/express-slow-down": "1.3.2",
"@types/nodemailer": "^6.4.0",
"@types/passport": "^1.0.6",
"deepmerge": "^4.2.2",
"express": "^4.17.1",
"express-slow-down": "1.6.0",
"nano": "^10.0.0",
"nodemailer": "^6.7.0",
"nunjucks": "^3.2.3",
Expand Down
31 changes: 29 additions & 2 deletions src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
import { NextFunction, Request, Response, Router } from 'express';
import { Authenticator } from 'passport';
import slowDown from 'express-slow-down';
import { Config } from './types/config';
import { SlRequest } from './types/typings';
import { User, ValidErr } from './user';
Expand Down Expand Up @@ -38,9 +39,32 @@ export default function (
})(req, res, next);
}

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

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

router.post(
'/login',
speedLimiter,
function (req, res, next) {
loginLocal(req, res, next);
},
Expand All @@ -62,6 +86,7 @@ export default function (
);
}
);
}

if (!disabled.includes('refresh'))
router.post(
Expand Down Expand Up @@ -464,7 +489,9 @@ export default function (
if (env !== 'development') {
isExpected
? res.status(err.status).json(err)
: res.status(500).json({ status: 500, error: 'Internal Server Error' });
: res
.status(500)
.json({ status: 500, error: 'Internal Server Error' });
} else {
res.status(err.status || 500).json(err);
}
Expand Down
2 changes: 2 additions & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SESTransport from 'nodemailer/lib/ses-transport';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import StreamTransport from 'nodemailer/lib/stream-transport';
import { ConsentConfig, PooledSMTPOptions } from './typings';
import { Options as ExpressSlowDownOptions } from 'express-slow-down';

export interface TestConfig {
/** Use a stub transport so no email is actually sent. Default: false */
Expand Down Expand Up @@ -97,6 +98,7 @@ export interface SecurityConfig {
* forward to the express error mechanism (`next(err)`)
*/
forwardErrors?: boolean;
loginRateLimit?: ExpressSlowDownOptions;
}

export interface LengthConstraint {
Expand Down

0 comments on commit af8524d

Please sign in to comment.