js-backend/index.js
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);
}