Skip to content

Commit

Permalink
Merge pull request #357 from sasjs/cosmosdb-issue
Browse files Browse the repository at this point in the history
fix: use RateLimiterMemory instead of RateLimiterMongo
  • Loading branch information
allanbowe authored Apr 27, 2023
2 parents 70c3834 + 7df9588 commit 77fac66
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 94 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
# After this, access is blocked for an hour
# Store number for 90 days since first fail
# Store number for 24 days since first fail
# Once a successful login is attempted, it resets
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
Expand Down
152 changes: 71 additions & 81 deletions api/src/routes/api/spec/web.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,77 @@ describe('web', () => {
})
})

describe('SASLogon/authorize', () => {
let csrfToken: string
let authCookies: string

beforeAll(async () => {
;({ csrfToken } = await getCSRF(app))

await userController.createUser(user)

const credentials = {
username: user.username,
password: user.password
}

;({ authCookies } = await performLogin(app, credentials, csrfToken))
})

afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})

it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({ clientId })

expect(res.body).toHaveProperty('code')
})

it('should respond with Bad Request if CSRF Token is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.send({ clientId })
.expect(400)

expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})

it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({})
.expect(400)

expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})

it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
})
.expect(403)

expect(res.text).toEqual('Error: Invalid clientId.')
expect(res.body).toEqual({})
})
})

describe('SASLogon/login', () => {
let csrfToken: string

Expand Down Expand Up @@ -187,78 +258,6 @@ describe('web', () => {
expect(res.body).toEqual({})
})
})

describe('SASLogon/authorize', () => {
let csrfToken: string
let authCookies: string

beforeAll(async () => {
await deleteDocumentsFromLimitersCollections()
;({ csrfToken } = await getCSRF(app))

await userController.createUser(user)

const credentials = {
username: user.username,
password: user.password
}

;({ authCookies } = await performLogin(app, credentials, csrfToken))
})

afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})

it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({ clientId })

expect(res.body).toHaveProperty('code')
})

it('should respond with Bad Request if CSRF Token is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.send({ clientId })
.expect(400)

expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})

it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({})
.expect(400)

expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})

it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
})
.expect(403)

expect(res.text).toEqual('Error: Invalid clientId.')
expect(res.body).toEqual({})
})
})
})

const getCSRF = async (app: Express) => {
Expand All @@ -285,12 +284,3 @@ const extractCSRF = (text: string) =>
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
text
)![1]

const deleteDocumentsFromLimitersCollections = async () => {
const { collections } = mongoose.connection
const login_fail_ip_per_day_collection = collections['login_fail_ip_per_day']
await login_fail_ip_per_day_collection.deleteMany({})
const login_fail_consecutive_username_and_ip_collection =
collections['login_fail_consecutive_username_and_ip']
await login_fail_consecutive_username_and_ip_collection.deleteMany({})
}
21 changes: 9 additions & 12 deletions api/src/utils/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import mongoose from 'mongoose'
import { RateLimiterMongo } from 'rate-limiter-flexible'
import { RateLimiterMemory } from 'rate-limiter-flexible'

export class RateLimiter {
private static instance: RateLimiter
private limiterSlowBruteByIP: RateLimiterMongo
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMongo
private limiterSlowBruteByIP: RateLimiterMemory
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMemory
private maxWrongAttemptsByIpPerDay: number
private maxConsecutiveFailsByUsernameAndIp: number

Expand All @@ -19,19 +18,17 @@ export class RateLimiter {
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
)

this.limiterSlowBruteByIP = new RateLimiterMongo({
storeClient: mongoose.connection,
this.limiterSlowBruteByIP = new RateLimiterMemory({
keyPrefix: 'login_fail_ip_per_day',
points: this.maxWrongAttemptsByIpPerDay,
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24 // Block for 1 day
})

this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMongo({
storeClient: mongoose.connection,
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMemory({
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: this.maxConsecutiveFailsByUsernameAndIp,
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
duration: 60 * 60 * 24 * 24, // Store number for 24 days since first fail
blockDuration: 60 * 60 // Block for 1 hour
})
}
Expand Down Expand Up @@ -60,8 +57,7 @@ export class RateLimiter {
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
])

// NOTE: To make use of blockDuration option from RateLimiterMongo
// comparison in both following if statements should have greater than symbol
// NOTE: To make use of blockDuration option, comparison in both following if statements should have greater than symbol
// otherwise, blockDuration option will not work
// For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration

Expand Down Expand Up @@ -103,10 +99,11 @@ export class RateLimiter {
if (rlRejected instanceof Error) {
throw rlRejected
} else {
// based upon the implementation of consume method of RateLimiterMongo
// based upon the implementation of consume method of RateLimiterMemory
// we are sure that rlRejected will contain msBeforeNext
// for further reference,
// see https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection
// or see https://github.com/animir/node-rate-limiter-flexible#ratelimiterres-object
return Math.ceil(rlRejected.msBeforeNext / 1000)
}
}
Expand Down

0 comments on commit 77fac66

Please sign in to comment.