mbkauthe 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/PACKAGE.md +1 -1
- package/.github/workflows/publish.yml +35 -2
- package/README.md +3 -3
- package/docs/api.md +70 -4
- package/env.md +1 -1
- package/lib/main.js +285 -105
- package/lib/pool.js +2 -2
- package/lib/validateSessionAndRole.js +22 -47
- package/package.json +6 -3
- package/public/bg.avif +0 -0
- package/public/main.js +5 -7
- package/views/2fa.handlebars +6 -6
- package/views/Error/dError.handlebars +3 -3
- package/views/info.handlebars +13 -10
- package/views/loginmbkauthe.handlebars +52 -14
- package/views/sharedStyles.handlebars +1 -1
- package/views/showmessage.handlebars +1 -1
package/lib/main.js
CHANGED
|
@@ -5,14 +5,14 @@ import session from "express-session";
|
|
|
5
5
|
import pgSession from "connect-pg-simple";
|
|
6
6
|
const PgSession = pgSession(session);
|
|
7
7
|
import { dblogin } from "./pool.js";
|
|
8
|
-
import { authenticate } from "./validateSessionAndRole.js";
|
|
8
|
+
import { authenticate, validateSession, validateSessionAndRole } from "./validateSessionAndRole.js";
|
|
9
9
|
import fetch from 'node-fetch';
|
|
10
10
|
import cookieParser from "cookie-parser";
|
|
11
11
|
import bcrypt from 'bcrypt';
|
|
12
12
|
import rateLimit from 'express-rate-limit';
|
|
13
13
|
import speakeasy from "speakeasy";
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import passport from 'passport';
|
|
15
|
+
import GitHubStrategy from 'passport-github2';
|
|
16
16
|
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
import fs from "fs";
|
|
@@ -36,16 +36,26 @@ router.get('/mbkauthe/main.js', (req, res) => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
// CSRF protection middleware
|
|
39
|
-
const csrfProtection = csurf({ cookie:
|
|
39
|
+
const csrfProtection = csurf({ cookie: true });
|
|
40
40
|
|
|
41
41
|
// CORS and security headers
|
|
42
42
|
router.use((req, res, next) => {
|
|
43
43
|
const origin = req.headers.origin;
|
|
44
|
-
if (origin
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if (origin) {
|
|
45
|
+
try {
|
|
46
|
+
const originUrl = new URL(origin);
|
|
47
|
+
const allowedDomain = `.${mbkautheVar.DOMAIN}`;
|
|
48
|
+
// Exact match or subdomain match (must end with .domain.com, not just domain.com)
|
|
49
|
+
if (originUrl.hostname === mbkautheVar.DOMAIN ||
|
|
50
|
+
(originUrl.hostname.endsWith(allowedDomain) && originUrl.hostname.charAt(originUrl.hostname.length - allowedDomain.length - 1) !== '.')) {
|
|
51
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
52
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
53
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
54
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Invalid origin URL, skip CORS headers
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
next();
|
|
51
61
|
});
|
|
@@ -71,6 +81,12 @@ const TwoFALimit = rateLimit({
|
|
|
71
81
|
message: { success: false, message: "Too many 2FA attempts, please try again later" }
|
|
72
82
|
});
|
|
73
83
|
|
|
84
|
+
const GitHubOAuthLimit = rateLimit({
|
|
85
|
+
windowMs: 5 * 60 * 1000,
|
|
86
|
+
max: 10,
|
|
87
|
+
message: "Too many GitHub login attempts, please try again later"
|
|
88
|
+
});
|
|
89
|
+
|
|
74
90
|
const sessionConfig = {
|
|
75
91
|
store: new PgSession({
|
|
76
92
|
pool: dblogin,
|
|
@@ -85,7 +101,7 @@ const sessionConfig = {
|
|
|
85
101
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
86
102
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
87
103
|
httpOnly: true,
|
|
88
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
104
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
89
105
|
sameSite: 'lax',
|
|
90
106
|
path: '/'
|
|
91
107
|
},
|
|
@@ -100,14 +116,16 @@ router.use(async (req, res, next) => {
|
|
|
100
116
|
try {
|
|
101
117
|
const sessionId = req.cookies.sessionId;
|
|
102
118
|
|
|
103
|
-
// Validate sessionId format (should be 64 hex characters)
|
|
119
|
+
// Validate sessionId format (should be 64 hex characters) and normalize to lowercase
|
|
104
120
|
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
105
121
|
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
106
122
|
return next();
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
const
|
|
110
|
-
|
|
125
|
+
const normalizedSessionId = sessionId.toLowerCase();
|
|
126
|
+
|
|
127
|
+
const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE LOWER("SessionId") = $1 AND "Active" = true`;
|
|
128
|
+
const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [normalizedSessionId] });
|
|
111
129
|
|
|
112
130
|
if (result.rows.length > 0) {
|
|
113
131
|
const user = result.rows[0];
|
|
@@ -117,8 +135,7 @@ router.use(async (req, res, next) => {
|
|
|
117
135
|
UserName: user.UserName,
|
|
118
136
|
role: user.Role,
|
|
119
137
|
Role: user.Role,
|
|
120
|
-
sessionId,
|
|
121
|
-
allowedApps: user.AllowedApps,
|
|
138
|
+
sessionId: normalizedSessionId,
|
|
122
139
|
};
|
|
123
140
|
}
|
|
124
141
|
} catch (err) {
|
|
@@ -131,7 +148,7 @@ router.use(async (req, res, next) => {
|
|
|
131
148
|
const getCookieOptions = () => ({
|
|
132
149
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
133
150
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
134
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
151
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
135
152
|
sameSite: 'lax',
|
|
136
153
|
path: '/',
|
|
137
154
|
httpOnly: true
|
|
@@ -139,32 +156,64 @@ const getCookieOptions = () => ({
|
|
|
139
156
|
|
|
140
157
|
const getClearCookieOptions = () => ({
|
|
141
158
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
142
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true'
|
|
159
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
143
160
|
sameSite: 'lax',
|
|
144
161
|
path: '/',
|
|
145
162
|
httpOnly: true
|
|
146
163
|
});
|
|
147
164
|
|
|
165
|
+
router.get('/mbkauthe/test', validateSession, LoginLimit, async(req, res) => {
|
|
166
|
+
if(req.session?.user) {
|
|
167
|
+
return res.send(`
|
|
168
|
+
<head> <script src="/mbkauthe/main.js"></script> </head>
|
|
169
|
+
<p>if you are seeing this page than User is logged in.</p>
|
|
170
|
+
<p>id: '${req.session.user.id}', UserName: '${req.session.user.username}', Role: '${req.session.user.role}', SessionId: '${req.session.user.sessionId}'</p>
|
|
171
|
+
<button onclick="logout()">Logout</button><br>
|
|
172
|
+
<a href="/mbkauthe/info">Info Page</a><br>
|
|
173
|
+
<a href="/mbkauthe/login">Login Page</a><br>
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
148
178
|
async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
149
179
|
try {
|
|
180
|
+
// Ensure both username formats are available for compatibility
|
|
181
|
+
const username = user.username || user.UserName;
|
|
182
|
+
if (!username) {
|
|
183
|
+
throw new Error('Username is required in user object');
|
|
184
|
+
}
|
|
185
|
+
|
|
150
186
|
// smaller session id is sufficient and faster to generate/serialize
|
|
151
187
|
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
152
|
-
console.log(`[mbkauthe] Generated session ID for username: ${
|
|
188
|
+
console.log(`[mbkauthe] Generated session ID for username: ${username}`);
|
|
189
|
+
|
|
190
|
+
// Regenerate session to prevent session fixation attacks
|
|
191
|
+
const oldSessionId = req.sessionID;
|
|
192
|
+
await new Promise((resolve, reject) => {
|
|
193
|
+
req.session.regenerate((err) => {
|
|
194
|
+
if (err) reject(err);
|
|
195
|
+
else resolve();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
153
198
|
|
|
154
|
-
// Delete old
|
|
155
|
-
await dblogin.query(
|
|
199
|
+
// Delete all old sessions for this user from session table
|
|
200
|
+
await dblogin.query({
|
|
201
|
+
name: 'login-delete-old-user-sessions',
|
|
202
|
+
text: 'DELETE FROM "session" WHERE sess::text LIKE $1',
|
|
203
|
+
values: [`%"username":"${username}"%`]
|
|
204
|
+
});
|
|
156
205
|
|
|
157
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [
|
|
206
|
+
await dblogin.query({ name: 'login-update-session-id', text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, values: [
|
|
158
207
|
sessionId,
|
|
159
208
|
user.id,
|
|
160
|
-
]);
|
|
209
|
+
] });
|
|
161
210
|
|
|
162
211
|
req.session.user = {
|
|
163
212
|
id: user.id,
|
|
164
|
-
username:
|
|
165
|
-
UserName:
|
|
166
|
-
role: user.role,
|
|
167
|
-
Role: user.role,
|
|
213
|
+
username: username,
|
|
214
|
+
UserName: username,
|
|
215
|
+
role: user.role || user.Role,
|
|
216
|
+
Role: user.role || user.Role,
|
|
168
217
|
sessionId,
|
|
169
218
|
};
|
|
170
219
|
|
|
@@ -182,7 +231,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
182
231
|
|
|
183
232
|
const cookieOptions = getCookieOptions();
|
|
184
233
|
res.cookie("sessionId", sessionId, cookieOptions);
|
|
185
|
-
console.log(`[mbkauthe] User "${
|
|
234
|
+
console.log(`[mbkauthe] User "${username}" logged in successfully`);
|
|
186
235
|
|
|
187
236
|
const responsePayload = {
|
|
188
237
|
success: true,
|
|
@@ -216,8 +265,8 @@ router.use(async (req, res, next) => {
|
|
|
216
265
|
|
|
217
266
|
router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
218
267
|
try {
|
|
219
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL`);
|
|
220
|
-
await dblogin.query('DELETE FROM "session"');
|
|
268
|
+
await dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` });
|
|
269
|
+
await dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' });
|
|
221
270
|
|
|
222
271
|
req.session.destroy((err) => {
|
|
223
272
|
if (err) {
|
|
@@ -280,11 +329,11 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
280
329
|
|
|
281
330
|
try {
|
|
282
331
|
const userQuery = `SELECT id, "UserName", "Password", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
|
|
283
|
-
const userResult = await dblogin.query({ name: 'get-user
|
|
332
|
+
const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
|
|
284
333
|
|
|
285
334
|
if (userResult.rows.length === 0) {
|
|
286
|
-
console.log(`[mbkauthe]
|
|
287
|
-
return res.status(
|
|
335
|
+
console.log(`[mbkauthe] Login failed: invalid credentials`);
|
|
336
|
+
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
288
337
|
}
|
|
289
338
|
|
|
290
339
|
const user = userResult.rows[0];
|
|
@@ -299,18 +348,18 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
299
348
|
try {
|
|
300
349
|
const result = await bcrypt.compare(password, user.Password);
|
|
301
350
|
if (!result) {
|
|
302
|
-
console.log("[mbkauthe]
|
|
303
|
-
return res.status(401).json({ success: false, errorCode: 603, message: "
|
|
351
|
+
console.log("[mbkauthe] Login failed: invalid credentials");
|
|
352
|
+
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
304
353
|
}
|
|
305
|
-
console.log("[mbkauthe] Password
|
|
354
|
+
console.log("[mbkauthe] Password validated successfully");
|
|
306
355
|
} catch (err) {
|
|
307
356
|
console.error("[mbkauthe] Error comparing password:", err);
|
|
308
357
|
return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` });
|
|
309
358
|
}
|
|
310
359
|
} else {
|
|
311
360
|
if (user.Password !== password) {
|
|
312
|
-
console.log(`[mbkauthe]
|
|
313
|
-
return res.status(401).json({ success: false, errorCode: 603, message: "
|
|
361
|
+
console.log(`[mbkauthe] Login failed: invalid credentials`);
|
|
362
|
+
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
314
363
|
}
|
|
315
364
|
}
|
|
316
365
|
|
|
@@ -329,7 +378,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
329
378
|
|
|
330
379
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
|
|
331
380
|
const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
332
|
-
const twoFAResult = await dblogin.query({ name: '
|
|
381
|
+
const twoFAResult = await dblogin.query({ name: 'login-check-2fa-status', text: query, values: [trimmedUsername] });
|
|
333
382
|
|
|
334
383
|
if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
|
|
335
384
|
// 2FA is enabled, prompt for token on a separate page
|
|
@@ -367,6 +416,7 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
367
416
|
layout: false,
|
|
368
417
|
customURL: mbkautheVar.loginRedirectURL || '/home',
|
|
369
418
|
csrfToken: req.csrfToken(),
|
|
419
|
+
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
370
420
|
});
|
|
371
421
|
});
|
|
372
422
|
|
|
@@ -391,7 +441,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
391
441
|
|
|
392
442
|
try {
|
|
393
443
|
const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
394
|
-
const twoFAResult = await dblogin.query(query, [username]);
|
|
444
|
+
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
395
445
|
|
|
396
446
|
if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
|
|
397
447
|
return res.status(500).json({ success: false, message: "2FA is not configured correctly." });
|
|
@@ -421,15 +471,15 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
421
471
|
}
|
|
422
472
|
});
|
|
423
473
|
|
|
424
|
-
router.post("/mbkauthe/api/logout", LogoutLimit,
|
|
474
|
+
router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
425
475
|
if (req.session.user) {
|
|
426
476
|
try {
|
|
427
477
|
const { id, username } = req.session.user;
|
|
428
478
|
|
|
429
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, [id]);
|
|
479
|
+
await dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] });
|
|
430
480
|
|
|
431
481
|
if (req.sessionID) {
|
|
432
|
-
await dblogin.query('DELETE FROM "session" WHERE sid = $1', [req.sessionID]);
|
|
482
|
+
await dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] });
|
|
433
483
|
}
|
|
434
484
|
|
|
435
485
|
req.session.destroy((err) => {
|
|
@@ -463,7 +513,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
|
|
|
463
513
|
userLoggedIn: !!req.session?.user,
|
|
464
514
|
username: req.session?.user?.username || '',
|
|
465
515
|
version: packageJson.version,
|
|
466
|
-
appName: mbkautheVar.APP_NAME.
|
|
516
|
+
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
467
517
|
csrfToken: req.csrfToken(),
|
|
468
518
|
});
|
|
469
519
|
});
|
|
@@ -483,9 +533,19 @@ async function getLatestVersion() {
|
|
|
483
533
|
}
|
|
484
534
|
}
|
|
485
535
|
|
|
486
|
-
router.get(
|
|
487
|
-
|
|
536
|
+
router.get("/mbkauthe/bg.avif", (req, res) => {
|
|
537
|
+
res.sendFile("bg.avif", { root: path.join(process.cwd(), "public") });
|
|
538
|
+
});
|
|
488
539
|
|
|
540
|
+
router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
|
|
541
|
+
let latestVersion;
|
|
542
|
+
const parameters = req.query;
|
|
543
|
+
let authorized = false;
|
|
544
|
+
|
|
545
|
+
if (parameters.password && mbkautheVar.Main_SECRET_TOKEN) {
|
|
546
|
+
authorized = String(parameters.password) === String(mbkautheVar.Main_SECRET_TOKEN);
|
|
547
|
+
}
|
|
548
|
+
|
|
489
549
|
try {
|
|
490
550
|
latestVersion = await getLatestVersion();
|
|
491
551
|
//latestVersion = "Under Development"; // Placeholder for the latest version
|
|
@@ -499,6 +559,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
|
|
|
499
559
|
mbkautheVar: mbkautheVar,
|
|
500
560
|
version: packageJson.version,
|
|
501
561
|
latestVersion,
|
|
562
|
+
authorized: authorized,
|
|
502
563
|
});
|
|
503
564
|
} catch (err) {
|
|
504
565
|
console.error("[mbkauthe] Error fetching version information:", err);
|
|
@@ -515,7 +576,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
|
|
|
515
576
|
`);
|
|
516
577
|
}
|
|
517
578
|
});
|
|
518
|
-
|
|
579
|
+
|
|
519
580
|
// Configure GitHub Strategy for login
|
|
520
581
|
passport.use('github-login', new GitHubStrategy({
|
|
521
582
|
clientID: process.env.GITHUB_CLIENT_ID,
|
|
@@ -526,28 +587,35 @@ passport.use('github-login', new GitHubStrategy({
|
|
|
526
587
|
async (accessToken, refreshToken, profile, done) => {
|
|
527
588
|
try {
|
|
528
589
|
// Check if this GitHub account is linked to any user
|
|
529
|
-
const githubUser = await dblogin.query(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
590
|
+
const githubUser = await dblogin.query({
|
|
591
|
+
name: 'github-login-get-user',
|
|
592
|
+
text: 'SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id" FROM user_github ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.github_id = $1',
|
|
593
|
+
values: [profile.id]
|
|
594
|
+
});
|
|
533
595
|
|
|
534
596
|
if (githubUser.rows.length === 0) {
|
|
535
597
|
// GitHub account is not linked to any user
|
|
536
|
-
|
|
598
|
+
const error = new Error('GitHub account not linked to any user');
|
|
599
|
+
error.code = 'GITHUB_NOT_LINKED';
|
|
600
|
+
return done(error);
|
|
537
601
|
}
|
|
538
602
|
|
|
539
603
|
const user = githubUser.rows[0];
|
|
540
604
|
|
|
541
605
|
// Check if the user account is active
|
|
542
606
|
if (!user.Active) {
|
|
543
|
-
|
|
607
|
+
const error = new Error('Account is inactive');
|
|
608
|
+
error.code = 'ACCOUNT_INACTIVE';
|
|
609
|
+
return done(error);
|
|
544
610
|
}
|
|
545
611
|
|
|
546
612
|
// Check if user is authorized for this app (same logic as regular login)
|
|
547
613
|
if (user.Role !== "SuperAdmin") {
|
|
548
614
|
const allowedApps = user.AllowedApps;
|
|
549
615
|
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
550
|
-
|
|
616
|
+
const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
|
|
617
|
+
error.code = 'NOT_AUTHORIZED';
|
|
618
|
+
return done(error);
|
|
551
619
|
}
|
|
552
620
|
}
|
|
553
621
|
|
|
@@ -561,6 +629,7 @@ passport.use('github-login', new GitHubStrategy({
|
|
|
561
629
|
});
|
|
562
630
|
} catch (err) {
|
|
563
631
|
console.error('[mbkauthe] GitHub login error:', err);
|
|
632
|
+
err.code = err.code || 'GITHUB_AUTH_ERROR';
|
|
564
633
|
return done(err);
|
|
565
634
|
}
|
|
566
635
|
}
|
|
@@ -580,25 +649,128 @@ router.use(passport.initialize());
|
|
|
580
649
|
router.use(passport.session());
|
|
581
650
|
|
|
582
651
|
// GitHub login initiation
|
|
583
|
-
router.get('/mbkauthe/api/github/login',
|
|
652
|
+
router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
|
|
653
|
+
// Store redirect parameter in session before OAuth flow (validate to prevent open redirect)
|
|
654
|
+
const redirect = req.query.redirect;
|
|
655
|
+
if (redirect && typeof redirect === 'string') {
|
|
656
|
+
// Only allow relative URLs or same-origin URLs to prevent open redirect attacks
|
|
657
|
+
if (redirect.startsWith('/') && !redirect.startsWith('//')) {
|
|
658
|
+
req.session.oauthRedirect = redirect;
|
|
659
|
+
} else {
|
|
660
|
+
console.warn(`[mbkauthe] Invalid redirect parameter rejected: ${redirect}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
passport.authenticate('github-login')(req, res, next);
|
|
664
|
+
});
|
|
584
665
|
|
|
585
666
|
// GitHub login callback
|
|
586
667
|
router.get('/mbkauthe/api/github/login/callback',
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
668
|
+
GitHubOAuthLimit,
|
|
669
|
+
(req, res, next) => {
|
|
670
|
+
passport.authenticate('github-login', {
|
|
671
|
+
session: false // We'll handle session manually
|
|
672
|
+
}, (err, user, info) => {
|
|
673
|
+
// Custom error handling for passport authentication
|
|
674
|
+
if (err) {
|
|
675
|
+
console.error('[mbkauthe] GitHub authentication error:', err);
|
|
676
|
+
|
|
677
|
+
// Map error codes to user-friendly messages
|
|
678
|
+
switch(err.code) {
|
|
679
|
+
case 'GITHUB_NOT_LINKED':
|
|
680
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
681
|
+
layout: false,
|
|
682
|
+
code: '403',
|
|
683
|
+
error: 'GitHub Account Not Linked',
|
|
684
|
+
message: 'Your GitHub account is not linked to any user in our system. To link your GitHub account, a User must connect their GitHub account to mbktech account through the user settings.',
|
|
685
|
+
page: '/mbkauthe/login',
|
|
686
|
+
pagename: 'Login',
|
|
687
|
+
version: packageJson.version,
|
|
688
|
+
app: mbkautheVar.APP_NAME
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
case 'ACCOUNT_INACTIVE':
|
|
692
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
693
|
+
layout: false,
|
|
694
|
+
code: '403',
|
|
695
|
+
error: 'Account Inactive',
|
|
696
|
+
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
697
|
+
page: '/mbkauthe/login',
|
|
698
|
+
pagename: 'Login',
|
|
699
|
+
version: packageJson.version,
|
|
700
|
+
app: mbkautheVar.APP_NAME
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
case 'NOT_AUTHORIZED':
|
|
704
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
705
|
+
layout: false,
|
|
706
|
+
code: '403',
|
|
707
|
+
error: 'Not Authorized',
|
|
708
|
+
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
709
|
+
page: '/mbkauthe/login',
|
|
710
|
+
pagename: 'Login',
|
|
711
|
+
version: packageJson.version,
|
|
712
|
+
app: mbkautheVar.APP_NAME
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
default:
|
|
716
|
+
return res.status(500).render('Error/dError.handlebars', {
|
|
717
|
+
layout: false,
|
|
718
|
+
code: '500',
|
|
719
|
+
error: 'Authentication Error',
|
|
720
|
+
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
721
|
+
page: '/mbkauthe/login',
|
|
722
|
+
pagename: 'Login',
|
|
723
|
+
version: packageJson.version,
|
|
724
|
+
app: mbkautheVar.APP_NAME,
|
|
725
|
+
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!user) {
|
|
731
|
+
console.error('[mbkauthe] GitHub callback: No user data received');
|
|
732
|
+
return res.status(401).render('Error/dError.handlebars', {
|
|
733
|
+
layout: false,
|
|
734
|
+
code: '401',
|
|
735
|
+
error: 'Authentication Failed',
|
|
736
|
+
message: 'GitHub authentication failed. Please try again.',
|
|
737
|
+
page: '/mbkauthe/login',
|
|
738
|
+
pagename: 'Login',
|
|
739
|
+
version: packageJson.version,
|
|
740
|
+
app: mbkautheVar.APP_NAME
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Authentication successful, attach user to request
|
|
745
|
+
req.user = user;
|
|
746
|
+
next();
|
|
747
|
+
})(req, res, next);
|
|
748
|
+
},
|
|
591
749
|
async (req, res) => {
|
|
592
750
|
try {
|
|
593
751
|
const githubUser = req.user;
|
|
594
752
|
|
|
595
|
-
// Find the actual user record
|
|
596
|
-
const userQuery = `SELECT
|
|
597
|
-
const userResult = await dblogin.query(
|
|
753
|
+
// Find the actual user record with named query
|
|
754
|
+
const userQuery = `SELECT id, "UserName", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
|
|
755
|
+
const userResult = await dblogin.query({
|
|
756
|
+
name: 'github-callback-get-user',
|
|
757
|
+
text: userQuery,
|
|
758
|
+
values: [githubUser.username]
|
|
759
|
+
});
|
|
598
760
|
|
|
599
761
|
if (userResult.rows.length === 0) {
|
|
600
|
-
console.
|
|
601
|
-
return res.
|
|
762
|
+
console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
|
|
763
|
+
return res.status(404).render('Error/dError.handlebars', {
|
|
764
|
+
layout: false,
|
|
765
|
+
code: '404',
|
|
766
|
+
error: 'User Not Found',
|
|
767
|
+
message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
|
|
768
|
+
page: '/mbkauthe/login',
|
|
769
|
+
pagename: 'Login',
|
|
770
|
+
version: packageJson.version,
|
|
771
|
+
app: mbkautheVar.APP_NAME,
|
|
772
|
+
details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
|
|
773
|
+
});
|
|
602
774
|
}
|
|
603
775
|
|
|
604
776
|
const user = userResult.rows[0];
|
|
@@ -606,14 +778,20 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
606
778
|
// Check 2FA if enabled
|
|
607
779
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
|
|
608
780
|
const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
609
|
-
const twoFAResult = await dblogin.query(
|
|
781
|
+
const twoFAResult = await dblogin.query({
|
|
782
|
+
name: 'github-check-2fa-status',
|
|
783
|
+
text: twoFAQuery,
|
|
784
|
+
values: [githubUser.username]
|
|
785
|
+
});
|
|
610
786
|
|
|
611
787
|
if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
|
|
612
788
|
// 2FA is enabled, store pre-auth user and redirect to 2FA
|
|
613
789
|
req.session.preAuthUser = {
|
|
614
790
|
id: user.id,
|
|
615
791
|
username: user.UserName,
|
|
792
|
+
UserName: user.UserName,
|
|
616
793
|
role: user.Role,
|
|
794
|
+
Role: user.Role,
|
|
617
795
|
loginMethod: 'github'
|
|
618
796
|
};
|
|
619
797
|
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
@@ -621,62 +799,64 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
621
799
|
}
|
|
622
800
|
}
|
|
623
801
|
|
|
624
|
-
// Complete login process
|
|
802
|
+
// Complete login process using the shared function
|
|
625
803
|
const userForSession = {
|
|
626
804
|
id: user.id,
|
|
627
805
|
username: user.UserName,
|
|
806
|
+
UserName: user.UserName,
|
|
628
807
|
role: user.Role,
|
|
808
|
+
Role: user.Role
|
|
629
809
|
};
|
|
630
810
|
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
// Delete old session record for this user
|
|
636
|
-
await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.UserName]);
|
|
811
|
+
// For OAuth redirect flow, we need to handle redirect differently
|
|
812
|
+
// Store the redirect URL before calling completeLoginProcess
|
|
813
|
+
const oauthRedirect = req.session.oauthRedirect;
|
|
814
|
+
delete req.session.oauthRedirect;
|
|
637
815
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
role: user.Role,
|
|
647
|
-
sessionId,
|
|
816
|
+
// Custom response handler for OAuth flow - wrap the response object
|
|
817
|
+
const originalJson = res.json.bind(res);
|
|
818
|
+
const originalStatus = res.status.bind(res);
|
|
819
|
+
let statusCode = 200;
|
|
820
|
+
|
|
821
|
+
res.status = function(code) {
|
|
822
|
+
statusCode = code;
|
|
823
|
+
return originalStatus(code);
|
|
648
824
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
[user.UserName, req.sessionID]
|
|
660
|
-
);
|
|
661
|
-
} catch (e) {
|
|
662
|
-
console.log("[mbkauthe] GitHub login: Failed to update username in session table:", e);
|
|
825
|
+
|
|
826
|
+
res.json = function(data) {
|
|
827
|
+
if (data.success && statusCode === 200) {
|
|
828
|
+
// If login successful, redirect instead of sending JSON
|
|
829
|
+
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/home';
|
|
830
|
+
console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
|
|
831
|
+
// Restore original methods before redirect
|
|
832
|
+
res.json = originalJson;
|
|
833
|
+
res.status = originalStatus;
|
|
834
|
+
return res.redirect(redirectUrl);
|
|
663
835
|
}
|
|
836
|
+
// Restore original methods for error responses
|
|
837
|
+
res.json = originalJson;
|
|
838
|
+
res.status = originalStatus;
|
|
839
|
+
return originalJson(data);
|
|
840
|
+
};
|
|
664
841
|
|
|
665
|
-
|
|
666
|
-
res.cookie("sessionId", sessionId, cookieOptions);
|
|
667
|
-
console.log(`[mbkauthe] GitHub login: User "${user.UserName}" logged in successfully`);
|
|
668
|
-
|
|
669
|
-
// Redirect to the configured URL or home
|
|
670
|
-
const redirectUrl = mbkautheVar.loginRedirectURL || '/home';
|
|
671
|
-
res.redirect(redirectUrl);
|
|
672
|
-
});
|
|
842
|
+
await completeLoginProcess(req, res, userForSession);
|
|
673
843
|
|
|
674
844
|
} catch (err) {
|
|
675
845
|
console.error('[mbkauthe] GitHub login callback error:', err);
|
|
676
|
-
res.
|
|
846
|
+
return res.status(500).render('Error/dError.handlebars', {
|
|
847
|
+
layout: false,
|
|
848
|
+
code: '500',
|
|
849
|
+
error: 'Internal Server Error',
|
|
850
|
+
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
851
|
+
page: '/mbkauthe/login',
|
|
852
|
+
pagename: 'Login',
|
|
853
|
+
version: packageJson.version,
|
|
854
|
+
app: mbkautheVar.APP_NAME,
|
|
855
|
+
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
856
|
+
});
|
|
677
857
|
}
|
|
678
858
|
}
|
|
679
859
|
);
|
|
680
|
-
|
|
860
|
+
|
|
681
861
|
export { getLatestVersion };
|
|
682
862
|
export default router;
|
package/lib/pool.js
CHANGED
|
@@ -36,8 +36,8 @@ const poolConfig = {
|
|
|
36
36
|
// - keep max small to avoid exhausting DB connections
|
|
37
37
|
// - reduce idle time so connections are returned sooner
|
|
38
38
|
// - set a short connection timeout to fail fast
|
|
39
|
-
max:
|
|
40
|
-
idleTimeoutMillis:
|
|
39
|
+
max: 20,
|
|
40
|
+
idleTimeoutMillis: 10000,
|
|
41
41
|
connectionTimeoutMillis: 5000,
|
|
42
42
|
};
|
|
43
43
|
|