pre async

This commit is contained in:
Siwat Sirichai 2024-05-05 16:59:26 +07:00
parent f7824d67e5
commit 512a69319c
13 changed files with 633 additions and 362 deletions

23
config/database.js Normal file
View file

@ -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;

32
config/http.js Normal file
View file

@ -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;

32
config/ldap.js Normal file
View file

@ -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;

View file

@ -1,6 +1,8 @@
let fs = require("fs"), let fs = require("fs");
passport = require("passport"), let passport = require("passport");
SamlStrategy = require("passport-saml").Strategy; let SamlStrategy = require("passport-saml").Strategy;
let directory = require("../directory.js");
passport.serializeUser(function (user, done) { passport.serializeUser(function (user, done) {
done(null, user); done(null, user);
}); });
@ -13,7 +15,7 @@ passport.use(
{ {
entryPoint: "https://sso.satitm.chula.ac.th/adfs/ls", entryPoint: "https://sso.satitm.chula.ac.th/adfs/ls",
issuer: "https://localhost:3000", 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"), privateKey: fs.readFileSync("adfs_connect/urn_satitm_sso_selfservice.key", "utf-8"),
acceptedClockSkewMs: -1, acceptedClockSkewMs: -1,
identifierFormat: null, identifierFormat: null,
@ -21,13 +23,20 @@ passport.use(
racComparison: "exact", racComparison: "exact",
}, },
function (profile, done) { function (profile, done) {
console.log("profile", profile); // Query Active Directory for user details
let user = profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"]; // username is the UPN
return done(null, { // Store the user's group and DN in the session
username: profile["username"], let username = profile["username"];
first_name: profile["first_name"], attributes = ["dn", "memberOf"];
last_name: profile["last_name"], directory.queryUser(username, attributes, function (err, user) {
org_unit: profile["org_unit"], if (err) {
console.log("Error:", err);
} else {
console.log("User:", user);
profile["dn"] = user.dn;
profile["memberOf"] = user.memberOf;
return done(null, profile);
}
}); });
} }
) )

77
directory.js Normal file
View file

@ -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
};

129
index.js
View file

