mirror of
https://github.com/sasjs/server.git
synced 2026-01-05 05:40:06 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c44ec35b3d | ||
|
|
77fac663c5 | ||
| 7df9588e66 | |||
| 6a520f5b26 | |||
|
|
70c3834022 | ||
|
|
dbf6c7de08 | ||
|
|
d49ea47bd7 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
|||||||
|
## [0.33.3](https://github.com/sasjs/server/compare/v0.33.2...v0.33.3) (2023-04-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* use RateLimiterMemory instead of RateLimiterMongo ([6a520f5](https://github.com/sasjs/server/commit/6a520f5b26a3e2ed6345721b30ff4e3d9bfa903d))
|
||||||
|
|
||||||
|
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
|
||||||
|
|
||||||
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
|
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
|
|||||||
|
|
||||||
|
|
||||||
# After this, access is blocked for an hour
|
# 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
|
# Once a successful login is attempted, it resets
|
||||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ export const createSASProgram = async (
|
|||||||
%mend;
|
%mend;
|
||||||
%_sasjs_server_init()
|
%_sasjs_server_init()
|
||||||
|
|
||||||
proc printto print="%sysfunc(getoption(log))";
|
|
||||||
run;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
|
|||||||
@@ -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', () => {
|
describe('SASLogon/login', () => {
|
||||||
let csrfToken: string
|
let csrfToken: string
|
||||||
|
|
||||||
@@ -187,78 +258,6 @@ describe('web', () => {
|
|||||||
expect(res.body).toEqual({})
|
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) => {
|
const getCSRF = async (app: Express) => {
|
||||||
@@ -285,12 +284,3 @@ const extractCSRF = (text: string) =>
|
|||||||
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
||||||
text
|
text
|
||||||
)![1]
|
)![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({})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import mongoose from 'mongoose'
|
import { RateLimiterMemory } from 'rate-limiter-flexible'
|
||||||
import { RateLimiterMongo } from 'rate-limiter-flexible'
|
|
||||||
|
|
||||||
export class RateLimiter {
|
export class RateLimiter {
|
||||||
private static instance: RateLimiter
|
private static instance: RateLimiter
|
||||||
private limiterSlowBruteByIP: RateLimiterMongo
|
private limiterSlowBruteByIP: RateLimiterMemory
|
||||||
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMongo
|
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMemory
|
||||||
private maxWrongAttemptsByIpPerDay: number
|
private maxWrongAttemptsByIpPerDay: number
|
||||||
private maxConsecutiveFailsByUsernameAndIp: number
|
private maxConsecutiveFailsByUsernameAndIp: number
|
||||||
|
|
||||||
@@ -19,19 +18,17 @@ export class RateLimiter {
|
|||||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
)
|
)
|
||||||
|
|
||||||
this.limiterSlowBruteByIP = new RateLimiterMongo({
|
this.limiterSlowBruteByIP = new RateLimiterMemory({
|
||||||
storeClient: mongoose.connection,
|
|
||||||
keyPrefix: 'login_fail_ip_per_day',
|
keyPrefix: 'login_fail_ip_per_day',
|
||||||
points: this.maxWrongAttemptsByIpPerDay,
|
points: this.maxWrongAttemptsByIpPerDay,
|
||||||
duration: 60 * 60 * 24,
|
duration: 60 * 60 * 24,
|
||||||
blockDuration: 60 * 60 * 24 // Block for 1 day
|
blockDuration: 60 * 60 * 24 // Block for 1 day
|
||||||
})
|
})
|
||||||
|
|
||||||
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMongo({
|
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMemory({
|
||||||
storeClient: mongoose.connection,
|
|
||||||
keyPrefix: 'login_fail_consecutive_username_and_ip',
|
keyPrefix: 'login_fail_consecutive_username_and_ip',
|
||||||
points: this.maxConsecutiveFailsByUsernameAndIp,
|
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
|
blockDuration: 60 * 60 // Block for 1 hour
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -60,8 +57,7 @@ export class RateLimiter {
|
|||||||
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
||||||
])
|
])
|
||||||
|
|
||||||
// NOTE: To make use of blockDuration option from RateLimiterMongo
|
// NOTE: To make use of blockDuration option, comparison in both following if statements should have greater than symbol
|
||||||
// comparison in both following if statements should have greater than symbol
|
|
||||||
// otherwise, blockDuration option will not work
|
// otherwise, blockDuration option will not work
|
||||||
// For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration
|
// For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration
|
||||||
|
|
||||||
@@ -103,10 +99,11 @@ export class RateLimiter {
|
|||||||
if (rlRejected instanceof Error) {
|
if (rlRejected instanceof Error) {
|
||||||
throw rlRejected
|
throw rlRejected
|
||||||
} else {
|
} 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
|
// we are sure that rlRejected will contain msBeforeNext
|
||||||
// for further reference,
|
// for further reference,
|
||||||
// see https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection
|
// 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)
|
return Math.ceil(rlRejected.msBeforeNext / 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user