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

Commit a91b84b

Browse files
committed
- Added link to /about
- 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
1 parent 1ebc047 commit a91b84b

11 files changed

Lines changed: 232 additions & 47 deletions

File tree

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ You will need to restart the command line in such case
3838

3939
### Docker Container
4040

41-
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`.
41+
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`
42+
and a production-ready environment via `docker-compose -f docker-compose.yml up --build`.
43+
This environment requires to set the environment variables for all external services.
4244

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

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

5355
```json
5456
{
57+
"default": { // Default behavior of the website, if no SSO flow is used
58+
"branding": { // Allows branding the login page
59+
"backgroundColor": "#f7f9fb", // Page background color
60+
"fontColor": "#888", // Color of the text below the login box
61+
"legalName": "OWASP Foundation", // Legal name displayed below the login box
62+
"privacyPolicy": "https://owasp.org/www-policy/operational/privacy", // Link to privacy policy, mandatory
63+
"imprint": "https://owasp.org/contact/", // Link to legal imprint, optional
64+
"logo": "https://owasp.org/assets/images/logo.png" // Link to logo
65+
},
66+
"syslog": { // Configure a syslog server that will receive audit logs in CEF format, optional
67+
"target": "default-siem", // IP or hostname
68+
"protocol": "tcp" // Protocol
69+
// Check out all parameters at https://cyamato.github.io/SyslogPro/module-SyslogPro-Syslog.html
70+
}
71+
},
5572
"1": { // ID of the website
5673
"jwt": "hello-world", // JWT secret for authentication flow
5774
"signedRequestsOnly": false, // If set to true, only signed login requests are allowed

docker-compose.test.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
version: '3.7'
22

33
services:
4+
# Emulate database
45
database:
56
image: mysql:5
67
volumes:
@@ -13,13 +14,24 @@ services:
1314
MYSQL_USER: owasp_sso
1415
MYSQL_PASSWORD: insecure-default-password
1516

17+
# Database admin
1618
database-admin:
1719
image: adminer
1820
restart: always
1921
ports:
2022
- 8008:8080
2123

24+
# Emulate SMTP
2225
smtp:
2326
image: mailhog/mailhog
2427
restart: always
2528

29+
# Emulate syslog server for the central SIEM
30+
# Run it alone like this: docker run --name rsyslog.service -h test-host -p 514:514 jumanjiman/rsyslog
31+
# You can test payloads like here: https://superuser.com/a/1229424/497745
32+
# It is not enabled by default, as the website does not fully work if the server does not exist.
33+
# You need to enable this manually in to websites.json
34+
default-siem:
35+
image: jumanjiman/rsyslog
36+
hostname: default-siem
37+
restart: always

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ services:
3131
- ARGON2TIME
3232
- ARGON2MEMORY
3333
- AUDITPAGELENGTH
34+
- SYSLOGHEARTBEAT
3435
- FALLBACKEMAILFROM
3536

3637
frontend:

js-backend/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ ARGON2TIME=5
3030
ARGON2MEMORY=200000
3131

3232
AUDITPAGELENGTH=5
33+
SYSLOGHEARTBEAT=60
3334

3435
FALLBACKEMAILFROM=SSO@owasp.org

js-backend/index.js

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require("dotenv").config();
22
const https = require("https");
33
const express = require("express");
4+
const syslogPro = require("syslog-pro");
45
const rateLimit = require("express-rate-limit");
56

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

28+
// Load page settings
2729
const customPages = require("./websites.json");
30+
for (let websiteIndex of Object.keys(customPages)) {
31+
const thisPage = customPages[websiteIndex];
32+
33+
if(thisPage.hasOwnProperty("syslog")) {
34+
// Load syslog handler
35+
// Documentation: https://cyamato.github.io/SyslogPro/module-SyslogPro-Syslog.html
36+
thisPage.syslog = new syslogPro.Syslog(thisPage.syslog);
37+
console.log("Loaded syslog for website key", websiteIndex);
38+
}
39+
}
2840

2941
// Custom classes
42+
const packageList = require("./package.json");
3043
const {DB, User, PwUtil, Audit, Mailer} = require("./utils");
44+
Audit.prepareLoggers(customPages, packageList.version);
3145

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

3852
// Configure Fido2
3953
if(hostname == "localhost" && !disableLocalhostPatching) {
40-
const packageList = require("./package.json");
41-
4254
const utilsLocation = require.resolve("fido2-lib/lib/utils.js");
4355
if(packageList.dependencies["fido2-lib"] == "^2.1.1" && fs.statSync(utilsLocation).size == 7054) {
4456
// 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
@@ -170,7 +182,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
170182
});
171183
});
172184
app.post("/authenticator/delete", isAuthenticated, (req, res, next) => {
173-
Audit.add(req.user.id, getIP(req), "authenticator", "remove", req.body.handle).then(aID => {
185+
Audit.add(req, "authenticator", "remove", req.body.handle).then(aID => {
174186
User.removeAuthenticator(req.body.type, req.user.id, req.body.handle).then(() => {
175187
next();
176188
}).catch(err => {
@@ -182,9 +194,15 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
182194
}, showSuccess);
183195
app.get("/email-confirm", onEmailConfirm, createAuthToken);
184196

185-
// JWT flow
197+
// User SSO flow
186198
app.route("/flow/in").get(onFlowIn, showSuccess).post(onFlowIn, showSuccess);
187199
app.post("/flow/out", isAuthenticated, onFlowOut, showSuccess);
200+
app.get("/default-page", (req, res, next) => {
201+
const defaultPage = customPages["default"];
202+
return res.status(200).json({
203+
branding: defaultPage.branding,
204+
});
205+
});
188206

189207
// SAML
190208
// Test flow: https://samltest.id/start-idp-test/
@@ -277,7 +295,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
277295
app.post("/local/change", onChange, createAuthToken);
278296
app.post("/local/session-clean", isAuthenticated, (req, res, next) => {
279297
const token = req.user.token;
280-
Audit.add(req.user.id, getIP(req), "session", "clean", null).then(() => {
298+
Audit.add(req, "session", "clean", null).then(() => {
281299
User.cleanSession(req.user.id, token).then(() => {
282300
next();
283301
}).catch(err => {
@@ -311,12 +329,12 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
311329
console.error(err);
312330
res.status(404).send("No user with this username/password combination found");
313331
}).then(() => {
314-
return Audit.add(req.user.id, getIP(req), "login", "password", null);
332+
return Audit.add(req, "login", "password", null);
315333
}).then(() => {
316334
req.loginEmail = req.body.username;
317335
next();
318336
}).catch(err => {
319-
//console.error(err)
337+
console.error(err);
320338
res.status(500).send("Internal error during login");
321339
});
322340
}, createLoginToken);
@@ -328,7 +346,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
328346
}), isLoggedIn, (req, res, next) => {
329347
const email = req.user.username;
330348

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

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

412430
}).then(() => {
413431
return User.addAuthenticator("fido2", req.user.username, label, {
@@ -505,7 +523,7 @@ PwUtil.createRandomString(30).then(tempJwtToken => {
505523
};
506524
return Promise.all([
507525
User.updateAuthenticatorCounter("fido2", thisCred.userHandle, returnObj.counter),
508-
Audit.add(userId, getIP(req), "authenticator", "login", thisCred.label + " (" + thisCred.userHandle + ")"),
526+
Audit.add(req, "authenticator", "login", thisCred.label + " (" + thisCred.userHandle + ")"),
509527
]);
510528
}).then(() => {
511529
next();
@@ -642,7 +660,7 @@ function onFlowIn(req, res, next) {
642660
User.findUserByName(email).then(userData => {
643661
req.loginEmail = email;
644662
req.user = userData;
645-
return Audit.add(req.user.id, getIP(req), "page", "request", thisPage.name);
663+
return Audit.add(req, "page", "request", thisPage.name);
646664
}).then(() => {
647665
// Artificially log in as this user
648666
createLoginToken(req, res, next);
@@ -656,7 +674,7 @@ function onFlowIn(req, res, next) {
656674
req.loginEmail = email;
657675
req.user = userData;
658676

659-
return Audit.add(req.user.id, getIP(req), "page", "registration", thisPage.name);
677+
return Audit.add(req, "page", "registration", thisPage.name);
660678
}).then(() => {
661679
createLoginToken(req, res, next);
662680
}).catch(err => {
@@ -685,7 +703,7 @@ function onFlowOut(req, res, next) {
685703
return res.status(400).send("Invalid session JWT");
686704
}
687705

688-
Audit.add(req.user.id, getIP(req), "page", "login", thisPage.name).then(() => {
706+
Audit.add(req, "page", "login", thisPage.name).then(() => {
689707
if(jwtRequest.jwt) {
690708
jwt.sign({
691709
sub: req.user.username,
@@ -694,7 +712,7 @@ function onFlowOut(req, res, next) {
694712
}, thisPage.jwt, {
695713
expiresIn: shortJWTAge,
696714
}, (err, jwtData) => {
697-
Audit.add(req.user.id, getIP(req), "page", "login", thisPage.name).then(() => {
715+
Audit.add(req, "page", "login", thisPage.name).then(() => {
698716
const returnObj = {
699717
redirect: thisPage.redirect,
700718
token: jwtData,
@@ -761,8 +779,6 @@ function onCertLogin(req, res, next) {
761779
//console.log("cert login", cert, req.user)
762780

763781
if(!cert.subject) {
764-
console.log("no subject", req.headers["x-tls-verified"]);
765-
766782
// No direct connection - check header value
767783
if(req.headers.hasOwnProperty("x-tls-verified") && req.headers["x-tls-verified"] == "SUCCESS") {
768784
//console.log("receive certificate via proxy", req.headers["x-tls-cert"]);
@@ -847,7 +863,7 @@ function onCertLogin(req, res, next) {
847863
}
848864

849865
if(!certHandler.webhook || !certHandler.webhook.url) {
850-
return Audit.add(req.user.id, getIP(req), "authenticator", "login", thisPage.name + " certificate").then(() => {
866+
return Audit.add(req, "authenticator", "login", thisPage.name + " certificate").then(() => {
851867
next();
852868
}).catch(err => {
853869
console.error(err);
@@ -868,7 +884,7 @@ function onCertLogin(req, res, next) {
868884
if(!passCertificate) {
869885
return res.status(403).send("Certificate denied by page");
870886
} else {
871-
Audit.add(req.user.id, getIP(req), "authenticator", "login", thisPage.name + " certificate").then(() => {
887+
Audit.add(req, "authenticator", "login", thisPage.name + " certificate").then(() => {
872888
next();
873889
}).catch(err => {
874890
console.error(err);
@@ -902,7 +918,7 @@ function onCertLogin(req, res, next) {
902918
//console.log("allowed fingerprints", fingerprints)
903919
if(cert.fingerprint256 in fingerprints) {
904920
const thisCert = fingerprints[cert.fingerprint256];
905-
Audit.add(req.user.id, getIP(req), "authenticator", "login", thisCert.label + " (" + cert.fingerprint256 + ")").then(() => {
921+
Audit.add(req, "authenticator", "login", thisCert.label + " (" + cert.fingerprint256 + ")").then(() => {
906922
next();
907923
});
908924
} else {
@@ -920,7 +936,7 @@ function onEmailConfirm(req, res, next) {
920936
const token = req.query.token;
921937
const action = req.query.action;
922938

923-
Audit.add(req.user ? req.user.id : null, getIP(req), action, "email", null).then(aID => {
939+
Audit.add(req, action, "email", null).then(aID => {
924940
switch(action) {
925941
default:
926942
return res.status(400).send("Invalid action");
@@ -929,7 +945,7 @@ function onEmailConfirm(req, res, next) {
929945
case "change":
930946
return res.redirect(303, "/password-change.html?" + token);
931947
case "login":
932-
return User.resolveEmailActivation(token, getIP(req), action).then(confirmation => {
948+
return User.resolveEmailActivation(token, Audit.getIP(req), action).then(confirmation => {
933949
next();
934950
}).catch(err => {
935951
res.status(400).send(err);
@@ -943,7 +959,7 @@ function onEmailConfirm(req, res, next) {
943959
function onRegister(req, res, next) {
944960
const email = req.body.email;
945961

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

991+
if(email.indexOf('"') != -1) {
992+
return res.send(500).send("Email address can't be used for generating certificates");
993+
}
994+
975995
// On Windows you can use bash.exe delivered with Git and add it to your PATH environment variable
976996
execFile("bash", [
977997
"-c", "scripts/create-client.bash '"+email+"' '"+email+"'",
@@ -990,7 +1010,7 @@ function onCertRegister(req, res, next) {
9901010
return res.status(500).send("Internal error");
9911011
}
9921012

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

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

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

10701090
let confirmation;
10711091
let userId;
1072-
User.resolveEmailActivation(token, getIP(req), "change", true).then(confirmed => {
1092+
User.resolveEmailActivation(token, Audit.getIP(req), "change", true).then(confirmed => {
10731093
confirmation = confirmed;
10741094
return User.findUserByName(confirmation.username);
10751095
}).then(userData => {
@@ -1092,10 +1112,6 @@ function onChange(req, res, next) {
10921112
});
10931113
}
10941114

1095-
function getIP(req) {
1096-
return req.headers["x-forwarded-for"] || req.connection.remoteAddress;
1097-
}
1098-
10991115
function str2ab(str) {
11001116
const enc = new TextEncoder();
11011117
return enc.encode(str);

js-backend/package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js-backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"nodemailer": "^6.4.4",
3030
"password-validator": "^4.1.3",
3131
"samlp": "^3.4.1",
32+
"syslog-pro": "^1.0.0",
3233
"validator": "^12.2.0"
3334
},
3435
"devDependencies": {

0 commit comments

Comments
 (0)