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 <mnickson@sidingsmedia.com>
pull/2558/head
Matthew Nickson 2 years ago
parent cd796898d0
commit e7feca1cd6
No known key found for this signature in database
GPG Key ID: BF229DCFD4748E05

@ -3,6 +3,8 @@ 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 } = require("./rate-limiter");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
/** /**
* Login to web app * Login to web app
@ -33,6 +35,32 @@ exports.login = async function (username, password) {
return null; return null;
}; };
/**
* Validate a provided API key
* @param {string} key API Key passed by client
* @returns {Promise<bool>}
*/
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 for myAuthorizer
* @callback myAuthorizerCB * @callback myAuthorizerCB
@ -84,3 +112,36 @@ exports.basicAuth = async function (req, res, next) {
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();
}
};

@ -87,7 +87,7 @@ log.debug("server", "Importing Background Jobs");
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs"); const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter"); const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
const { basicAuth } = require("./auth"); const { apiAuth } = require("./auth");
const { login } = require("./auth"); const { login } = require("./auth");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
@ -230,7 +230,7 @@ let needSetup = false;
// Prometheus API metrics /metrics // Prometheus API metrics /metrics
// With Basic Auth using the first user's username/password // With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics()); app.get("/metrics", apiAuth, prometheusAPIMetrics());
app.use("/", expressStaticGzip("dist", { app.use("/", expressStaticGzip("dist", {
enableBrotli: true, enableBrotli: true,

@ -5,6 +5,7 @@ const crypto = require("crypto");
const passwordHash = require("../password-hash"); const passwordHash = require("../password-hash");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const APIKey = require("../model/api_key"); const APIKey = require("../model/api_key");
const { Settings } = require("../settings");
const { sendAPIKeyList } = require("../client"); const { sendAPIKeyList } = require("../client");
/** /**
@ -29,6 +30,10 @@ module.exports.apiKeySocketHandler = (socket) => {
let formattedKey = bean.id + "-" + clearKey; let formattedKey = bean.id + "-" + clearKey;
await sendAPIKeyList(socket); 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({ callback({
ok: true, ok: true,
msg: "Added Successfully.", msg: "Added Successfully.",

Loading…
Cancel
Save