mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 01:02:05 -07:00
Restored original jwt token handling
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
Common configuration settings.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
apiTokenSecret: process.env.TOKENSECRET ? process.env.TOKENSECRET : 'secret-jwt-token'
|
||||
}
|
||||
@@ -41,7 +41,6 @@
|
||||
"helmet": "^3.21.2",
|
||||
"ioredis": "^4.14.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"key-encoder": "^2.0.3",
|
||||
"level": "^6.0.0",
|
||||
"mkdirp": "^1.0.0",
|
||||
"mocha": "^7.1.1",
|
||||
|
||||
@@ -5,10 +5,9 @@ const RateLimit = require('express-rate-limit')
|
||||
const axios = require('axios')
|
||||
|
||||
const wlogger = require('../util/winston-logging')
|
||||
const config = require('../../config')
|
||||
|
||||
const jwt = require('jsonwebtoken')
|
||||
const KeyEncoder = require('key-encoder').default
|
||||
const keyEncoder = new KeyEncoder('secp256k1')
|
||||
|
||||
// Redis
|
||||
const redisOptions = {
|
||||
@@ -31,8 +30,8 @@ const rateLimitOptions = {
|
||||
|
||||
// This hard-coded value is temporary. It will be swapped out with an environment
|
||||
// variable when moved to production.
|
||||
const publicKey =
|
||||
'03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d'
|
||||
// const publicKey =
|
||||
// '03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d'
|
||||
|
||||
// Set max requests per minute
|
||||
const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS
|
||||
@@ -270,18 +269,19 @@ class RateLimits {
|
||||
|
||||
// Decode the JWT token if one exists.
|
||||
if (req.locals.jwtToken) {
|
||||
const jwtOptions = {
|
||||
algorithms: ['ES256']
|
||||
}
|
||||
// const jwtOptions = {
|
||||
// algorithms: ['ES256']
|
||||
// }
|
||||
|
||||
const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem')
|
||||
// const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem')
|
||||
|
||||
// Validate the JWT token.
|
||||
decoded = _this.jwt.verify(
|
||||
req.locals.jwtToken,
|
||||
pemPublicKey,
|
||||
jwtOptions
|
||||
)
|
||||
// decoded = _this.jwt.verify(
|
||||
// req.locals.jwtToken,
|
||||
// pemPublicKey,
|
||||
// jwtOptions
|
||||
// )
|
||||
decoded = _this.jwt.verify(req.locals.jwtToken, config.apiTokenSecret)
|
||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||
|
||||
userId = decoded.id
|
||||
@@ -398,69 +398,69 @@ class RateLimits {
|
||||
// potentially be used by Bitcoin.com.
|
||||
// Rather than using apiLevel, the rateLimit is explicitly recorded in the
|
||||
// JWT token.
|
||||
async rateLimitSimple (req, res, next) {
|
||||
try {
|
||||
let userId
|
||||
let decoded = {}
|
||||
|
||||
// Create a res.locals object if not passed in.
|
||||
if (!req.locals) {
|
||||
req.locals = {
|
||||
// default values
|
||||
jwtToken: '',
|
||||
proLimit: false,
|
||||
rateLimit: 3
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the JWT token if one exists.
|
||||
if (req.locals.jwtToken) {
|
||||
const jwtOptions = {
|
||||
algorithms: ['ES256']
|
||||
}
|
||||
|
||||
const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem')
|
||||
|
||||
// Validate the JWT token.
|
||||
decoded = _this.jwt.verify(
|
||||
req.locals.jwtToken,
|
||||
pemPublicKey,
|
||||
jwtOptions
|
||||
)
|
||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||
|
||||
userId = decoded.id
|
||||
} else {
|
||||
wlogger.debug('No JWT token found!')
|
||||
}
|
||||
|
||||
// Code here for the rate limiter is adapted from this example:
|
||||
// https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
|
||||
try {
|
||||
// Key for Redis key/value pair.
|
||||
const key = userId || req.ip
|
||||
|
||||
const pointsToConsume = _this.calcPoints2(decoded)
|
||||
|
||||
wlogger.debug(`User ${key} consuming ${pointsToConsume}.`)
|
||||
|
||||
await _this.rateLimiter.consume(key, pointsToConsume)
|
||||
} catch (err) {
|
||||
// console.log(`err: `, err)
|
||||
|
||||
// Rate limited was triggered
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://fullstack.cash`
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
wlogger.error('Error in route-ratelimit.js/rateLimitSimple(): ', err)
|
||||
// throw err
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
// async rateLimitSimple (req, res, next) {
|
||||
// try {
|
||||
// let userId
|
||||
// let decoded = {}
|
||||
//
|
||||
// // Create a res.locals object if not passed in.
|
||||
// if (!req.locals) {
|
||||
// req.locals = {
|
||||
// // default values
|
||||
// jwtToken: '',
|
||||
// proLimit: false,
|
||||
// rateLimit: 3
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Decode the JWT token if one exists.
|
||||
// if (req.locals.jwtToken) {
|
||||
// const jwtOptions = {
|
||||
// algorithms: ['ES256']
|
||||
// }
|
||||
//
|
||||
// const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem')
|
||||
//
|
||||
// // Validate the JWT token.
|
||||
// decoded = _this.jwt.verify(
|
||||
// req.locals.jwtToken,
|
||||
// pemPublicKey,
|
||||
// jwtOptions
|
||||
// )
|
||||
// // console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||
//
|
||||
// userId = decoded.id
|
||||
// } else {
|
||||
// wlogger.debug('No JWT token found!')
|
||||
// }
|
||||
//
|
||||
// // Code here for the rate limiter is adapted from this example:
|
||||
// // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
|
||||
// try {
|
||||
// // Key for Redis key/value pair.
|
||||
// const key = userId || req.ip
|
||||
//
|
||||
// const pointsToConsume = _this.calcPoints2(decoded)
|
||||
//
|
||||
// wlogger.debug(`User ${key} consuming ${pointsToConsume}.`)
|
||||
//
|
||||
// await _this.rateLimiter.consume(key, pointsToConsume)
|
||||
// } catch (err) {
|
||||
// // console.log(`err: `, err)
|
||||
//
|
||||
// // Rate limited was triggered
|
||||
// res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
// return res.json({
|
||||
// error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://fullstack.cash`
|
||||
// })
|
||||
// }
|
||||
// } catch (err) {
|
||||
// wlogger.error('Error in route-ratelimit.js/rateLimitSimple(): ', err)
|
||||
// // throw err
|
||||
// }
|
||||
//
|
||||
// next()
|
||||
// }
|
||||
|
||||
// Calculates the points consumed, based on the explicit rateLimit defined
|
||||
// in the JWT token.
|
||||
|
||||
@@ -618,185 +618,6 @@ describe('#route-ratelimits & jwt-auth', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#rateLimitSimple', () => {
|
||||
it('should pass through rate-limit middleware', async () => {
|
||||
req.baseUrl = '/v3'
|
||||
req.path = '/control/getNetworkInfo'
|
||||
req.url = req.path
|
||||
req.method = 'GET'
|
||||
|
||||
// Call the route twice to trigger the rate handling.
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
|
||||
// next() will be called if rate-limit is not triggered
|
||||
assert.equal(next.called, true)
|
||||
})
|
||||
|
||||
it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => {
|
||||
req.baseUrl = '/v3'
|
||||
req.path = '/control/getNetworkInfo'
|
||||
req.url = req.path
|
||||
req.method = 'GET'
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
// console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
'next should not be called if rate limit was triggered.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should NOT trigger rate-limit for free-tier at 5 RPM', async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = '/v3'
|
||||
req.path = '/control/getNetworkInfo'
|
||||
req.url = req.path
|
||||
req.method = 'GET'
|
||||
|
||||
req.locals.jwtToken = 'some-token'
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 10,
|
||||
id: '5e3a0415eb29a962da2708c1'
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
// console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
true,
|
||||
'next should be called if rate limit was not triggered.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should trigger rate-limit for free tier after 10 RPM', async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = '/v3'
|
||||
req.path = '/control/getNetworkInfo'
|
||||
req.url = req.path
|
||||
req.method = 'GET'
|
||||
|
||||
req.locals.jwtToken = 'some-token'
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 10,
|
||||
id: '5e3a0415eb29a962da2708c2'
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
// console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
'next should not be called if rate limit was triggered.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should NOT trigger rate-limit handler for pro-tier at 25 RPM', async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = '/v3'
|
||||
req.path = '/control/getNetworkInfo'
|
||||
req.url = req.path
|
||||
req.method = 'GET'
|
||||
|
||||
req.locals.jwtToken = 'some-token'
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 100,
|
||||
id: '5e3a0415eb29a962da2708c3'
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
// console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
true,
|
||||
'next should be called if rate limit was not triggered.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should still rate-limit at a higher RPM for pro-tier', async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = '/v3'
|
||||
req.path = '/control/getNetworkInfo'
|
||||
req.url = req.path
|
||||
req.method = 'GET'
|
||||
|
||||
req.locals.jwtToken = 'some-token'
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 100,
|
||||
id: '5e3a0415eb29a962da2708c4'
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 150; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
// console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
'next should NOT be called if rate limit was triggered.'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Generates a Basic authorization header.
|
||||
|
||||
Reference in New Issue
Block a user