@ -2,26 +2,13 @@ let passport = require('passport');
let express = require('express'); let express = require('express');
let https = require('https'); let https = require('https');
let fs = require('fs'); let fs = require('fs');
let ldap = require('ldapjs'); let directory = require('./directory.js');
let http_config = require('./config/http.js');
let app = express(); let app = express();
require('./config/passport.js'); require('./config/passport.js');
let session = require('express-session'); let session = require('express-session');
const { group } = require('console'); 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({ app.use(session({
secret: 'RLCCDwstDuT6nMJf5kko7C', secret: 'RLCCDwstDuT6nMJf5kko7C',
resave: false, resave: false,
@ -30,115 +17,17 @@ app.use(session({
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.get('/', function (req, res) { let authRoutes = require('./routes/auth.js');
response = 'Hello World!<br>'; app.use('/', authRoutes);
console.log('User:', req.user); let psRelationStudentRoutes = require('./routes/ps_relation_student.js');
if (req.user) { app.use('/selfservice/api', psRelationStudentRoutes);
// Query Active Directory for user details let psRelationParentRoutes = require('./routes/ps_relation_parent.js');
// username is the UPN app.use('/selfservice/api', psRelationParentRoutes);
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 + '<br>';
response += 'First Name: ' + req.user.first_name + '<br>';
response += 'Last Name: ' + req.user.last_name + '<br>';
response += 'Group: ' + groups + '<br>';
response += '<a href="/logout">Logout</a>';
res.send(response);
});
});
let server = https.createServer(http_config.options, app);
}
else {
response += '<a href="/login">Login</a>';
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);
server.listen(3000, function () { server.listen(3000, function () {
console.log('Listening on port 3000'); console.log('Listening on port 3000');
}); });

View file

@ -1,228 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=8; IE=EDGE">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
body {
height: 100%;
font-family: Helvetica, Arial, sans-serif;
color: #6a6a6a;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
background: url(https://sso.satitm.chula.ac.th/img/bg_landscape.png) no-repeat;
background-size: cover;
overflow: hidden;
}
.form-container {
background-color: rgba(255, 255, 255, 0.8);
padding: 20px;
border-radius: 10px;
}
input[type=date],
input[type=email],
input[type=number],
input[type=password],
input[type=search],
input[type=tel],
input[type=text],
input[type=time],
input[type=url],
select,
textarea {
color: #262626;
vertical-align: baseline;
margin: .2em;
border-style: solid;
border-width: 1px;
border-color: #a9a9a9;
background-color: #fff;
box-sizing: border-box;
padding: 2px .5em;
appearance: none;
border-radius: 0;
}
input:focus {
border-color: #646464;
box-shadow: 0 0 1px 0 #a2a2a2;
outline: 0;
}
button {
padding: .5em 1em;
border: 1px solid;
border-radius: 3px;
min-width: 6em;
font-weight: 400;
font-size: .8em;
cursor: pointer;
}
button.primary {
color: #fff;
background-color: rgb(47, 113, 178);
border-color: rgb(34, 103, 173);
}
.form-body {
display: flex;
margin-left: auto;
margin-right: auto;
align-items: center;
align-content: center;
}
.message-container {
height: 1280px;
width: 1280px;
padding: 0;
margin: 10px;
}
.logo {
height: 800px;
object-fit: contain;
}
table {
background-color: #fff;
border-spacing: 0;
margin: 1em;
}
table>tbody>tr>td:first-of-type:not([colspan]) {
white-space: nowrap;
color: rgba(0, 0, 0, .5);
}
table>tbody>tr>td:first-of-type {
vertical-align: top;
}
table>tbody>tr>td {
padding: .3em .3em;
}
.field {
display: table-row;
}
.field> :first-child {
display: table-cell;
width: 20%;
}
.field.single> :first-child {
display: inline;
}
.field> :not(:first-child) {
width: auto;
max-width: 100%;
display: inline-flex;
align-items: baseline;
virtical-align: top;
box-sizing: border-box;
margin: .3em;
}
.field> :not(:first-child)>input {
width: 230px;
}
.form-footer {
display: inline-flex;
justify-content: flex-start;
}
.form-footer>* {
margin: 1em;
}
.text-scrollable {
overflow: auto;
height: 150px;
border: 1px solid rgb(200, 200, 200);
padding: 5px;
font-size: 1em;
}
.text-centered {
text-align: center;
}
.text-container {
margin: 1em 1.5em;
}
.flex-container {
display: flex;
}
.flex-container.column {
flex-direction: column;
}
</style>
<title>
Firewall Authentication
</title>
</head>
<body>
<div class="message-container">
<div class="logo">
<div class="container">
<div class="form-body">
<div class="form-container">
<h1 style="text-align: center; margin-top: 0;">
SATITM SSO
</h1>
<form action="%%AUTH_POST_URL%%" method="post">
<input type="hidden" name="%%REDIRID%%" value="%%PROTURI%%">
<input type="hidden" name="%%MAGICID%%" value="%%MAGICVAL%%">
<input type="hidden" name="%%METHODID%%" value="%%METHODVAL%%">
<p>
%%QUESTION%%
</p>
<div class="field">
<label for="ft_un">
Email
</label>
<div>
<input name="%%USERNAMEID%%" id="ft_un" type="text" autocorrect="off" autocapitalize="off">
</div>
</div>
<div class="field">
<label for="ft_pd">
Password
</label>
<div>
<input name="%%PASSWORDID%%" id="ft_pd" type="password" autocomplete="off">
</div>
</div>
<div class="form-footer">
<button class="primary" type="submit" style="margin-bottom: 0;">
Continue
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

107
node_modules/.package-lock.json generated vendored
View file

@ -171,6 +171,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -346,6 +354,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@ -458,6 +474,11 @@
"node": ">= 0.10" "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": { "node_modules/ldap-filter": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
@ -487,6 +508,19 @@
"node": ">=10.13.0" "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": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "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", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "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": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "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", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "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": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@ -844,6 +931,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -899,6 +994,18 @@
"node": ">= 0.4.0" "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": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

111
package-lock.json generated
View file

@ -12,8 +12,10 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"ldapjs": "^2.2.3", "ldapjs": "^2.2.3",
"mysql2": "^3.9.7",
"passport": "^0.4.1", "passport": "^0.4.1",
"passport-saml": "^2.0.0" "passport-saml": "^2.0.0",
"uuid": "^9.0.1"
} }
}, },
"node_modules/@xmldom/xmldom": { "node_modules/@xmldom/xmldom": {
@ -183,6 +185,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -358,6 +368,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@ -470,6 +488,11 @@
"node": ">= 0.10" "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": { "node_modules/ldap-filter": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
@ -499,6 +522,19 @@
"node": ">=10.13.0" "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": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "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", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "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": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "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", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "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": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@ -856,6 +945,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -911,6 +1008,18 @@
"node": ">= 0.4.0" "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": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -14,9 +14,11 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.2",
"ldapjs": "^2.2.3",
"mysql2": "^3.9.7",
"passport": "^0.4.1", "passport": "^0.4.1",
"passport-saml": "^2.0.0", "passport-saml": "^2.0.0",
"express-session": "^1.17.2", "uuid": "^9.0.1"
"ldapjs": "^2.2.3"
} }
} }

69
routes/auth.js Normal file
View file

@ -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!<br>';
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 + '<br>';
response += 'First Name: ' + req.user.first_name + '<br>';
response += 'Last Name: ' + req.user.last_name + '<br>';
usertype_str_map = ['Unknown', 'Student', 'Parent'];
response += 'User Type: ' + usertype_str_map[directory.getUserType(req, res)] + '<br>';
response += '<a href="/selfservice/api/logout">Logout</a>';
res.send(response);
}
});
}
else {
response += '<a href="/selfservice/api/login">Login</a>';
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;

View file

@ -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;

View file

@ -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;