Switched to using Authorization header

Prometheus doesn't support using custom headers for exporters, however
it does support using the Authorisation header with basic auth. As
such, we switched from using X-API-Key to Authorization with the basic
scheme and an empty username field.

Also added a rate limit for API endpoints of 60 requests in a minute

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
This commit is contained in:
Matthew Nickson 2023-02-15 21:53:49 +00:00
parent 1d4af39820
commit b8720b46c3
No known key found for this signature in database
GPG key ID: BF229DCFD4748E05
2 changed files with 53 additions and 28 deletions

View file

@ -2,7 +2,7 @@ const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { loginRateLimiter } = require("./rate-limiter");
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
@ -37,10 +37,9 @@ exports.login = async function (username, password) {
/**
* Validate a provided API key
* @param {string} key API Key passed by client
* @returns {Promise<bool>}
* @param {string} key API key to verify
*/
async function validateAPIKey(key) {
async function verifyAPIKey(key) {
if (typeof key !== "string") {
return false;
}
@ -64,8 +63,8 @@ async function validateAPIKey(key) {
}
/**
* Callback for myAuthorizer
* @callback myAuthorizerCB
* Callback for basic auth authorizers
* @callback authCallback
* @param {any} err Any error encountered
* @param {boolean} authorized Is the client authorized?
*/
@ -74,9 +73,31 @@ async function validateAPIKey(key) {
* Custom authorizer for express-basic-auth
* @param {string} username
* @param {string} password
* @param {myAuthorizerCB} callback
* @param {authCallback} callback
*/
function myAuthorizer(username, password, callback) {
function apiAuthorizer(username, password, callback) {
// API Rate Limit
apiRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
verifyAPIKey(password).then((valid) => {
callback(null, valid);
// Only allow a set number of api requests per minute
// (currently set to 60)
apiRateLimiter.removeTokens(1);
});
} else {
callback(null, false);
}
});
}
/**
* Custom authorizer for express-basic-auth
* @param {string} username
* @param {string} password
* @param {authCallback} callback
*/
function userAuthorizer(username, password, callback) {
// Login Rate Limit
loginRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
@ -101,7 +122,7 @@ function myAuthorizer(username, password, callback) {
*/
exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({
authorizer: myAuthorizer,
authorizer: userAuthorizer,
authorizeAsync: true,
challenge: true,
});
@ -124,25 +145,21 @@ exports.basicAuth = async function (req, res, next) {
exports.apiAuth = async function (req, res, next) {
if (!await Settings.get("disableAuth")) {
let usingAPIKeys = await Settings.get("apiKeysEnabled");
loginRateLimiter.pass(null, 0).then((pass) => {
let middleware;
if (usingAPIKeys) {
let pwd = req.get("X-API-Key");
if (pwd !== null && pwd !== undefined) {
validateAPIKey(pwd).then((valid) => {
if (valid) {
next();
} else {
res.status(401).send();
}
middleware = basicAuth({
authorizer: apiAuthorizer,
authorizeAsync: true,
challenge: true,
});
} else {
res.status(401).send();
}
} else {
exports.basicAuth(req, res, next);
}
middleware = basicAuth({
authorizer: userAuthorizer,
authorizeAsync: true,
challenge: true,
});
}
middleware(req, res, next);
} else {
next();
}

View file

@ -54,6 +54,13 @@ const loginRateLimiter = new KumaRateLimiter({
errorMessage: "Too frequently, try again later."
});
const apiRateLimiter = new KumaRateLimiter({
tokensPerInterval: 60,
interval: "minute",
fireImmediately: true,
errorMessage: "Too frequently, try again later."
});
const twoFaRateLimiter = new KumaRateLimiter({
tokensPerInterval: 30,
interval: "minute",
@ -63,5 +70,6 @@ const twoFaRateLimiter = new KumaRateLimiter({
module.exports = {
loginRateLimiter,
apiRateLimiter,
twoFaRateLimiter,
};