OWASP/SSO_Project

View on GitHub
js-backend/index.js

Summary

Maintainability
A
0 mins
Test Coverage
require("dotenv").config();
const https = require("https");
const express = require("express");
const syslogPro = require("syslog-pro");
const pki = require("node-forge").pki;

const { execFileSync } = require("child_process");
const fs = require("fs");

// Load page settings
const customPages = require("./websites.json");
for (let websiteIndex of Object.keys(customPages)) {
    const thisPage = customPages[websiteIndex];
    
    if(thisPage.hasOwnProperty("syslog")) {
        // Load syslog handler
        // Documentation: https://cyamato.github.io/SyslogPro/module-SyslogPro-Syslog.html
        thisPage.syslog = new syslogPro.Syslog(thisPage.syslog);
        console.log("Loaded syslog for website key", websiteIndex);
    }
}

// Custom classes
const packageList = require("./package.json");
const {User, PwUtil, Audit, JWT, Mailer} = require("./utils");
Audit.prepareLoggers(customPages, packageList.version);

const expressPort = process.env.BACKENDPORT || 3000;
const frontendPort = process.env.FRONTENDPORT || 8080;
const hostname = process.env.DOMAIN || "localhost";

// Configure Fido2
const fido2Options = {
    protocol: "https",
    rpId: hostname,
    rpName: process.env.ISSUERNAME || "OWASP SSO",
    attestation: "none",
    factor: process.env.FIDO2FACTOR || "either",
    timeout: parseInt(process.env.FIDO2TIMEOUT) || 15*60,
    protocol: "https",
    origin: frontendPort==443 ? "https://" + hostname : "https://" + hostname + ":" + frontendPort,
};

// Entrypoint
const app = express();
let ownJwtToken = null;

// Create keys if they don't exist (not mounted/first start)
const keyPath = "keys";
if(!fs.existsSync(keyPath+"/server_key.pem")) {
    const createServerCert = execFileSync("bash", [
        "-c", "scripts/setup.bash '"+fido2Options.rpName+"'",
    ]);
    console.log("Server keys have been generated");
}

let serverInstance;
const serverKey = fs.readFileSync(keyPath+"/server_key.pem");
const serverCrt = fs.readFileSync(keyPath+"/server_cert.pem");

// Preprocess CA list
const caList = [serverCrt], caMap = {};
caMap["native"] = pki.createCaStore([ serverCrt.toString() ]);
const caFolder = keyPath+"/ca";
const caFiles = fs.readdirSync(caFolder);
caFiles.forEach(caFile => {
    if(caFile.endsWith(".pem")) {
        const readCa = fs.readFileSync(caFolder+"/"+caFile);
        caMap[caFile] = pki.createCaStore([ readCa.toString() ]);
        caList.push(readCa);
    }
});
console.log(caList.length + " CA loaded");
bundleCAs(caList);

PwUtil.createRandomString(30).then(tempJwtToken => {
    ownJwtToken = tempJwtToken;
    if(hostname == "localhost") ownJwtToken = "hello-friend";
    process.env.UNIQUEJWTTOKEN = ownJwtToken;
    
    // Rate limitation middleware
    app.set("trust proxy", "loopback, 172.16.0.0/12");
    
    // Security headers
    app.disable("x-powered-by");
    app.use((req, res, next) => {
        // Security headers
        res.removeHeader("X-Powered-By");
        
        res.set({
            "X-Content-Type-Options": "nosniff",
            "Referrer-Policy": "strict-origin",
            "Feature-Policy": "default 'none'",
            "Content-Security-Policy": "default 'none'",
            "X-XSS-Protection": "1; mode=block",
        });
        
        if(hostname != "localhost") {
            res.set({
                "Strict-Transport-Security": "Strict-Transport-Security: max-age=31536000",
                "X-Frame-Options": "SAMEORIGIN",
            });
        } else {
            res.set({
                "Access-Control-Allow-Origin": "https://"+hostname+":"+frontendPort,
                "Access-Control-Allow-Headers": "content-type, authorization, x-sso-token",
                "Access-Control-Allow-Methods": "GET, POST",
            });
        }
        
        next();
    });
    
    // Parser middleware
    app.use(require("body-parser").urlencoded({ extended: true }));
    app.use(express.json());
    
    const MiddlewareHelper = new (require("./utils/middleware.js").MiddlewareHelper)(User.db);
    app.use(MiddlewareHelper.parseAuthHeader.bind(MiddlewareHelper));
    app.use(MiddlewareHelper.rateLimit(5, 500, "Too many generic requests, please try again later."));
    
    // Flow loader to separate functionalities
    const FlowLoader = require("./flows").FlowLoader;
    const flowLoader = new FlowLoader(fido2Options, customPages, caMap, serverCrt, serverKey);
    flowLoader.addRoutes(app);
    
    // Start webserver
    if(hostname == "localhost") {
        console.log("Starting API webserver on https://localhost:"+expressPort);
    } else {
        console.log("Starting API webserver for production via internal port "+expressPort);
    }
    
    const httpsOpts = {
        key: serverKey,
        cert: serverCrt,
        requestCert: true,
        rejectUnauthorized: false,
        ca: caList,
    };
    serverInstance = https.createServer(httpsOpts, app).listen(expressPort);
});

// The nginx project requires a single PEM file for all CAs to be used for client certificates
// As this application generates and refreshes it, it makes most sense to bundle them here and mount the file to the other host
// This also protects against the typical issues of mounting volumes if files still exist
// It gets called after loading the files from the constructor anyways and will monitor if new CAs pop up
function bundleCAs(caList) {
    fs.writeFile(keyPath+"/bundled-ca.pem", caList.join(""), "utf8", err => {
        if(err) {
            console.error("Writing bundled CA failed", err);
        }
    });
    
    const numInitialCustomCAs = caList.length - 1;
    setInterval(() => {
        // Monitor for new CAs
        const numCurrentCustomCAs = fs.readdirSync(keyPath+"/ca").length;
        if(numCurrentCustomCAs != numInitialCustomCAs) {
            console.warn("Number of custom CAs has changed from", numInitialCustomCAs, "to", numCurrentCustomCAs);
            // Exit application for automated restart, loading new CAs
            
            require("process").exit(205);
        }
    }, 15 * 60 * 1000);
}