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

View File

@ -54,6 +54,13 @@ const loginRateLimiter = new KumaRateLimiter({
errorMessage: "Too frequently, try again later." 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({ const twoFaRateLimiter = new KumaRateLimiter({
tokensPerInterval: 30, tokensPerInterval: 30,
interval: "minute", interval: "minute",
@ -63,5 +70,6 @@ const twoFaRateLimiter = new KumaRateLimiter({
module.exports = { module.exports = {
loginRateLimiter, loginRateLimiter,
apiRateLimiter,
twoFaRateLimiter, twoFaRateLimiter,
}; };