From e7feca1cd661b2215e4c08aadd7985f988b145b8 Mon Sep 17 00:00:00 2001 From: Matthew Nickson Date: Wed, 15 Feb 2023 00:39:29 +0000 Subject: [PATCH] Added API key authentication handler API key authentication is now possible by making use of the X-API-Key header. API authentication will only be enabled when a user adds their first API key, up until this point, they can still use their username and password to authenticate with API endpoints. After the user adds their first API key, they may only use API keys in future to authenticate with the API. In this commit, the prometheus /metrics endpoint has been changed over to the new authentication system. Signed-off-by: Matthew Nickson --- server/auth.js | 61 +++++++++++++++++++ server/server.js | 4 +- .../socket-handlers/api-key-socket-handler.js | 5 ++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/server/auth.js b/server/auth.js index fd19b0e44..f2c59d66f 100644 --- a/server/auth.js +++ b/server/auth.js @@ -3,6 +3,8 @@ const passwordHash = require("./password-hash"); const { R } = require("redbean-node"); const { setting } = require("./util-server"); const { loginRateLimiter } = require("./rate-limiter"); +const { Settings } = require("./settings"); +const dayjs = require("dayjs"); /** * Login to web app @@ -33,6 +35,32 @@ exports.login = async function (username, password) { return null; }; +/** + * Validate a provided API key + * @param {string} key API Key passed by client + * @returns {Promise} + */ +async function validateAPIKey(key) { + if (typeof key !== "string") { + return false; + } + + let index = key.substring(0, key.indexOf("-")); + let clear = key.substring(key.indexOf("-") + 1, key.length); + console.log(index); + console.log(clear); + + let hash = await R.findOne("api_key", " id=? ", [ index ]); + + let current = dayjs(); + let expiry = dayjs(hash.expires); + if (expiry.diff(current) < 0, !hash.active) { + return false; + } + + return hash && passwordHash.verify(clear, hash.key); +} + /** * Callback for myAuthorizer * @callback myAuthorizerCB @@ -84,3 +112,36 @@ exports.basicAuth = async function (req, res, next) { next(); } }; + +/** + * Use X-API-Key header if API keys enabled, else use basic auth + * @param {express.Request} req Express request object + * @param {express.Response} res Express response object + * @param {express.NextFunction} 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) => { + 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(); + } + }); + } else { + res.status(401).send(); + } + } else { + exports.basicAuth(req, res, next); + } + }); + } else { + next(); + } +}; diff --git a/server/server.js b/server/server.js index 183a5bb38..8696aa709 100644 --- a/server/server.js +++ b/server/server.js @@ -87,7 +87,7 @@ log.debug("server", "Importing Background Jobs"); const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs"); const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter"); -const { basicAuth } = require("./auth"); +const { apiAuth } = require("./auth"); const { login } = require("./auth"); const passwordHash = require("./password-hash"); @@ -230,7 +230,7 @@ let needSetup = false; // Prometheus API metrics /metrics // With Basic Auth using the first user's username/password - app.get("/metrics", basicAuth, prometheusAPIMetrics()); + app.get("/metrics", apiAuth, prometheusAPIMetrics()); app.use("/", expressStaticGzip("dist", { enableBrotli: true, diff --git a/server/socket-handlers/api-key-socket-handler.js b/server/socket-handlers/api-key-socket-handler.js index a80dca830..cf124cad3 100644 --- a/server/socket-handlers/api-key-socket-handler.js +++ b/server/socket-handlers/api-key-socket-handler.js @@ -5,6 +5,7 @@ const crypto = require("crypto"); const passwordHash = require("../password-hash"); const apicache = require("../modules/apicache"); const APIKey = require("../model/api_key"); +const { Settings } = require("../settings"); const { sendAPIKeyList } = require("../client"); /** @@ -29,6 +30,10 @@ module.exports.apiKeySocketHandler = (socket) => { let formattedKey = bean.id + "-" + clearKey; await sendAPIKeyList(socket); + // Enable API auth if the user creates a key, otherwise only basic + // auth will be used for API. + await Settings.set("apiKeysEnabled", true); + callback({ ok: true, msg: "Added Successfully.",