summaryrefslogtreecommitdiffstats
path: root/server/auth.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/auth.js')
-rw-r--r--server/auth.js179
1 files changed, 179 insertions, 0 deletions
diff --git a/server/auth.js b/server/auth.js
new file mode 100644
index 0000000..597cf3d
--- /dev/null
+++ b/server/auth.js
@@ -0,0 +1,179 @@
+const basicAuth = require("express-basic-auth");
+const passwordHash = require("./password-hash");
+const { R } = require("redbean-node");
+const { setting } = require("./util-server");
+const { log } = require("../src/util");
+const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
+const { Settings } = require("./settings");
+const dayjs = require("dayjs");
+
+/**
+ * Login to web app
+ * @param {string} username Username to login with
+ * @param {string} password Password to login with
+ * @returns {Promise<(Bean|null)>} User or null if login failed
+ */
+exports.login = async function (username, password) {
+ if (typeof username !== "string" || typeof password !== "string") {
+ return null;
+ }
+
+ let user = await R.findOne("user", " username = ? AND active = 1 ", [
+ username,
+ ]);
+
+ if (user && passwordHash.verify(password, user.password)) {
+ // Upgrade the hash to bcrypt
+ if (passwordHash.needRehash(user.password)) {
+ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
+ passwordHash.generate(password),
+ user.id,
+ ]);
+ }
+ return user;
+ }
+
+ return null;
+};
+
+/**
+ * Validate a provided API key
+ * @param {string} key API key to verify
+ * @returns {boolean} API is ok?
+ */
+async function verifyAPIKey(key) {
+ if (typeof key !== "string") {
+ return false;
+ }
+
+ // uk prefix + key ID is before _
+ let index = key.substring(2, key.indexOf("_"));
+ let clear = key.substring(key.indexOf("_") + 1, key.length);
+
+ let hash = await R.findOne("api_key", " id=? ", [ index ]);
+
+ if (hash === null) {
+ return false;
+ }
+
+ 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 basic auth authorizers
+ * @callback authCallback
+ * @param {any} err Any error encountered
+ * @param {boolean} authorized Is the client authorized?
+ */
+
+/**
+ * Custom authorizer for express-basic-auth
+ * @param {string} username Username to login with
+ * @param {string} password Password to login with
+ * @param {authCallback} callback Callback to handle login result
+ * @returns {void}
+ */
+function apiAuthorizer(username, password, callback) {
+ // API Rate Limit
+ apiRateLimiter.pass(null, 0).then((pass) => {
+ if (pass) {
+ verifyAPIKey(password).then((valid) => {
+ if (!valid) {
+ log.warn("api-auth", "Failed API auth attempt: invalid API Key");
+ }
+ callback(null, valid);
+ // Only allow a set number of api requests per minute
+ // (currently set to 60)
+ apiRateLimiter.removeTokens(1);
+ });
+ } else {
+ log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
+ callback(null, false);
+ }
+ });
+}
+
+/**
+ * Custom authorizer for express-basic-auth
+ * @param {string} username Username to login with
+ * @param {string} password Password to login with
+ * @param {authCallback} callback Callback to handle login result
+ * @returns {void}
+ */
+function userAuthorizer(username, password, callback) {
+ // Login Rate Limit
+ loginRateLimiter.pass(null, 0).then((pass) => {
+ if (pass) {
+ exports.login(username, password).then((user) => {
+ callback(null, user != null);
+
+ if (user == null) {
+ log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
+ loginRateLimiter.removeTokens(1);
+ }
+ });
+ } else {
+ log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
+ callback(null, false);
+ }
+ });
+}
+
+/**
+ * Use basic auth if auth is not disabled
+ * @param {express.Request} req Express request object
+ * @param {express.Response} res Express response object
+ * @param {express.NextFunction} next Next handler in chain
+ * @returns {Promise<void>}
+ */
+exports.basicAuth = async function (req, res, next) {
+ const middleware = basicAuth({
+ authorizer: userAuthorizer,
+ authorizeAsync: true,
+ challenge: true,
+ });
+
+ const disabledAuth = await setting("disableAuth");
+
+ if (!disabledAuth) {
+ middleware(req, res, next);
+ } else {
+ next();
+ }
+};
+
+/**
+ * Use use API Key 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 Next handler in chain
+ * @returns {Promise<void>}
+ */
+exports.apiAuth = async function (req, res, next) {
+ if (!await Settings.get("disableAuth")) {
+ let usingAPIKeys = await Settings.get("apiKeysEnabled");
+ let middleware;
+ if (usingAPIKeys) {
+ middleware = basicAuth({
+ authorizer: apiAuthorizer,
+ authorizeAsync: true,
+ challenge: true,
+ });
+ } else {
+ middleware = basicAuth({
+ authorizer: userAuthorizer,
+ authorizeAsync: true,
+ challenge: true,
+ });
+ }
+ middleware(req, res, next);
+ } else {
+ next();
+ }
+};