diff --git a/config/database.js b/config/database.js
new file mode 100644
index 0000000..e368e4e
--- /dev/null
+++ b/config/database.js
@@ -0,0 +1,23 @@
+
+const mysql = require('mysql2');
+
+// Create a connection pool
+const database = mysql.createPool({
+ host: '192.168.0.236',
+ user: 'cudnodejs',
+ password: 'iDvuHQsPXF5AasESydypgu',
+ database: 'cudnodejs',
+ connectionLimit: 10
+});
+
+database.getConnection((err, connection) => {
+ if(err) {
+ console.error('Error connecting to the database:', err);
+ } else {
+ console.log('Connected to the database');
+ connection.release();
+ }
+});
+
+// Export the connection pool
+module.exports = database;
diff --git a/config/http.js b/config/http.js
new file mode 100644
index 0000000..13de3e4
--- /dev/null
+++ b/config/http.js
@@ -0,0 +1,32 @@
+let fs = require('fs');
+
+const options = {
+ key: fs.readFileSync('adfs_connect/urn_satitm_sso_selfservice.key'),
+ cert: fs.readFileSync('adfs_connect/urn_satitm_sso_selfservice.cert'),
+ ciphers: [
+ 'ECDHE-RSA-AES128-GCM-SHA256',
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
+ 'ECDHE-RSA-AES256-GCM-SHA384',
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
+ 'DHE-RSA-AES128-GCM-SHA256',
+ 'ECDHE-RSA-AES128-SHA256',
+ 'DHE-RSA-AES128-SHA256',
+ 'ECDHE-RSA-AES256-SHA384',
+ 'DHE-RSA-AES256-SHA384',
+ 'ECDHE-RSA-AES256-SHA256',
+ 'DHE-RSA-AES256-SHA256',
+ 'HIGH',
+ '!aNULL',
+ '!eNULL',
+ '!EXPORT',
+ '!DES',
+ '!RC4',
+ '!MD5',
+ '!PSK',
+ '!SRP',
+ '!CAMELLIA'
+ ].join(':'),
+ honorCipherOrder: true
+ };
+
+module.exports.options = options;
\ No newline at end of file
diff --git a/config/ldap.js b/config/ldap.js
new file mode 100644
index 0000000..c060200
--- /dev/null
+++ b/config/ldap.js
@@ -0,0 +1,32 @@
+let ldap = require('ldapjs');
+let fs = require('fs');
+let tls = require('tls');
+
+let satitm_directory = ldap.createClient({
+ url: 'ldaps://ad.satitm.chula.ac.th:636',
+ tlsOptions: {
+ rejectUnauthorized: false
+ }
+});
+
+// Save server's certificate to file for same-host verification
+satitm_directory.on('connect', function(socket) {
+ socket.on('secureConnect', function() {
+ if (socket.getPeerCertificate().raw) {
+ fs.writeFileSync('certificate.pem', socket.getPeerCertificate().raw);
+ satitm_directory.tlsOptions = {
+ ca: [fs.readFileSync('certificate.pem')]
+ };
+ }
+ });
+});
+
+satitm_directory.bind('CN=SSOManager,OU=Service Accounts,DC=ad,DC=satitm,DC=chula,DC=ac,DC=th', '39BK5LCeU2NY2oG3beeBJH', function (err) {
+ if (err) {
+ console.log('Error:', err);
+ } else {
+ console.log('Connected to SATITM Active Directory');
+ }
+});
+
+module.exports = satitm_directory;
\ No newline at end of file
diff --git a/config/passport.js b/config/passport.js
index 6a8f426..eb5b940 100644
--- a/config/passport.js
+++ b/config/passport.js
@@ -1,6 +1,8 @@
-let fs = require("fs"),
- passport = require("passport"),
- SamlStrategy = require("passport-saml").Strategy;
+let fs = require("fs");
+let passport = require("passport");
+let SamlStrategy = require("passport-saml").Strategy;
+let directory = require("../directory.js");
+
passport.serializeUser(function (user, done) {
done(null, user);
});
@@ -13,7 +15,7 @@ passport.use(
{
entryPoint: "https://sso.satitm.chula.ac.th/adfs/ls",
issuer: "https://localhost:3000",
- callbackUrl: "https://localhost:3000/selfservice/activedirectory/postResponse",
+ callbackUrl: "https://localhost:3000/selfservice/api/login/postResponse",
privateKey: fs.readFileSync("adfs_connect/urn_satitm_sso_selfservice.key", "utf-8"),
acceptedClockSkewMs: -1,
identifierFormat: null,
@@ -21,13 +23,20 @@ passport.use(
racComparison: "exact",
},
function (profile, done) {
- console.log("profile", profile);
- let user = profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"];
- return done(null, {
- username: profile["username"],
- first_name: profile["first_name"],
- last_name: profile["last_name"],
- org_unit: profile["org_unit"],
+ // Query Active Directory for user details
+ // username is the UPN
+ // Store the user's group and DN in the session
+ let username = profile["username"];
+ attributes = ["dn", "memberOf"];
+ directory.queryUser(username, attributes, function (err, user) {
+ if (err) {
+ console.log("Error:", err);
+ } else {
+ console.log("User:", user);
+ profile["dn"] = user.dn;
+ profile["memberOf"] = user.memberOf;
+ return done(null, profile);
+ }
});
}
)
diff --git a/directory.js b/directory.js
new file mode 100644
index 0000000..7cb9ec4
--- /dev/null
+++ b/directory.js
@@ -0,0 +1,77 @@
+let satitm_directory = require('./config/ldap.js');
+// Search for a user in the directory
+async function queryUser(upn, attributes) {
+ return new Promise((resolve, reject) => {
+ let opts = {
+ filter: `(userPrincipalName=${upn})`,
+ scope: 'sub',
+ attributes: attributes
+ };
+ satitm_directory.search('DC=ad,DC=satitm,DC=chula,DC=ac,DC=th', opts, function(err, ldapRes) {
+ ldapRes.on('searchEntry', function(entry) {
+ console.log('entry: ' + JSON.stringify(entry.object));
+ resolve(entry.object);
+ });
+ ldapRes.on('error', function(err) {
+ console.error('error: ' + err.message);
+ reject(err);
+ });
+ ldapRes.on('end', function(result) {
+ console.log('status: ' + result.status);
+ });
+ });
+ });
+}
+
+function setAttribute(upn, attribute, value, callback) {
+ // First, get DN of the user from the UPN
+ let attributes = ['dn'];
+
+}
+
+// 0: Unkown, 1: Student, 2: Parent
+const USER_TYPE = {
+ UNKNOWN: 0,
+ STUDENT: 1,
+ PARENT: 2
+};
+
+// Determine the type of user
+// Student is in OU=Students,OU=Users,DC=ad,DC=satitm,DC=chula,DC=ac,DC=th
+// Parent is in OU=Parents,OU=Users,DC=ad,DC=satitm,DC=chula,DC=ac,DC=th
+function getUserType(req, res) {
+ // The user's DN is present in the session as req.user.dn
+ // To convert DN to OU, remove from first CN= to first ,
+ let ou = req.user.dn.substring(req.user.dn.indexOf(',') + 1);
+ console.log('OU:', ou);
+ if (ou === 'OU=Students,DC=ad,DC=satitm,DC=chula,DC=ac,DC=th') {
+ return USER_TYPE.STUDENT;
+ }
+ else if (ou === 'OU=Parents,DC=ad,DC=satitm,DC=chula,DC=ac,DC=th') {
+ return USER_TYPE.PARENT;
+ }
+ else {
+ return USER_TYPE.UNKNOWN;
+ }
+}
+
+async function getPrimaryParent(student_upn, callback) {
+ return new Promise((resolve, reject) => {
+ // Query primaryParent attribute in the student's LDAP entry
+ let attributes = ['primaryParent'];
+ queryUser(student_upn, attributes, function(err, student) {
+ if (err) {
+ reject(err);
+ } else {
+ let primaryParent = student.primaryParent;
+ resolve(primaryParent);
+ }
+ });
+ });
+}
+
+module.exports = {
+ queryUser: queryUser,
+ getUserType: getUserType,
+ USER_TYPE: USER_TYPE
+};
\ No newline at end of file
diff --git a/index.js b/index.js
index 929c685..42d16c9 100644
--- a/index.js
+++ b/index.js
@@ -2,26 +2,13 @@ let passport = require('passport');
let express = require('express');
let https = require('https');
let fs = require('fs');
-let ldap = require('ldapjs');
-
+let directory = require('./directory.js');
+let http_config = require('./config/http.js');
let app = express();
require('./config/passport.js');
let session = require('express-session');
const { group } = require('console');
-let satitm_directory = ldap.createClient({
- url: 'ldap://ad.satitm.chula.ac.th:389'
-});
-
-satitm_directory.bind('CN=SSOManager,OU=Service Accounts,DC=ad,DC=satitm,DC=chula,DC=ac,DC=th', '39BK5LCeU2NY2oG3beeBJH', function (err) {
- if (err) {
- console.log('Error:', err);
- }
- else {
- console.log('Connected to SATITM Active Directory');
- }
-});
-
app.use(session({
secret: 'RLCCDwstDuT6nMJf5kko7C',
resave: false,
@@ -30,115 +17,17 @@ app.use(session({
app.use(passport.initialize());
app.use(passport.session());
-
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
-app.get('/', function (req, res) {
- response = 'Hello World!
';
- console.log('User:', req.user);
- if (req.user) {
- // Query Active Directory for user details
- // username is the UPN
- let username = req.user.username;
- let opts = {
- filter: `(userPrincipalName=${username})`,// replace 'username' with the actual username
- scope: 'sub',
- attributes: ['dn', 'memberOf']
- };
- let groups = '';
- satitm_directory.search('DC=ad,DC=satitm,DC=chula,DC=ac,DC=th', opts, function(err, ldapRes) {
- ldapRes.on('searchEntry', function(entry) {
- console.log('entry: ' + JSON.stringify(entry.object));
- groups = entry.object.memberOf;
- });
- ldapRes.on('error', function(err) {
- console.error('error: ' + err.message);
- });
- ldapRes.on('end', function(result) {
- console.log('status: ' + result.status);
- console.log('User:', req.user);
- response += 'Username: ' + req.user.username + '
';
- response += 'First Name: ' + req.user.first_name + '
';
- response += 'Last Name: ' + req.user.last_name + '
';
- response += 'Group: ' + groups + '
';
- response += 'Logout';
- res.send(response);
- });
- });
+let authRoutes = require('./routes/auth.js');
+app.use('/', authRoutes);
+let psRelationStudentRoutes = require('./routes/ps_relation_student.js');
+app.use('/selfservice/api', psRelationStudentRoutes);
+let psRelationParentRoutes = require('./routes/ps_relation_parent.js');
+app.use('/selfservice/api', psRelationParentRoutes);
-
- }
- else {
- response += 'Login';
- res.send(response);
- }
-});
-
-app.get('/logout', function (req, res) {
- req.logout();
- res.redirect('/');
-});
-
-app.get('/login',
- passport.authenticate('saml', { failureRedirect: '/selfservice', failureFlash: true }),
- function (req, res) {
- res.redirect('https://localhost:3000/');
- }
-);
-
-app.use(function(req, res, next) {
- console.log('Received request:', req.method, req.url);
- console.log('Data:', req.body);
- next();
-});
-
-app.post('/selfservice/activedirectory/postResponse',
- passport.authenticate('saml', { failureRedirect: '/selfservice',successRedirect: '/', failureFlash: true }),
- function (req, res) {
- console.log('SAML authentication successful');
- res.redirect('https://localhost:3000/');
- }
-);
-//app.get('selfservice/secure', validUser, routes.secure);
-
-function validUser(req, res, next) {
- if (!req.user) {
- res.redirect('https://localhost:3000/login');
- }
- next();
-}
-
-const options = {
- key: fs.readFileSync('adfs_connect/urn_satitm_sso_selfservice.key'),
- cert: fs.readFileSync('adfs_connect/urn_satitm_sso_selfservice.cert'),
- ciphers: [
- 'ECDHE-RSA-AES128-GCM-SHA256',
- 'ECDHE-ECDSA-AES128-GCM-SHA256',
- 'ECDHE-RSA-AES256-GCM-SHA384',
- 'ECDHE-ECDSA-AES256-GCM-SHA384',
- 'DHE-RSA-AES128-GCM-SHA256',
- 'ECDHE-RSA-AES128-SHA256',
- 'DHE-RSA-AES128-SHA256',
- 'ECDHE-RSA-AES256-SHA384',
- 'DHE-RSA-AES256-SHA384',
- 'ECDHE-RSA-AES256-SHA256',
- 'DHE-RSA-AES256-SHA256',
- 'HIGH',
- '!aNULL',
- '!eNULL',
- '!EXPORT',
- '!DES',
- '!RC4',
- '!MD5',
- '!PSK',
- '!SRP',
- '!CAMELLIA'
- ].join(':'),
- honorCipherOrder: true
-};
-
-let server = https.createServer(options, app);
+let server = https.createServer(http_config.options, app);
server.listen(3000, function () {
console.log('Listening on port 3000');
});
\ No newline at end of file
diff --git a/login.html b/login.html
deleted file mode 100644
index b02a9ba..0000000
--- a/login.html
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
-
-
-
-
-
-
-
- Firewall Authentication
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json
index 8181152..7c8c59d 100644
--- a/node_modules/.package-lock.json
+++ b/node_modules/.package-lock.json
@@ -171,6 +171,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -346,6 +354,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -458,6 +474,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
+ },
"node_modules/ldap-filter": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
@@ -487,6 +508,19 @@
"node": ">=10.13.0"
}
},
+ "node_modules/long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+ },
+ "node_modules/lru-cache": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
+ "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
+ "engines": {
+ "node": ">=16.14"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -543,6 +577,54 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
+ "node_modules/mysql2": {
+ "version": "3.9.7",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
+ "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
+ "dependencies": {
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.6.3",
+ "long": "^5.2.1",
+ "lru-cache": "^8.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/mysql2/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
+ "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
+ "dependencies": {
+ "lru-cache": "^7.14.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/named-placeholders/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -792,6 +874,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+ },
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@@ -844,6 +931,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -899,6 +994,18 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/package-lock.json b/package-lock.json
index 1eef4a3..d42b2c0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,8 +12,10 @@
"express": "^4.17.1",
"express-session": "^1.17.2",
"ldapjs": "^2.2.3",
+ "mysql2": "^3.9.7",
"passport": "^0.4.1",
- "passport-saml": "^2.0.0"
+ "passport-saml": "^2.0.0",
+ "uuid": "^9.0.1"
}
},
"node_modules/@xmldom/xmldom": {
@@ -183,6 +185,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -358,6 +368,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -470,6 +488,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
+ },
"node_modules/ldap-filter": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
@@ -499,6 +522,19 @@
"node": ">=10.13.0"
}
},
+ "node_modules/long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+ },
+ "node_modules/lru-cache": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
+ "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
+ "engines": {
+ "node": ">=16.14"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -555,6 +591,54 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
+ "node_modules/mysql2": {
+ "version": "3.9.7",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
+ "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
+ "dependencies": {
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.6.3",
+ "long": "^5.2.1",
+ "lru-cache": "^8.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/mysql2/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
+ "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
+ "dependencies": {
+ "lru-cache": "^7.14.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/named-placeholders/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -804,6 +888,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+ },
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@@ -856,6 +945,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -911,6 +1008,18 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/package.json b/package.json
index 4faf9f1..03fc8d5 100644
--- a/package.json
+++ b/package.json
@@ -14,9 +14,11 @@
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
+ "express-session": "^1.17.2",
+ "ldapjs": "^2.2.3",
+ "mysql2": "^3.9.7",
"passport": "^0.4.1",
"passport-saml": "^2.0.0",
- "express-session": "^1.17.2",
- "ldapjs": "^2.2.3"
+ "uuid": "^9.0.1"
}
}
diff --git a/routes/auth.js b/routes/auth.js
new file mode 100644
index 0000000..7f635e6
--- /dev/null
+++ b/routes/auth.js
@@ -0,0 +1,69 @@
+let express = require('express');
+let router = express.Router();
+let passport = require('passport');
+let directory = require('../directory.js');
+
+router.get('/selfservice/api', function (req, res) {
+ response = 'Hello World!
';
+ console.log('User:', req.user);
+ if (req.user) {
+ // Query Active Directory for user details
+ // username is the UPN
+ let username = req.user.username;
+ attributes = ['dn', 'memberOf'];
+ directory.queryUser(username, attributes, function(err, user) {
+ if (err) {
+ console.log('Error:', err);
+ }
+ else {
+ console.log('User:', user);
+ response += 'Username: ' + req.user.username + '
';
+ response += 'First Name: ' + req.user.first_name + '
';
+ response += 'Last Name: ' + req.user.last_name + '
';
+ usertype_str_map = ['Unknown', 'Student', 'Parent'];
+ response += 'User Type: ' + usertype_str_map[directory.getUserType(req, res)] + '
';
+ response += 'Logout';
+ res.send(response);
+ }
+ });
+ }
+ else {
+ response += 'Login';
+ res.send(response);
+ }
+ });
+
+ router.get('/selfservice/api/logout', function (req, res) {
+ req.logout();
+ res.redirect('/selfservice/api');
+ });
+
+ router.get('/selfservice/api/login',
+ passport.authenticate('saml', { failureRedirect: '/selfservice/api', failureFlash: true }),
+ function (req, res) {
+ res.redirect(end);
+ }
+ );
+
+ router.use(function(req, res, next) {
+ console.log('Received request:', req.method, req.url);
+ console.log('Data:', req.body);
+ next();
+ });
+
+ router.post('/selfservice/api/login/postResponse',
+ passport.authenticate('saml', { failureRedirect: '/selfservice/api',successRedirect: '/selfservice/api', failureFlash: true }),
+ function (req, res) {
+ console.log('SAML authentication successful');
+ res.redirect('/selfservice');
+ }
+ );
+
+ function validUser(req, res, next) {
+ if (!req.user) {
+ res.redirect('/api/login');
+ }
+ next();
+ }
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/ps_relation_parent.js b/routes/ps_relation_parent.js
new file mode 100644
index 0000000..688d5c9
--- /dev/null
+++ b/routes/ps_relation_parent.js
@@ -0,0 +1,87 @@
+// This file contains the routes for the the account linking process on the parent side.
+
+let express = require('express');
+let router = express.Router();
+let passport = require('passport');
+let database = require('../config/database.js');
+
+// Consume the pairing code
+// Return the student's UPN then delete the pairing code
+function consumePairingCode(pairing_code, callback) {
+ let sql = 'SELECT upn FROM ps_pairing_codes WHERE pairing_code = ?';
+ database.query(sql, pairing_code, function (err, result) {
+ if (err) {
+ console.log('Error:', err);
+ return callback(err, null);
+ } else {
+ if (result.length === 0) {
+ return callback(null, null);
+ } else {
+ let upn = result[0].upn;
+ let sql = 'DELETE FROM ps_pairing_codes WHERE pairing_code = ?';
+ database.query(sql, pairing_code, function (err, result) {
+ if (err) {
+ console.log('Error:', err);
+ } else {
+ console.log('Pairing code consumed');
+ }
+ });
+ return callback(null, upn);
+ }
+ }
+ });
+}
+
+router.get('/parent/:parent_upn/add-student', function (req, res) {
+ if(!req.isAuthenticated()) {
+ return res.status(401).send('Unauthorized');
+ }
+ let parent_upn = req.params.parent_upn;
+ // Is the logged in user a parent with the same UPN as the one in the URL?
+ // If not, return a 403 Forbidden response
+ if (req.user.username !== parent_upn) {
+ return res.status(403).send('Forbidden, UPN mismatch');
+ }
+ // Consume the pairing code, if it return null, return a 404 Not Found response
+ // Don't update the parent's student list yet
+ // Note that we won't return the student's details in this route
+ // Just a success message
+ let pairing_code = req.query.pairing_code;
+ // Is the pairing code in the query string?
+ if (!pairing_code) {
+ return res.status(400).send('Bad Request, pairing_code missing');
+ }
+ let student_upn = '';
+ consumePairingCode(pairing_code, function (err, upn) {
+ if (err) {
+ return res.status(500).send('Internal Server Error');
+ }
+ if (upn === null) {
+ return res.status(404).send('Invalid pairing code');
+ }
+ student_upn = upn;
+ res.send('Student added');
+ // Set the LDAP attribute parent to the parent's UPN in the student's LDAP entry
+ });
+});
+
+router.get('/parent/:parent_upn', function (req, res) {
+ if(!req.isAuthenticated()) {
+ return res.status(401).send('Unauthorized');
+ }
+ let parent_upn = req.params.parent_upn;
+ // Is the logged in user a parent with the same UPN as the one in the URL?
+ // If not, return a 403 Forbidden response
+ if (req.user.username !== parent_upn) {
+ return res.status(403).send('Forbidden, UPN mismatch');
+ }
+ // Return the parent's details in the session in JSON format
+ allowedAttributes = ['username', 'first_name', 'last_name'];
+ let parent = {};
+ allowedAttributes.forEach(function (attribute) {
+ parent[attribute] = req.user[attribute];
+ });
+ res.json(parent);
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/ps_relation_student.js b/routes/ps_relation_student.js
new file mode 100644
index 0000000..b83704e
--- /dev/null
+++ b/routes/ps_relation_student.js
@@ -0,0 +1,63 @@
+// This file contains the routes for the the account linking process on the student side.
+
+let express = require('express');
+let router = express.Router();
+let passport = require('passport');
+let database = require('../config/database.js');
+let uuid = require('uuid');
+
+function storePairingCode(upn, pairing_code) {
+ // If a student-pairing_code pair already exists, update the pairing code
+ // Else, insert a new student-pairing_code pair
+ let sql = 'INSERT INTO ps_pairing_codes (upn, pairing_code) VALUES (?, ?) ON DUPLICATE KEY UPDATE pairing_code = ?';
+ let values = [upn, pairing_code, pairing_code];
+ database.query(sql, values, function (err, result) {
+ if (err) {
+ console.log('Error:', err);
+ } else {
+ console.log('Pairing code stored');
+ }
+ });
+}
+
+router.get('/student/:upn/pairing-code', function (req, res) {
+ if(!req.isAuthenticated()) {
+ return res.status(401).send('Unauthorized');
+ }
+ let upn = req.params.upn;
+ // Is the logged in user a student with the same UPN as the one in the URL?
+ // If not, return a 403 Forbidden response
+ if (req.user.username !== upn) {
+ console.log('UPN mismatch');
+ console.log('req.user.upn:', req.user.upn);
+ console.log('upn:', upn);
+ return res.status(403).send('Forbidden, UPN mismatch');
+ }
+ // Generate a uuid (v4) as the pairing code
+ let pairing_code = uuid.v4();
+ // Store the pairing code in the database
+ storePairingCode(upn, pairing_code);
+ // Return the pairing code
+ res.send(pairing_code);
+});
+
+router.get('/student/:upn', function (req, res) {
+ if(!req.isAuthenticated()) {
+ return res.status(401).send('Unauthorized');
+ }
+ let upn = req.params.upn;
+ // Is the logged in user a student with the same UPN as the one in the URL?
+ // If not, return a 403 Forbidden response
+ if (req.user.username !== upn) {
+ return res.status(403).send('Forbidden, UPN mismatch');
+ }
+ // Return the student's details in the response in JSON format
+ allowedAttributes = ['username', 'first_name', 'last_name'];
+ let student = {};
+ allowedAttributes.forEach(function (attribute) {
+ student[attribute] = req.user[attribute];
+ });
+ res.json(student);
+});
+
+module.exports = router;
\ No newline at end of file