• auth.js
  • 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 = ? ", [
                    await 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();
        }
    };