From 512a69319c00d7978a036f5299245c311bc6dee5 Mon Sep 17 00:00:00 2001 From: Siwat Sirichai Date: Sun, 5 May 2024 16:59:26 +0700 Subject: [PATCH] pre async --- config/database.js | 23 ++++ config/http.js | 32 +++++ config/ldap.js | 32 +++++ config/passport.js | 31 +++-- directory.js | 77 +++++++++++ index.js | 129 ++---------------- login.html | 228 -------------------------------- node_modules/.package-lock.json | 107 +++++++++++++++ package-lock.json | 111 +++++++++++++++- package.json | 6 +- routes/auth.js | 69 ++++++++++ routes/ps_relation_parent.js | 87 ++++++++++++ routes/ps_relation_student.js | 63 +++++++++ 13 files changed, 633 insertions(+), 362 deletions(-) create mode 100644 config/database.js create mode 100644 config/http.js create mode 100644 config/ldap.js create mode 100644 directory.js delete mode 100644 login.html create mode 100644 routes/auth.js create mode 100644 routes/ps_relation_parent.js create mode 100644 routes/ps_relation_student.js 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