Skip to content

Commit

Permalink
feat(app-factory.js): added rate-limiting facility
Browse files Browse the repository at this point in the history
used express-rate-limit package, currently fetching configurations from production.js file

feat #139
  • Loading branch information
kdhttps committed Nov 23, 2020
1 parent 1b7d9da commit 152b4b8
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 2 deletions.
4 changes: 3 additions & 1 deletion config/production.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module.exports = {
passportFile: '/etc/gluu/conf/passport-config.json',
saltFile: '/etc/gluu/conf/salt',
timerInterval: 60000
timerInterval: 60000,
rateLimitWindowMs: 24 * 60 * 60 * 1000, // 24 hrs in milliseconds
rateLimitMaxRequestAllow: 1000
}
4 changes: 4 additions & 0 deletions config/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,14 @@ const passportConfigAuthorizedResponse = {

const root = process.cwd()
const passportFile = `${root}/test/testdata/passport-config.json`
const rateLimitWindowMs = 24 * 60 * 60 * 1000
const rateLimitMaxRequestAllow = 45

module.exports = {
saltFile,
passportConfig,
rateLimitWindowMs,
rateLimitMaxRequestAllow,
timerInterval,
passportFile,
passportConfigAuthorizedResponse
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"eslint-plugin-no-wildcard-postmessage": "^0.2.0",
"express": "^4.16.4",
"express-prom-bundle": "^6.2.0",
"express-rate-limit": "^5.2.3",
"express-session": "^1.15.6",
"jsonwebtoken": "^8.4.0",
"memcached": "^2.2.2",
Expand Down
2 changes: 2 additions & 0 deletions server/app-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const routes = require('./routes')
const metricsMiddleware = require('../server/utils/metrics')
const csurf = require('csurf')
const { randomSecret } = require('./utils/misc')
const { rateLimiter } = require('./rate-limiter')

class AppFactory {
createApp () {
Expand All @@ -20,6 +21,7 @@ class AppFactory {
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(csurf({ cookie: true }))
app.use(rateLimiter)

app.use(session({
cookie: {
Expand Down
29 changes: 29 additions & 0 deletions server/rate-limiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const rateLimit = require('express-rate-limit')
const config = require('config')

/**
* Timeframe in miliseconds for which requests are checked/remembered
* default is 24 hrs in miliseconds
*/
const windowMs = (
config.has('rateLimitWindowMs') && config.get('rateLimitWindowMs')
) || 24 * 60 * 60 * 1000

/**
* Max number of connections during windowMs milliseconds before sending a 429 response.
* Default is 1000 number of requests in 24 hrs
*/
const max = (
config.has('rateLimitMaxRequestAllow') && config.get('rateLimitMaxRequestAllow')
) || 1000

const rateLimiter = rateLimit({
windowMs,
max,
message: `You have exceeded the ${max} requests in ${windowMs} milliseconds limit!`,
headers: true
})

module.exports = {
rateLimiter
}
11 changes: 10 additions & 1 deletion test/app-factory.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable security/detect-non-literal-fs-filename */
const chai = require('chai')
const assert = chai.assert
const rewire = require('rewire')
const appFactoryRewire = rewire('../server/app-factory.js')
const sinon = require('sinon')
const { rateLimiter } = require('../server/rate-limiter')

/**
* Helper: Returns the argument call number with matching args
Expand All @@ -27,20 +29,26 @@ function assertCalledWithFunctionAsArg (spyFn, argFn) {

describe('csurf middleware', () => {
const rewiredCsurf = appFactoryRewire.__get__('csurf')
const rewiredRateLimiter = appFactoryRewire.__get__('rateLimiter')

it('should exist', () => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
assert.exists(rewiredCsurf)
assert.exists(rewiredRateLimiter)
})

it('should be a function', () => {
assert.isFunction(rewiredCsurf)
assert.isFunction(rewiredRateLimiter)
})

it('should be equal csurf module', () => {
assert.strictEqual(rewiredCsurf, require('csurf'))
})

it('should be equal express-rate-limit module', () => {
assert.strictEqual(rewiredRateLimiter, rateLimiter)
})

it('should be called once as app.use arg', () => {
const csurf = require('csurf')
const app = appFactoryRewire.__get__('app')
Expand All @@ -51,6 +59,7 @@ describe('csurf middleware', () => {
appInstance.createApp()

assertCalledWithFunctionAsArg(appUseSpy, csurf({ cookies: true }))
assertCalledWithFunctionAsArg(appUseSpy, rewiredRateLimiter)
sinon.restore()
})
})
14 changes: 14 additions & 0 deletions test/features/rate-limiting.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Feature: Rate limiting

Application should control the number of request comes and
special against the DOC(denial-of-service) attack
# Issue: https://github.com/GluuFederation/gluu-passport/issues/139

Below test cases are using test.js config. Current rate limit is 45.
In first step, we requested only 3 times to successfully reach limit i.e. 45
because we already requested 42 time in endpoint-metrics-steps.js test cases

Scenario: Application should limit the request
Given requesting application 3 times
When requesting more than 45
Then should return request limit exceeded with http status 429
28 changes: 28 additions & 0 deletions test/features/steps/rate-limiting-steps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { Given, When, Then } = require('cucumber')
const got = require('got')
const chai = require('chai')
const assert = chai.assert

Given('requesting application {int} times', async (max) => {
for (let i = 0; i < max; i++) {
const response = await got('http://127.0.0.1:8090/passport/health-check', { retry: 0 })
assert.equal(response.statusCode, 200,
'response.statusCode is NOT 200')
}
})

When('requesting more than 45', async () => {
try {
await got('http://127.0.0.1:8090/passport/health-check', { retry: 0 })
} catch (err) {
assert.equal(err.message, 'Response code 429 (Too Many Requests)', 'response is not 429')
}
})

Then('should return request limit exceeded with http status 429', async () => {
try {
await got('http://127.0.0.1:8090/passport/health-check', { retry: 0 })
} catch (err) {
assert.equal(err.message, 'Response code 429 (Too Many Requests)', 'response is not 429')
}
})

0 comments on commit 152b4b8

Please sign in to comment.