mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
feat(tiers): Enforcing tiered access to endpoints
This commit is contained in:
+2
-1
@@ -75,7 +75,8 @@ const auth = new AuthMW()
|
||||
app.use(`/${v3prefix}/`, auth.mw())
|
||||
|
||||
// Rate limit on all v3 routes
|
||||
app.use(`/${v3prefix}/`, routeRateLimit)
|
||||
app.use(`/${v3prefix}/`, routeRateLimit) // Establish and enforce rate limits.
|
||||
app.use(`/${v3prefix}/`, jwtAuth.routeAccess) // Enforce access tiers.
|
||||
app.use(`/${v3prefix}/` + `health-check`, healthCheckV3)
|
||||
app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router)
|
||||
app.use(`/${v3prefix}/` + `control`, controlV3.router)
|
||||
|
||||
@@ -10,12 +10,9 @@
|
||||
// req.locals.jwtToken property.
|
||||
const getTokenFromHeaders = (req, res, next) => {
|
||||
try {
|
||||
// console.log(`getTokenFromHeaders2: Searching headers for a JWT token.`)
|
||||
|
||||
// console.log(`req.headers: `, req.headers)
|
||||
|
||||
// If the authorization header exists.
|
||||
// Only executes if the authorization header exists.
|
||||
if (req.headers.authorization) {
|
||||
// Retrieve the auth string from the header object.
|
||||
const authStr = req.headers.authorization
|
||||
|
||||
// If the header is proceeded by the word 'Token'
|
||||
@@ -31,18 +28,67 @@ const getTokenFromHeaders = (req, res, next) => {
|
||||
proLimit: false,
|
||||
apiLevel: 0
|
||||
}
|
||||
} else {
|
||||
req.locals.jwtToken = token
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error in getTokenFromHeaders2: `, err)
|
||||
console.log(`Error in getTokenFromHeaders: `, err)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// This middleware analyizes the combination of JWT token and apiLevel. It will
|
||||
// stop users from accessing routes they have not paid for.
|
||||
//
|
||||
// It is assumed this middleware is run AFTER the route-ratelimit.js middleware,
|
||||
// so that the req.locals.apiLevel property has been populated.
|
||||
const routeAccess = (req, res, next) => {
|
||||
try {
|
||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
||||
console.log(`req.url: `, req.url)
|
||||
|
||||
const locals = req.locals
|
||||
const url = req.url
|
||||
|
||||
const level20Routes = ["insight", "bitcore", "blockbook"]
|
||||
|
||||
// JWT token is included in header.
|
||||
if (!!locals.jwtToken && locals.jwtToken !== "") {
|
||||
// API Level is 0 (free tier): do nothing. All endpoints are open to
|
||||
// free public access within the drastically limited rate limits.
|
||||
|
||||
if (locals.apiLevel === 10) {
|
||||
// API level is 10 (full node tier)
|
||||
|
||||
// Loop through the routes that are not accessible to this tier.
|
||||
for (let i = 0; i < level20Routes.length; i++) {
|
||||
// If the requested route is for a higher tier, return a 403.
|
||||
if (url.indexOf(level20Routes[i]) > -1) {
|
||||
res.status(403)
|
||||
return res.json({
|
||||
error:
|
||||
"route is not accessible for your JWT tier. Upgrade or use anonymous access."
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// else if (locals.apiLevel > 11) {
|
||||
// // API level is 20 (indexer tier)
|
||||
// }
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error in routeAccess: `, err)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
const jwtAuth = {
|
||||
getTokenFromHeaders
|
||||
getTokenFromHeaders,
|
||||
routeAccess
|
||||
}
|
||||
|
||||
module.exports = jwtAuth
|
||||
|
||||
+101
-3
@@ -14,11 +14,15 @@ const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks")
|
||||
// Libraries under test
|
||||
let rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
const controlRoute = require("../../src/routes/v3/full-node/control")
|
||||
const jwtAuth = require("../../src/middleware/jwt-auth")
|
||||
|
||||
let req, res, next
|
||||
let originalEnvVars // Used during transition from integration to unit tests.
|
||||
|
||||
describe("#route-ratelimits", () => {
|
||||
// JWT token used in tests.
|
||||
const jwt = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ`
|
||||
|
||||
describe("#route-ratelimits & jwt-auth", () => {
|
||||
before(() => {
|
||||
// Save existing environment variables.
|
||||
originalEnvVars = {
|
||||
@@ -35,8 +39,8 @@ describe("#route-ratelimits", () => {
|
||||
// Setup the mocks before each test.
|
||||
beforeEach(() => {
|
||||
// Mock the req and res objects used by Express routes.
|
||||
req = mockReq
|
||||
res = mockRes
|
||||
req = Object.assign({}, mockReq)
|
||||
res = Object.assign({}, mockRes)
|
||||
next = mockNext
|
||||
|
||||
// Explicitly reset the parmas and body.
|
||||
@@ -45,6 +49,100 @@ describe("#route-ratelimits", () => {
|
||||
req.query = {}
|
||||
})
|
||||
|
||||
describe("#jwt-auth.js", () => {
|
||||
describe("#getTokenFromHeaders", () => {
|
||||
it(`should populate the req.locals object correctly`, () => {
|
||||
// Initialize req.locals
|
||||
req.locals = {
|
||||
proLimit: false,
|
||||
apiLevel: 0
|
||||
}
|
||||
|
||||
const header = `Token ${jwt}`
|
||||
req.headers.authorization = header
|
||||
|
||||
jwtAuth.getTokenFromHeaders(req, res, next)
|
||||
|
||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
||||
|
||||
assert.property(req.locals, "proLimit")
|
||||
assert.property(req.locals, "apiLevel")
|
||||
assert.property(req.locals, "jwtToken")
|
||||
assert.equal(req.locals.jwtToken, jwt)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#routeAccess", () => {
|
||||
it("should do nothing if req.locals.jwtToken is undefined", () => {
|
||||
// Initialize req.locals
|
||||
req.locals = {
|
||||
proLimit: false,
|
||||
apiLevel: 0
|
||||
}
|
||||
req.url = "/insight/address/details"
|
||||
|
||||
// Reset the history of the stub and assert it has not been called.
|
||||
res.status.resetHistory()
|
||||
assert.equal(res.status.called, false, "stub history reset")
|
||||
|
||||
jwtAuth.routeAccess(req, res, next)
|
||||
|
||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
||||
assert.equal(
|
||||
res.status.called,
|
||||
false,
|
||||
"stub should NOT have been called."
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if full-node tier tries to access indexer", () => {
|
||||
// Initialize req.locals
|
||||
req.locals = {
|
||||
proLimit: true,
|
||||
apiLevel: 10,
|
||||
jwtToken: jwt
|
||||
}
|
||||
req.url = "/insight/address/details"
|
||||
|
||||
try {
|
||||
res.status.resetHistory()
|
||||
assert.equal(res.status.called, false, "stub history reset")
|
||||
|
||||
jwtAuth.routeAccess(req, res, next)
|
||||
|
||||
assert.equal(res.status.called, true, "stub should have been called.")
|
||||
} catch (err) {
|
||||
console.log(`caught error: `, err)
|
||||
}
|
||||
})
|
||||
|
||||
it("should allow indexer tier tries access indexer endpoints", () => {
|
||||
// Initialize req.locals
|
||||
req.locals = {
|
||||
proLimit: true,
|
||||
apiLevel: 20,
|
||||
jwtToken: jwt
|
||||
}
|
||||
req.url = "/insight/address/details"
|
||||
|
||||
try {
|
||||
res.status.resetHistory()
|
||||
assert.equal(res.status.called, false, "stub history reset")
|
||||
|
||||
jwtAuth.routeAccess(req, res, next)
|
||||
|
||||
assert.equal(
|
||||
res.status.called,
|
||||
false,
|
||||
"stub should NOT have been called."
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(`caught error: `, err)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#routeRateLimit", () => {
|
||||
let routeRateLimit = rateLimitMiddleware.routeRateLimit
|
||||
const getInfo = controlRoute.testableComponents.getInfo
|
||||
|
||||
Reference in New Issue
Block a user