Skip to content
This repository has been archived by the owner on Feb 11, 2022. It is now read-only.

Commit

Permalink
- Added link to /about
Browse files Browse the repository at this point in the history
- Added CEF syslog logging for SIEM
- Fixed endless redirect bug
- Rewrote audit to automatically log to the correct syslog destinations
- Moved default page to backend, so that it can be easier customized
  • Loading branch information
JamesCullum committed Mar 29, 2020
1 parent 1ebc047 commit a91b84b
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 47 deletions.
19 changes: 18 additions & 1 deletion README.md
Expand Up @@ -38,7 +38,9 @@ You will need to restart the command line in such case

### Docker Container

You can use the `docker-compose.yml` file to start a production-ready environment via `docker-compose up -f docker-compose.yml -f docker-compose.test.yml --build`.
You can use the `docker-compose.yml` file to start a test environment via `docker-compose -f docker-compose.yml -f docker-compose.test.yml up --build`
and a production-ready environment via `docker-compose -f docker-compose.yml up --build`.
This environment requires to set the environment variables for all external services.

In the future, all components will be available to be directly pulled from the registry.

Expand All @@ -52,6 +54,21 @@ Below you find a sample configuration.

```json
{
"default": { // Default behavior of the website, if no SSO flow is used
"branding": { // Allows branding the login page
"backgroundColor": "#f7f9fb", // Page background color
"fontColor": "#888", // Color of the text below the login box
"legalName": "OWASP Foundation", // Legal name displayed below the login box
"privacyPolicy": "https://owasp.org/www-policy/operational/privacy", // Link to privacy policy, mandatory
"imprint": "https://owasp.org/contact/", // Link to legal imprint, optional
"logo": "https://owasp.org/assets/images/logo.png" // Link to logo
},
"syslog": { // Configure a syslog server that will receive audit logs in CEF format, optional
"target": "default-siem", // IP or hostname
"protocol": "tcp" // Protocol
// Check out all parameters at https://cyamato.github.io/SyslogPro/module-SyslogPro-Syslog.html
}
},
"1": { // ID of the website
"jwt": "hello-world", // JWT secret for authentication flow
"signedRequestsOnly": false, // If set to true, only signed login requests are allowed
Expand Down
12 changes: 12 additions & 0 deletions docker-compose.test.yml
@@ -1,6 +1,7 @@
version: '3.7'

services:
# Emulate database
database:
image: mysql:5
volumes:
Expand All @@ -13,13 +14,24 @@ services:
MYSQL_USER: owasp_sso
MYSQL_PASSWORD: insecure-default-password

# Database admin
database-admin:
image: adminer
restart: always
ports:
- 8008:8080

# Emulate SMTP
smtp:
image: mailhog/mailhog
restart: always

# Emulate syslog server for the central SIEM
# Run it alone like this: docker run --name rsyslog.service -h test-host -p 514:514 jumanjiman/rsyslog
# You can test payloads like here: https://superuser.com/a/1229424/497745
# It is not enabled by default, as the website does not fully work if the server does not exist.
# You need to enable this manually in to websites.json
default-siem:
image: jumanjiman/rsyslog
hostname: default-siem
restart: always
1 change: 1 addition & 0 deletions docker-compose.yml
Expand Up @@ -31,6 +31,7 @@ services:
- ARGON2TIME
- ARGON2MEMORY
- AUDITPAGELENGTH
- SYSLOGHEARTBEAT
- FALLBACKEMAILFROM

frontend:
Expand Down
1 change: 1 addition & 0 deletions js-backend/.env
Expand Up @@ -30,5 +30,6 @@ ARGON2TIME=5
ARGON2MEMORY=200000

AUDITPAGELENGTH=5
SYSLOGHEARTBEAT=60

FALLBACKEMAILFROM=SSO@owasp.org
76 changes: 46 additions & 30 deletions js-backend/index.js
@@ -1,6 +1,7 @@
require("dotenv").config();
const https = require("https");
const express = require("express");
const syslogPro = require("syslog-pro");
const rateLimit = require("express-rate-limit");

const { execFile, execFileSync } = require("child_process");
Expand All @@ -24,10 +25,23 @@ PassportProfileMapper.prototype.getClaims = function() {
};
};

// 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 {DB, User, PwUtil, Audit, Mailer} = require("./utils");
Audit.prepareLoggers(customPages, packageList.version);

const expressPort = process.env.BACKENDPORT || 3000;
const frontendPort = process.env.FRONTENDPORT || 8080;
Expand All @@ -37,8 +51,6 @@ const emailFrom = process.env.SMTPUSER || process.env.FALLBACKEMAILFROM;

// Configure Fido2
if(hostname == "localhost" && !disableLocalhostPatching) {
const packageList = require("./package.json");

const utilsLocation = require.resolve("fido2-lib/lib/utils.js");
if(packageList.dependencies["fido2-lib"] == "^2.1.1" && fs.statSync(utilsLocation).size == 7054) {
// FIDO2-Lib does not natively support localhost and due to little maintenance this issue hasn't been fixed yet. See https://github.com/apowers313/fido2-lib/pull/19/files
Expand Down Expand Up @@ -170,7 +182,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
});
});
app.post("/authenticator/delete", isAuthenticated, (req, res, next) => {
Audit.add(req.user.id, getIP(req), "authenticator", "remove", req.body.handle).then(aID => {
Audit.add(req, "authenticator", "remove", req.body.handle).then(aID => {
User.removeAuthenticator(req.body.type, req.user.id, req.body.handle).then(() => {
next();
}).catch(err => {
Expand All @@ -182,9 +194,15 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
}, showSuccess);
app.get("/email-confirm", onEmailConfirm, createAuthToken);

// JWT flow
// User SSO flow
app.route("/flow/in").get(onFlowIn, showSuccess).post(onFlowIn, showSuccess);
app.post("/flow/out", isAuthenticated, onFlowOut, showSuccess);
app.get("/default-page", (req, res, next) => {
const defaultPage = customPages["default"];
return res.status(200).json({
branding: defaultPage.branding,
});
});

// SAML
// Test flow: https://samltest.id/start-idp-test/
Expand Down Expand Up @@ -277,7 +295,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
app.post("/local/change", onChange, createAuthToken);
app.post("/local/session-clean", isAuthenticated, (req, res, next) => {
const token = req.user.token;
Audit.add(req.user.id, getIP(req), "session", "clean", null).then(() => {
Audit.add(req, "session", "clean", null).then(() => {
User.cleanSession(req.user.id, token).then(() => {
next();
}).catch(err => {
Expand Down Expand Up @@ -311,12 +329,12 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
console.error(err);
res.status(404).send("No user with this username/password combination found");
}).then(() => {
return Audit.add(req.user.id, getIP(req), "login", "password", null);
return Audit.add(req, "login", "password", null);
}).then(() => {
req.loginEmail = req.body.username;
next();
}).catch(err => {
//console.error(err)
console.error(err);
res.status(500).send("Internal error during login");
});
}, createLoginToken);
Expand All @@ -328,7 +346,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
}), isLoggedIn, (req, res, next) => {
const email = req.user.username;

User.requestEmailActivation(email, getIP(req), "login").then(token => {
User.requestEmailActivation(email, Audit.getIP(req), "login").then(token => {
Mailer.sendMail({
from: "OWASP Single Sign-On <"+emailFrom+">",
to: email,
Expand Down Expand Up @@ -407,7 +425,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
publicKey: authnrData.get("credentialPublicKeyPem"),
};

return Audit.add(userId, getIP(req), "authenticator", "add", label + " ("+credId+")");
return Audit.add(req, "authenticator", "add", label + " ("+credId+")");

}).then(() => {
return User.addAuthenticator("fido2", req.user.username, label, {
Expand Down Expand Up @@ -505,7 +523,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
};
return Promise.all([
User.updateAuthenticatorCounter("fido2", thisCred.userHandle, returnObj.counter),
Audit.add(userId, getIP(req), "authenticator", "login", thisCred.label + " (" + thisCred.userHandle + ")"),
Audit.add(req, "authenticator", "login", thisCred.label + " (" + thisCred.userHandle + ")"),
]);
}).then(() => {
next();
Expand Down Expand Up @@ -642,7 +660,7 @@ function onFlowIn(req, res, next) {
User.findUserByName(email).then(userData => {
req.loginEmail = email;
req.user = userData;
return Audit.add(req.user.id, getIP(req), "page", "request", thisPage.name);
return Audit.add(req, "page", "request", thisPage.name);
}).then(() => {
// Artificially log in as this user
createLoginToken(req, res, next);
Expand All @@ -656,7 +674,7 @@ function onFlowIn(req, res, next) {
req.loginEmail = email;
req.user = userData;

return Audit.add(req.user.id, getIP(req), "page", "registration", thisPage.name);
return Audit.add(req, "page", "registration", thisPage.name);
}).then(() => {
createLoginToken(req, res, next);
}).catch(err => {
Expand Down Expand Up @@ -685,7 +703,7 @@ function onFlowOut(req, res, next) {
return res.status(400).send("Invalid session JWT");
}

Audit.add(req.user.id, getIP(req), "page", "login", thisPage.name).then(() => {
Audit.add(req, "page", "login", thisPage.name).then(() => {
if(jwtRequest.jwt) {
jwt.sign({
sub: req.user.username,
Expand All @@ -694,7 +712,7 @@ function onFlowOut(req, res, next) {
}, thisPage.jwt, {
expiresIn: shortJWTAge,
}, (err, jwtData) => {
Audit.add(req.user.id, getIP(req), "page", "login", thisPage.name).then(() => {
Audit.add(req, "page", "login", thisPage.name).then(() => {
const returnObj = {
redirect: thisPage.redirect,
token: jwtData,
Expand Down Expand Up @@ -761,8 +779,6 @@ function onCertLogin(req, res, next) {
//console.log("cert login", cert, req.user)

if(!cert.subject) {
console.log("no subject", req.headers["x-tls-verified"]);

// No direct connection - check header value
if(req.headers.hasOwnProperty("x-tls-verified") && req.headers["x-tls-verified"] == "SUCCESS") {
//console.log("receive certificate via proxy", req.headers["x-tls-cert"]);
Expand Down Expand Up @@ -847,7 +863,7 @@ function onCertLogin(req, res, next) {
}

if(!certHandler.webhook || !certHandler.webhook.url) {
return Audit.add(req.user.id, getIP(req), "authenticator", "login", thisPage.name + " certificate").then(() => {
return Audit.add(req, "authenticator", "login", thisPage.name + " certificate").then(() => {
next();
}).catch(err => {
console.error(err);
Expand All @@ -868,7 +884,7 @@ function onCertLogin(req, res, next) {
if(!passCertificate) {
return res.status(403).send("Certificate denied by page");
} else {
Audit.add(req.user.id, getIP(req), "authenticator", "login", thisPage.name + " certificate").then(() => {
Audit.add(req, "authenticator", "login", thisPage.name + " certificate").then(() => {
next();
}).catch(err => {
console.error(err);
Expand Down Expand Up @@ -902,7 +918,7 @@ function onCertLogin(req, res, next) {
//console.log("allowed fingerprints", fingerprints)
if(cert.fingerprint256 in fingerprints) {
const thisCert = fingerprints[cert.fingerprint256];
Audit.add(req.user.id, getIP(req), "authenticator", "login", thisCert.label + " (" + cert.fingerprint256 + ")").then(() => {
Audit.add(req, "authenticator", "login", thisCert.label + " (" + cert.fingerprint256 + ")").then(() => {
next();
});
} else {
Expand All @@ -920,7 +936,7 @@ function onEmailConfirm(req, res, next) {
const token = req.query.token;
const action = req.query.action;

Audit.add(req.user ? req.user.id : null, getIP(req), action, "email", null).then(aID => {
Audit.add(req, action, "email", null).then(aID => {
switch(action) {
default:
return res.status(400).send("Invalid action");
Expand All @@ -929,7 +945,7 @@ function onEmailConfirm(req, res, next) {
case "change":
return res.redirect(303, "/password-change.html?" + token);
case "login":
return User.resolveEmailActivation(token, getIP(req), action).then(confirmation => {
return User.resolveEmailActivation(token, Audit.getIP(req), action).then(confirmation => {
next();
}).catch(err => {
res.status(400).send(err);
Expand All @@ -943,7 +959,7 @@ function onEmailConfirm(req, res, next) {
function onRegister(req, res, next) {
const email = req.body.email;

User.requestEmailActivation(email, getIP(req), "registration").then(token => {
User.requestEmailActivation(email, Audit.getIP(req), "registration").then(token => {
Mailer.sendMail({
from: "OWASP Single Sign-On <"+emailFrom+">",
to: email,
Expand Down Expand Up @@ -972,6 +988,10 @@ function onCertRegister(req, res, next) {
const email = req.user.username;
const label = req.body.label;

if(email.indexOf('"') != -1) {
return res.send(500).send("Email address can't be used for generating certificates");
}

// On Windows you can use bash.exe delivered with Git and add it to your PATH environment variable
execFile("bash", [
"-c", "scripts/create-client.bash '"+email+"' '"+email+"'",
Expand All @@ -990,7 +1010,7 @@ function onCertRegister(req, res, next) {
return res.status(500).send("Internal error");
}

Audit.add(req.user.id, getIP(req), "authenticator", "add", label+" ("+certData.fingerprint256+")").then(() => {
Audit.add(req, "authenticator", "add", label+" ("+certData.fingerprint256+")").then(() => {
res.download(certPath, "client-certificate.p12", async err => {
//console.log("res.download", err)
fs.unlink(certPath, err => {
Expand All @@ -1015,7 +1035,7 @@ function onCertRegister(req, res, next) {
function onChangeRequest(req, res, next) {
const email = req.body.email;

User.requestEmailActivation(email, getIP(req), "change").then(token => {
User.requestEmailActivation(email, Audit.getIP(req), "change").then(token => {
Mailer.sendMail({
from: "OWASP Single Sign-On <"+emailFrom+">",
to: email,
Expand Down Expand Up @@ -1052,7 +1072,7 @@ function onActivate(req, res, next) {
const password = req.body.password;

PwUtil.checkPassword(null, password).then(() => {
return User.resolveEmailActivation(token, getIP(req), "registration");
return User.resolveEmailActivation(token, Audit.getIP(req), "registration");
}).then(confirmation => {
req.body.username = confirmation.username;
req.body.password = password;
Expand All @@ -1069,7 +1089,7 @@ function onChange(req, res, next) {

let confirmation;
let userId;
User.resolveEmailActivation(token, getIP(req), "change", true).then(confirmed => {
User.resolveEmailActivation(token, Audit.getIP(req), "change", true).then(confirmed => {
confirmation = confirmed;
return User.findUserByName(confirmation.username);
}).then(userData => {
Expand All @@ -1092,10 +1112,6 @@ function onChange(req, res, next) {
});
}

function getIP(req) {
return req.headers["x-forwarded-for"] || req.connection.remoteAddress;
}

function str2ab(str) {
const enc = new TextEncoder();
return enc.encode(str);
Expand Down
15 changes: 15 additions & 0 deletions js-backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions js-backend/package.json
Expand Up @@ -29,6 +29,7 @@
"nodemailer": "^6.4.4",
"password-validator": "^4.1.3",
"samlp": "^3.4.1",
"syslog-pro": "^1.0.0",
"validator": "^12.2.0"
},
"devDependencies": {
Expand Down

0 comments on commit a91b84b

Please sign in to comment.