mbkauthe 1.4.2 → 2.0.1
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/.env.example +1 -10
- package/README.md +3 -3
- package/docs/api.md +69 -3
- package/docs/db.md +0 -6
- package/env.md +69 -1
- package/lib/main.js +378 -182
- 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 +55 -13
- 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,66 @@ 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(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
206
|
+
await dblogin.query({
|
|
207
|
+
name: 'login-update-session-id', text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, values: [
|
|
208
|
+
sessionId,
|
|
209
|
+
user.id,
|
|
210
|
+
]
|
|
211
|
+
});
|
|
161
212
|
|
|
162
213
|
req.session.user = {
|
|
163
214
|
id: user.id,
|
|
164
|
-
username:
|
|
165
|
-
UserName:
|
|
166
|
-
role: user.role,
|
|
167
|
-
Role: user.role,
|
|
215
|
+
username: username,
|
|
216
|
+
UserName: username,
|
|
217
|
+
role: user.role || user.Role,
|
|
218
|
+
Role: user.role || user.Role,
|
|
168
219
|
sessionId,
|
|
169
220
|
};
|
|
170
221
|
|
|
@@ -182,7 +233,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
182
233
|
|
|
183
234
|
const cookieOptions = getCookieOptions();
|
|
184
235
|
res.cookie("sessionId", sessionId, cookieOptions);
|
|
185
|
-
console.log(`[mbkauthe] User "${
|
|
236
|
+
console.log(`[mbkauthe] User "${username}" logged in successfully`);
|
|
186
237
|
|
|
187
238
|
const responsePayload = {
|
|
188
239
|
success: true,
|
|
@@ -216,8 +267,8 @@ router.use(async (req, res, next) => {
|
|
|
216
267
|
|
|
217
268
|
router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
218
269
|
try {
|
|
219
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL`);
|
|
220
|
-
await dblogin.query('DELETE FROM "session"');
|
|
270
|
+
await dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` });
|
|
271
|
+
await dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' });
|
|
221
272
|
|
|
222
273
|
req.session.destroy((err) => {
|
|
223
274
|
if (err) {
|
|
@@ -280,11 +331,11 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
280
331
|
|
|
281
332
|
try {
|
|
282
333
|
const userQuery = `SELECT id, "UserName", "Password", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
|
|
283
|
-
const userResult = await dblogin.query({ name: 'get-user
|
|
334
|
+
const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
|
|
284
335
|
|
|
285
336
|
if (userResult.rows.length === 0) {
|
|
286
|
-
console.log(`[mbkauthe]
|
|
287
|
-
return res.status(
|
|
337
|
+
console.log(`[mbkauthe] Login failed: invalid credentials`);
|
|
338
|
+
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
288
339
|
}
|
|
289
340
|
|
|
290
341
|
const user = userResult.rows[0];
|
|
@@ -299,18 +350,18 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
299
350
|
try {
|
|
300
351
|
const result = await bcrypt.compare(password, user.Password);
|
|
301
352
|
if (!result) {
|
|
302
|
-
console.log("[mbkauthe]
|
|
303
|
-
return res.status(401).json({ success: false, errorCode: 603, message: "
|
|
353
|
+
console.log("[mbkauthe] Login failed: invalid credentials");
|
|
354
|
+
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
304
355
|
}
|
|
305
|
-
console.log("[mbkauthe] Password
|
|
356
|
+
console.log("[mbkauthe] Password validated successfully");
|
|
306
357
|
} catch (err) {
|
|
307
358
|
console.error("[mbkauthe] Error comparing password:", err);
|
|
308
359
|
return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` });
|
|
309
360
|
}
|
|
310
361
|
} else {
|
|
311
362
|
if (user.Password !== password) {
|
|
312
|
-
console.log(`[mbkauthe]
|
|
313
|
-
return res.status(401).json({ success: false, errorCode: 603, message: "
|
|
363
|
+
console.log(`[mbkauthe] Login failed: invalid credentials`);
|
|
364
|
+
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
314
365
|
}
|
|
315
366
|
}
|
|
316
367
|
|
|
@@ -329,7 +380,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
329
380
|
|
|
330
381
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
|
|
331
382
|
const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
332
|
-
const twoFAResult = await dblogin.query({ name: '
|
|
383
|
+
const twoFAResult = await dblogin.query({ name: 'login-check-2fa-status', text: query, values: [trimmedUsername] });
|
|
333
384
|
|
|
334
385
|
if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
|
|
335
386
|
// 2FA is enabled, prompt for token on a separate page
|
|
@@ -367,6 +418,7 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
367
418
|
layout: false,
|
|
368
419
|
customURL: mbkautheVar.loginRedirectURL || '/home',
|
|
369
420
|
csrfToken: req.csrfToken(),
|
|
421
|
+
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
370
422
|
});
|
|
371
423
|
});
|
|
372
424
|
|
|
@@ -391,7 +443,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
391
443
|
|
|
392
444
|
try {
|
|
393
445
|
const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
394
|
-
const twoFAResult = await dblogin.query(query, [username]);
|
|
446
|
+
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
395
447
|
|
|
396
448
|
if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
|
|
397
449
|
return res.status(500).json({ success: false, message: "2FA is not configured correctly." });
|
|
@@ -421,15 +473,15 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
421
473
|
}
|
|
422
474
|
});
|
|
423
475
|
|
|
424
|
-
router.post("/mbkauthe/api/logout", LogoutLimit,
|
|
476
|
+
router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
425
477
|
if (req.session.user) {
|
|
426
478
|
try {
|
|
427
479
|
const { id, username } = req.session.user;
|
|
428
480
|
|
|
429
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, [id]);
|
|
481
|
+
await dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] });
|
|
430
482
|
|
|
431
483
|
if (req.sessionID) {
|
|
432
|
-
await dblogin.query('DELETE FROM "session" WHERE sid = $1', [req.sessionID]);
|
|
484
|
+
await dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] });
|
|
433
485
|
}
|
|
434
486
|
|
|
435
487
|
req.session.destroy((err) => {
|
|
@@ -463,7 +515,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
|
|
|
463
515
|
userLoggedIn: !!req.session?.user,
|
|
464
516
|
username: req.session?.user?.username || '',
|
|
465
517
|
version: packageJson.version,
|
|
466
|
-
appName: mbkautheVar.APP_NAME.
|
|
518
|
+
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
467
519
|
csrfToken: req.csrfToken(),
|
|
468
520
|
});
|
|
469
521
|
});
|
|
@@ -483,8 +535,18 @@ async function getLatestVersion() {
|
|
|
483
535
|
}
|
|
484
536
|
}
|
|
485
537
|
|
|
486
|
-
router.get(
|
|
538
|
+
router.get("/mbkauthe/bg.avif", (req, res) => {
|
|
539
|
+
res.sendFile("bg.avif", { root: path.join(process.cwd(), "public") });
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
|
|
487
543
|
let latestVersion;
|
|
544
|
+
const parameters = req.query;
|
|
545
|
+
let authorized = false;
|
|
546
|
+
|
|
547
|
+
if (parameters.password && mbkautheVar.Main_SECRET_TOKEN) {
|
|
548
|
+
authorized = String(parameters.password) === String(mbkautheVar.Main_SECRET_TOKEN);
|
|
549
|
+
}
|
|
488
550
|
|
|
489
551
|
try {
|
|
490
552
|
latestVersion = await getLatestVersion();
|
|
@@ -499,6 +561,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
|
|
|
499
561
|
mbkautheVar: mbkautheVar,
|
|
500
562
|
version: packageJson.version,
|
|
501
563
|
latestVersion,
|
|
564
|
+
authorized: authorized,
|
|
502
565
|
});
|
|
503
566
|
} catch (err) {
|
|
504
567
|
console.error("[mbkauthe] Error fetching version information:", err);
|
|
@@ -515,64 +578,72 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
|
|
|
515
578
|
`);
|
|
516
579
|
}
|
|
517
580
|
});
|
|
518
|
-
|
|
581
|
+
|
|
519
582
|
// Configure GitHub Strategy for login
|
|
520
583
|
passport.use('github-login', new GitHubStrategy({
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
584
|
+
clientID: mbkautheVar.GITHUB_CLIENT_ID,
|
|
585
|
+
clientSecret: mbkautheVar.GITHUB_CLIENT_SECRET,
|
|
586
|
+
callbackURL: '/mbkauthe/api/github/login/callback',
|
|
587
|
+
scope: ['user:email']
|
|
525
588
|
},
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
role: user.Role,
|
|
559
|
-
githubId: user.github_id,
|
|
560
|
-
githubUsername: user.github_username
|
|
561
|
-
});
|
|
562
|
-
} catch (err) {
|
|
563
|
-
console.error('[mbkauthe] GitHub login error:', err);
|
|
564
|
-
return done(err);
|
|
589
|
+
async (accessToken, refreshToken, profile, done) => {
|
|
590
|
+
try {
|
|
591
|
+
// Check if this GitHub account is linked to any user
|
|
592
|
+
const githubUser = await dblogin.query({
|
|
593
|
+
name: 'github-login-get-user',
|
|
594
|
+
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',
|
|
595
|
+
values: [profile.id]
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (githubUser.rows.length === 0) {
|
|
599
|
+
// GitHub account is not linked to any user
|
|
600
|
+
const error = new Error('GitHub account not linked to any user');
|
|
601
|
+
error.code = 'GITHUB_NOT_LINKED';
|
|
602
|
+
return done(error);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const user = githubUser.rows[0];
|
|
606
|
+
|
|
607
|
+
// Check if the user account is active
|
|
608
|
+
if (!user.Active) {
|
|
609
|
+
const error = new Error('Account is inactive');
|
|
610
|
+
error.code = 'ACCOUNT_INACTIVE';
|
|
611
|
+
return done(error);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Check if user is authorized for this app (same logic as regular login)
|
|
615
|
+
if (user.Role !== "SuperAdmin") {
|
|
616
|
+
const allowedApps = user.AllowedApps;
|
|
617
|
+
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
618
|
+
const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
|
|
619
|
+
error.code = 'NOT_AUTHORIZED';
|
|
620
|
+
return done(error);
|
|
565
621
|
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Return user data for login
|
|
625
|
+
return done(null, {
|
|
626
|
+
id: user.id, // This should be the user ID from the Users table
|
|
627
|
+
username: user.UserName,
|
|
628
|
+
role: user.Role,
|
|
629
|
+
githubId: user.github_id,
|
|
630
|
+
githubUsername: user.github_username
|
|
631
|
+
});
|
|
632
|
+
} catch (err) {
|
|
633
|
+
console.error('[mbkauthe] GitHub login error:', err);
|
|
634
|
+
err.code = err.code || 'GITHUB_AUTH_ERROR';
|
|
635
|
+
return done(err);
|
|
566
636
|
}
|
|
637
|
+
}
|
|
567
638
|
));
|
|
568
639
|
|
|
569
640
|
// Serialize/Deserialize user for GitHub login
|
|
570
641
|
passport.serializeUser((user, done) => {
|
|
571
|
-
|
|
642
|
+
done(null, user);
|
|
572
643
|
});
|
|
573
644
|
|
|
574
645
|
passport.deserializeUser((user, done) => {
|
|
575
|
-
|
|
646
|
+
done(null, user);
|
|
576
647
|
});
|
|
577
648
|
|
|
578
649
|
// Initialize passport
|
|
@@ -580,103 +651,228 @@ router.use(passport.initialize());
|
|
|
580
651
|
router.use(passport.session());
|
|
581
652
|
|
|
582
653
|
// GitHub login initiation
|
|
583
|
-
router.get('/mbkauthe/api/github/login',
|
|
654
|
+
router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
|
|
655
|
+
if (mbkautheVar.GITHUB_LOGIN_ENABLED) {
|
|
656
|
+
// Store redirect parameter in session before OAuth flow (validate to prevent open redirect)
|
|
657
|
+
const redirect = req.query.redirect;
|
|
658
|
+
if (redirect && typeof redirect === 'string') {
|
|
659
|
+
// Only allow relative URLs or same-origin URLs to prevent open redirect attacks
|
|
660
|
+
if (redirect.startsWith('/') && !redirect.startsWith('//')) {
|
|
661
|
+
req.session.oauthRedirect = redirect;
|
|
662
|
+
} else {
|
|
663
|
+
console.warn(`[mbkauthe] Invalid redirect parameter rejected: ${redirect}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
passport.authenticate('github-login')(req, res, next);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
res.status(403).render('Error/dError.handlebars', {
|
|
670
|
+
layout: false,
|
|
671
|
+
code: '403',
|
|
672
|
+
error: 'GitHub Login Disabled',
|
|
673
|
+
message: 'GitHub login is currently disabled. Please use your username and password to log in.',
|
|
674
|
+
page: '/mbkauthe/login',
|
|
675
|
+
pagename: 'Login',
|
|
676
|
+
version: packageJson.version,
|
|
677
|
+
app: mbkautheVar.APP_NAME
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
});
|
|
584
681
|
|
|
585
682
|
// GitHub login callback
|
|
586
683
|
router.get('/mbkauthe/api/github/login/callback',
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
633
|
-
console.log(`[mbkauthe] GitHub login: Generated session ID for username: ${user.UserName}`);
|
|
634
|
-
|
|
635
|
-
// Delete old session record for this user
|
|
636
|
-
await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.UserName]);
|
|
637
|
-
|
|
638
|
-
await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [
|
|
639
|
-
sessionId,
|
|
640
|
-
user.id,
|
|
641
|
-
]);
|
|
642
|
-
|
|
643
|
-
req.session.user = {
|
|
644
|
-
id: user.id,
|
|
645
|
-
username: user.UserName,
|
|
646
|
-
role: user.Role,
|
|
647
|
-
sessionId,
|
|
648
|
-
};
|
|
649
|
-
|
|
650
|
-
req.session.save(async (err) => {
|
|
651
|
-
if (err) {
|
|
652
|
-
console.log("[mbkauthe] GitHub login session save error:", err);
|
|
653
|
-
return res.redirect('/mbkauthe/login?error=session_error');
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
await dblogin.query(
|
|
658
|
-
'UPDATE "session" SET username = $1 WHERE sid = $2',
|
|
659
|
-
[user.UserName, req.sessionID]
|
|
660
|
-
);
|
|
661
|
-
} catch (e) {
|
|
662
|
-
console.log("[mbkauthe] GitHub login: Failed to update username in session table:", e);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const cookieOptions = getCookieOptions();
|
|
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);
|
|
684
|
+
GitHubOAuthLimit,
|
|
685
|
+
(req, res, next) => {
|
|
686
|
+
passport.authenticate('github-login', {
|
|
687
|
+
session: false // We'll handle session manually
|
|
688
|
+
}, (err, user, info) => {
|
|
689
|
+
// Custom error handling for passport authentication
|
|
690
|
+
if (err) {
|
|
691
|
+
console.error('[mbkauthe] GitHub authentication error:', err);
|
|
692
|
+
|
|
693
|
+
// Map error codes to user-friendly messages
|
|
694
|
+
switch (err.code) {
|
|
695
|
+
case 'GITHUB_NOT_LINKED':
|
|
696
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
697
|
+
layout: false,
|
|
698
|
+
code: '403',
|
|
699
|
+
error: 'GitHub Account Not Linked',
|
|
700
|
+
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.',
|
|
701
|
+
page: '/mbkauthe/login',
|
|
702
|
+
pagename: 'Login',
|
|
703
|
+
version: packageJson.version,
|
|
704
|
+
app: mbkautheVar.APP_NAME
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
case 'ACCOUNT_INACTIVE':
|
|
708
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
709
|
+
layout: false,
|
|
710
|
+
code: '403',
|
|
711
|
+
error: 'Account Inactive',
|
|
712
|
+
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
713
|
+
page: '/mbkauthe/login',
|
|
714
|
+
pagename: 'Login',
|
|
715
|
+
version: packageJson.version,
|
|
716
|
+
app: mbkautheVar.APP_NAME
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
case 'NOT_AUTHORIZED':
|
|
720
|
+
return res.status(403).render('Error/dError.handlebars', {
|
|
721
|
+
layout: false,
|
|
722
|
+
code: '403',
|
|
723
|
+
error: 'Not Authorized',
|
|
724
|
+
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
725
|
+
page: '/mbkauthe/login',
|
|
726
|
+
pagename: 'Login',
|
|
727
|
+
version: packageJson.version,
|
|
728
|
+
app: mbkautheVar.APP_NAME
|
|
672
729
|
});
|
|
673
730
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
731
|
+
default:
|
|
732
|
+
return res.status(500).render('Error/dError.handlebars', {
|
|
733
|
+
layout: false,
|
|
734
|
+
code: '500',
|
|
735
|
+
error: 'Authentication Error',
|
|
736
|
+
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
737
|
+
page: '/mbkauthe/login',
|
|
738
|
+
pagename: 'Login',
|
|
739
|
+
version: packageJson.version,
|
|
740
|
+
app: mbkautheVar.APP_NAME,
|
|
741
|
+
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
742
|
+
});
|
|
677
743
|
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (!user) {
|
|
747
|
+
console.error('[mbkauthe] GitHub callback: No user data received');
|
|
748
|
+
return res.status(401).render('Error/dError.handlebars', {
|
|
749
|
+
layout: false,
|
|
750
|
+
code: '401',
|
|
751
|
+
error: 'Authentication Failed',
|
|
752
|
+
message: 'GitHub authentication failed. Please try again.',
|
|
753
|
+
page: '/mbkauthe/login',
|
|
754
|
+
pagename: 'Login',
|
|
755
|
+
version: packageJson.version,
|
|
756
|
+
app: mbkautheVar.APP_NAME
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Authentication successful, attach user to request
|
|
761
|
+
req.user = user;
|
|
762
|
+
next();
|
|
763
|
+
})(req, res, next);
|
|
764
|
+
},
|
|
765
|
+
async (req, res) => {
|
|
766
|
+
try {
|
|
767
|
+
const githubUser = req.user;
|
|
768
|
+
|
|
769
|
+
// Find the actual user record with named query
|
|
770
|
+
const userQuery = `SELECT id, "UserName", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
|
|
771
|
+
const userResult = await dblogin.query({
|
|
772
|
+
name: 'github-callback-get-user',
|
|
773
|
+
text: userQuery,
|
|
774
|
+
values: [githubUser.username]
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (userResult.rows.length === 0) {
|
|
778
|
+
console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
|
|
779
|
+
return res.status(404).render('Error/dError.handlebars', {
|
|
780
|
+
layout: false,
|
|
781
|
+
code: '404',
|
|
782
|
+
error: 'User Not Found',
|
|
783
|
+
message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
|
|
784
|
+
page: '/mbkauthe/login',
|
|
785
|
+
pagename: 'Login',
|
|
786
|
+
version: packageJson.version,
|
|
787
|
+
app: mbkautheVar.APP_NAME,
|
|
788
|
+
details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const user = userResult.rows[0];
|
|
793
|
+
|
|
794
|
+
// Check 2FA if enabled
|
|
795
|
+
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
|
|
796
|
+
const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
797
|
+
const twoFAResult = await dblogin.query({
|
|
798
|
+
name: 'github-check-2fa-status',
|
|
799
|
+
text: twoFAQuery,
|
|
800
|
+
values: [githubUser.username]
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
|
|
804
|
+
// 2FA is enabled, store pre-auth user and redirect to 2FA
|
|
805
|
+
req.session.preAuthUser = {
|
|
806
|
+
id: user.id,
|
|
807
|
+
username: user.UserName,
|
|
808
|
+
UserName: user.UserName,
|
|
809
|
+
role: user.Role,
|
|
810
|
+
Role: user.Role,
|
|
811
|
+
loginMethod: 'github'
|
|
812
|
+
};
|
|
813
|
+
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
814
|
+
return res.redirect('/mbkauthe/2fa');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Complete login process using the shared function
|
|
819
|
+
const userForSession = {
|
|
820
|
+
id: user.id,
|
|
821
|
+
username: user.UserName,
|
|
822
|
+
UserName: user.UserName,
|
|
823
|
+
role: user.Role,
|
|
824
|
+
Role: user.Role
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// For OAuth redirect flow, we need to handle redirect differently
|
|
828
|
+
// Store the redirect URL before calling completeLoginProcess
|
|
829
|
+
const oauthRedirect = req.session.oauthRedirect;
|
|
830
|
+
delete req.session.oauthRedirect;
|
|
831
|
+
|
|
832
|
+
// Custom response handler for OAuth flow - wrap the response object
|
|
833
|
+
const originalJson = res.json.bind(res);
|
|
834
|
+
const originalStatus = res.status.bind(res);
|
|
835
|
+
let statusCode = 200;
|
|
836
|
+
|
|
837
|
+
res.status = function (code) {
|
|
838
|
+
statusCode = code;
|
|
839
|
+
return originalStatus(code);
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
res.json = function (data) {
|
|
843
|
+
if (data.success && statusCode === 200) {
|
|
844
|
+
// If login successful, redirect instead of sending JSON
|
|
845
|
+
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/home';
|
|
846
|
+
console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
|
|
847
|
+
// Restore original methods before redirect
|
|
848
|
+
res.json = originalJson;
|
|
849
|
+
res.status = originalStatus;
|
|
850
|
+
return res.redirect(redirectUrl);
|
|
851
|
+
}
|
|
852
|
+
// Restore original methods for error responses
|
|
853
|
+
res.json = originalJson;
|
|
854
|
+
res.status = originalStatus;
|
|
855
|
+
return originalJson(data);
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
await completeLoginProcess(req, res, userForSession);
|
|
859
|
+
|
|
860
|
+
} catch (err) {
|
|
861
|
+
console.error('[mbkauthe] GitHub login callback error:', err);
|
|
862
|
+
return res.status(500).render('Error/dError.handlebars', {
|
|
863
|
+
layout: false,
|
|
864
|
+
code: '500',
|
|
865
|
+
error: 'Internal Server Error',
|
|
866
|
+
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
867
|
+
page: '/mbkauthe/login',
|
|
868
|
+
pagename: 'Login',
|
|
869
|
+
version: packageJson.version,
|
|
870
|
+
app: mbkautheVar.APP_NAME,
|
|
871
|
+
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
872
|
+
});
|
|
678
873
|
}
|
|
874
|
+
}
|
|
679
875
|
);
|
|
680
|
-
|
|
876
|
+
|
|
681
877
|
export { getLatestVersion };
|
|
682
878
|
export default router;
|