mbkauthe 2.5.0 → 3.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/LICENSE +339 -373
- package/README.md +106 -285
- package/docs/api.md +139 -1
- package/docs/db.md +1 -1
- package/docs/error-messages.md +557 -0
- package/index.d.ts +233 -0
- package/index.js +43 -32
- package/lib/config/cookies.js +52 -0
- package/lib/{config.js → config/index.js} +15 -95
- package/lib/config/security.js +8 -0
- package/lib/{pool.js → database/pool.js} +1 -1
- package/lib/main.js +27 -1041
- package/lib/{validateSessionAndRole.js → middleware/auth.js} +5 -3
- package/lib/middleware/index.js +106 -0
- package/lib/routes/auth.js +521 -0
- package/lib/routes/misc.js +272 -0
- package/lib/routes/oauth.js +325 -0
- package/lib/utils/errors.js +257 -0
- package/lib/utils/response.js +21 -0
- package/package.json +5 -2
- package/public/main.js +4 -4
- package/views/Error/dError.handlebars +1 -1
- package/views/errorCodes.handlebars +341 -0
- package/views/showmessage.handlebars +7 -6
package/lib/main.js
CHANGED
|
@@ -1,1066 +1,52 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
-
import csurf from "csurf";
|
|
3
|
-
import crypto from "crypto";
|
|
4
2
|
import session from "express-session";
|
|
5
|
-
import pgSession from "connect-pg-simple";
|
|
6
|
-
const PgSession = pgSession(session);
|
|
7
|
-
import { dblogin } from "./pool.js";
|
|
8
|
-
import { authenticate, validateSession, validateSessionAndRole } from "./validateSessionAndRole.js";
|
|
9
|
-
import fetch from 'node-fetch';
|
|
10
3
|
import cookieParser from "cookie-parser";
|
|
11
|
-
import rateLimit from 'express-rate-limit';
|
|
12
|
-
import speakeasy from "speakeasy";
|
|
13
4
|
import passport from 'passport';
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import { fileURLToPath } from "url";
|
|
17
|
-
import fs from "fs";
|
|
18
|
-
import path from "path";
|
|
5
|
+
import { packageJson } from "./config/index.js";
|
|
19
6
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
sessionConfig,
|
|
8
|
+
corsMiddleware,
|
|
9
|
+
sessionRestorationMiddleware,
|
|
10
|
+
sessionCookieSyncMiddleware
|
|
11
|
+
} from "./middleware/index.js";
|
|
12
|
+
import authRoutes from "./routes/auth.js";
|
|
13
|
+
import oauthRoutes from "./routes/oauth.js";
|
|
14
|
+
import miscRoutes from "./routes/misc.js";
|
|
24
15
|
|
|
25
16
|
const router = express.Router();
|
|
26
17
|
|
|
18
|
+
// Basic middleware
|
|
27
19
|
router.use(express.json());
|
|
28
20
|
router.use(express.urlencoded({ extended: true }));
|
|
29
21
|
router.use(cookieParser());
|
|
30
22
|
|
|
31
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
|
|
33
|
-
router.get('/mbkauthe/main.js', (req, res) => {
|
|
34
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
35
|
-
res.sendFile(path.join(__dirname, '..', 'public', 'main.js'));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
router.get('/icon.svg', (req, res) => {
|
|
39
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
40
|
-
res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
router.get(['/favicon.ico', '/icon.ico'], (req, res) => {
|
|
44
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
45
|
-
res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
router.get("/mbkauthe/bg.webp", (req, res) => {
|
|
49
|
-
const imgPath = path.join(__dirname, "..", "public", "bg.webp");
|
|
50
|
-
res.setHeader('Content-Type', 'image/webp');
|
|
51
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
52
|
-
const stream = fs.createReadStream(imgPath);
|
|
53
|
-
stream.on('error', (err) => {
|
|
54
|
-
console.error('[mbkauthe] Error streaming bg.webp:', err);
|
|
55
|
-
res.status(404).send('Image not found');
|
|
56
|
-
});
|
|
57
|
-
stream.pipe(res);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// CSRF protection middleware
|
|
61
|
-
const csrfProtection = csurf({ cookie: true });
|
|
62
|
-
|
|
63
23
|
// CORS and security headers
|
|
64
|
-
router.use(
|
|
65
|
-
const origin = req.headers.origin;
|
|
66
|
-
if (origin) {
|
|
67
|
-
try {
|
|
68
|
-
const originUrl = new URL(origin);
|
|
69
|
-
const allowedDomain = `.${mbkautheVar.DOMAIN}`;
|
|
70
|
-
// Exact match or subdomain match (must end with .domain.com, not just domain.com)
|
|
71
|
-
if (originUrl.hostname === mbkautheVar.DOMAIN ||
|
|
72
|
-
(originUrl.hostname.endsWith(allowedDomain) && originUrl.hostname.charAt(originUrl.hostname.length - allowedDomain.length - 1) !== '.')) {
|
|
73
|
-
res.header('Access-Control-Allow-Origin', origin);
|
|
74
|
-
res.header('Access-Control-Allow-Credentials', 'true');
|
|
75
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
76
|
-
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
77
|
-
}
|
|
78
|
-
} catch (err) {
|
|
79
|
-
// Invalid origin URL, skip CORS headers
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
next();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const LoginLimit = rateLimit({
|
|
86
|
-
windowMs: 1 * 60 * 1000,
|
|
87
|
-
max: 8,
|
|
88
|
-
message: { success: false, message: "Too many attempts, please try again later" },
|
|
89
|
-
skip: (req) => {
|
|
90
|
-
return !!req.session.user;
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const LogoutLimit = rateLimit({
|
|
95
|
-
windowMs: 1 * 60 * 1000,
|
|
96
|
-
max: 10,
|
|
97
|
-
message: { success: false, message: "Too many logout attempts, please try again later" }
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const TwoFALimit = rateLimit({
|
|
101
|
-
windowMs: 1 * 60 * 1000,
|
|
102
|
-
max: 5,
|
|
103
|
-
message: { success: false, message: "Too many 2FA attempts, please try again later" }
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const GitHubOAuthLimit = rateLimit({
|
|
107
|
-
windowMs: 5 * 60 * 1000,
|
|
108
|
-
max: 10,
|
|
109
|
-
message: "Too many GitHub login attempts, please try again later"
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const sessionConfig = {
|
|
113
|
-
store: new PgSession({
|
|
114
|
-
pool: dblogin,
|
|
115
|
-
tableName: "session",
|
|
116
|
-
createTableIfMissing: true
|
|
117
|
-
}),
|
|
118
|
-
secret: mbkautheVar.SESSION_SECRET_KEY,
|
|
119
|
-
resave: false,
|
|
120
|
-
saveUninitialized: false,
|
|
121
|
-
proxy: true,
|
|
122
|
-
cookie: {
|
|
123
|
-
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
124
|
-
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
125
|
-
httpOnly: true,
|
|
126
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
127
|
-
sameSite: 'lax',
|
|
128
|
-
path: '/'
|
|
129
|
-
},
|
|
130
|
-
name: 'mbkauthe.sid'
|
|
131
|
-
};
|
|
24
|
+
router.use(corsMiddleware);
|
|
132
25
|
|
|
26
|
+
// Session configuration
|
|
133
27
|
router.use(session(sessionConfig));
|
|
134
28
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!req.session.user && req.cookies.sessionId) {
|
|
138
|
-
const sessionId = req.cookies.sessionId;
|
|
139
|
-
|
|
140
|
-
// Early validation to avoid unnecessary processing
|
|
141
|
-
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
142
|
-
// Clear invalid cookie to prevent repeated attempts
|
|
143
|
-
res.clearCookie('sessionId', cachedClearCookieOptions);
|
|
144
|
-
return next();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
|
|
149
|
-
const normalizedSessionId = sessionId.toLowerCase();
|
|
150
|
-
|
|
151
|
-
const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE LOWER("SessionId") = $1 AND "Active" = true`;
|
|
152
|
-
const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [normalizedSessionId] });
|
|
153
|
-
|
|
154
|
-
if (result.rows.length > 0) {
|
|
155
|
-
const user = result.rows[0];
|
|
156
|
-
req.session.user = {
|
|
157
|
-
id: user.id,
|
|
158
|
-
username: user.UserName,
|
|
159
|
-
UserName: user.UserName,
|
|
160
|
-
role: user.Role,
|
|
161
|
-
Role: user.Role,
|
|
162
|
-
sessionId: normalizedSessionId,
|
|
163
|
-
allowedApps: user.AllowedApps,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
} catch (err) {
|
|
167
|
-
console.error("[mbkauthe] Session restoration error:", err);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
next();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
|
|
174
|
-
if (req.session?.user) {
|
|
175
|
-
return res.send(`
|
|
176
|
-
<head>
|
|
177
|
-
<script src="/mbkauthe/main.js"></script>
|
|
178
|
-
<style>
|
|
179
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background: #f5f5f5; }
|
|
180
|
-
.card { background: white; border-radius: 8px; padding: 25px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
181
|
-
.success { color: #16a085; border-left: 4px solid #16a085; padding-left: 15px; }
|
|
182
|
-
.user-info { background: #ecf0f1; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 14px; }
|
|
183
|
-
button { background: #e74c3c; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; margin: 10px 5px; }
|
|
184
|
-
button:hover { background: #c0392b; }
|
|
185
|
-
a { color: #3498db; text-decoration: none; margin: 0 10px; padding: 8px 12px; border: 1px solid #3498db; border-radius: 4px; display: inline-block; }
|
|
186
|
-
a:hover { background: #3498db; color: white; }
|
|
187
|
-
</style>
|
|
188
|
-
</head>
|
|
189
|
-
<div class="card">
|
|
190
|
-
<p class="success">✅ Authentication successful! User is logged in.</p>
|
|
191
|
-
<p>Welcome, <strong>${req.session.user.username}</strong>! Your role: <strong>${req.session.user.role}</strong></p>
|
|
192
|
-
<div class="user-info">
|
|
193
|
-
User ID: ${req.session.user.id}<br>
|
|
194
|
-
Session ID: ${req.session.user.sessionId.slice(0, 5)}...
|
|
195
|
-
</div>
|
|
196
|
-
<button onclick="logout()">Logout</button>
|
|
197
|
-
<a href="/mbkauthe/info">Info Page</a>
|
|
198
|
-
<a href="/mbkauthe/login">Login Page</a>
|
|
199
|
-
</div>
|
|
200
|
-
`);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
async function checkTrustedDevice(req, username) {
|
|
205
|
-
const deviceToken = req.cookies.device_token;
|
|
206
|
-
|
|
207
|
-
if (!deviceToken || typeof deviceToken !== 'string') {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const deviceQuery = `
|
|
213
|
-
SELECT td."UserName", td."LastUsed", td."ExpiresAt", u."id", u."Active", u."Role", u."AllowedApps"
|
|
214
|
-
FROM "TrustedDevices" td
|
|
215
|
-
JOIN "Users" u ON td."UserName" = u."UserName"
|
|
216
|
-
WHERE td."DeviceToken" = $1 AND td."UserName" = $2 AND td."ExpiresAt" > NOW()
|
|
217
|
-
`;
|
|
218
|
-
const deviceResult = await dblogin.query({
|
|
219
|
-
name: 'check-trusted-device',
|
|
220
|
-
text: deviceQuery,
|
|
221
|
-
values: [deviceToken, username]
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
if (deviceResult.rows.length > 0) {
|
|
225
|
-
const deviceUser = deviceResult.rows[0];
|
|
226
|
-
|
|
227
|
-
if (!deviceUser.Active) {
|
|
228
|
-
console.log(`[mbkauthe] Trusted device check: inactive account for username: ${username}`);
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (deviceUser.Role !== "SuperAdmin") {
|
|
233
|
-
const allowedApps = deviceUser.AllowedApps;
|
|
234
|
-
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
235
|
-
console.warn(`[mbkauthe] Trusted device check: User "${username}" is not authorized to use the application "${mbkautheVar.APP_NAME}"`);
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Update last used timestamp
|
|
241
|
-
await dblogin.query({
|
|
242
|
-
name: 'update-device-last-used',
|
|
243
|
-
text: 'UPDATE "TrustedDevices" SET "LastUsed" = NOW() WHERE "DeviceToken" = $1',
|
|
244
|
-
values: [deviceToken]
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
console.log(`[mbkauthe] Trusted device validated for user: ${username}`);
|
|
248
|
-
return {
|
|
249
|
-
id: deviceUser.id,
|
|
250
|
-
username: username,
|
|
251
|
-
role: deviceUser.Role,
|
|
252
|
-
Role: deviceUser.Role,
|
|
253
|
-
allowedApps: deviceUser.AllowedApps,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
} catch (deviceErr) {
|
|
257
|
-
console.error("[mbkauthe] Error checking trusted device:", deviceErr);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
|
|
264
|
-
try {
|
|
265
|
-
// Ensure both username formats are available for compatibility
|
|
266
|
-
const username = user.username || user.UserName;
|
|
267
|
-
if (!username) {
|
|
268
|
-
throw new Error('Username is required in user object');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// smaller session id is sufficient and faster to generate/serialize
|
|
272
|
-
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
273
|
-
console.log(`[mbkauthe] Generated session ID for username: ${username}`);
|
|
274
|
-
|
|
275
|
-
// Regenerate session to prevent session fixation attacks
|
|
276
|
-
const oldSessionId = req.sessionID;
|
|
277
|
-
await new Promise((resolve, reject) => {
|
|
278
|
-
req.session.regenerate((err) => {
|
|
279
|
-
if (err) reject(err);
|
|
280
|
-
else resolve();
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Run both queries in parallel for better performance
|
|
285
|
-
await Promise.all([
|
|
286
|
-
// Delete old sessions using indexed lookup on sess->'user'->>'id'
|
|
287
|
-
dblogin.query({
|
|
288
|
-
name: 'login-delete-old-user-sessions',
|
|
289
|
-
text: 'DELETE FROM "session" WHERE (sess->\'user\'->>\'id\')::int = $1',
|
|
290
|
-
values: [user.id]
|
|
291
|
-
}),
|
|
292
|
-
// Update session ID and last login time in Users table
|
|
293
|
-
dblogin.query({
|
|
294
|
-
name: 'login-update-session-and-last-login',
|
|
295
|
-
text: `UPDATE "Users" SET "SessionId" = $1, "last_login" = NOW() WHERE "id" = $2`,
|
|
296
|
-
values: [sessionId, user.id]
|
|
297
|
-
})
|
|
298
|
-
]);
|
|
299
|
-
|
|
300
|
-
req.session.user = {
|
|
301
|
-
id: user.id,
|
|
302
|
-
username: username,
|
|
303
|
-
UserName: username,
|
|
304
|
-
role: user.role || user.Role,
|
|
305
|
-
Role: user.role || user.Role,
|
|
306
|
-
sessionId,
|
|
307
|
-
allowedApps: user.allowedApps || user.AllowedApps,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
if (req.session.preAuthUser) {
|
|
311
|
-
delete req.session.preAuthUser;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
req.session.save(async (err) => {
|
|
315
|
-
if (err) {
|
|
316
|
-
console.error("[mbkauthe] Session save error:", err);
|
|
317
|
-
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
318
|
-
}
|
|
319
|
-
// avoid writing back into the session table here to reduce DB writes;
|
|
320
|
-
// the pg session store will already persist the session data.
|
|
321
|
-
|
|
322
|
-
res.cookie("sessionId", sessionId, cachedCookieOptions);
|
|
323
|
-
|
|
324
|
-
// Handle trusted device if requested
|
|
325
|
-
if (trustDevice) {
|
|
326
|
-
try {
|
|
327
|
-
const deviceToken = generateDeviceToken();
|
|
328
|
-
const deviceName = req.headers['user-agent'] ?
|
|
329
|
-
req.headers['user-agent'].substring(0, 255) : 'Unknown Device';
|
|
330
|
-
const userAgent = req.headers['user-agent'] || 'Unknown';
|
|
331
|
-
const ipAddress = req.ip || req.connection.remoteAddress || 'Unknown';
|
|
332
|
-
const expiresAt = new Date(Date.now() + DEVICE_TRUST_DURATION_MS);
|
|
333
|
-
|
|
334
|
-
await dblogin.query({
|
|
335
|
-
name: 'insert-trusted-device',
|
|
336
|
-
text: `INSERT INTO "TrustedDevices" ("UserName", "DeviceToken", "DeviceName", "UserAgent", "IpAddress", "ExpiresAt")
|
|
337
|
-
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
338
|
-
values: [username, deviceToken, deviceName, userAgent, ipAddress, expiresAt]
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
res.cookie("device_token", deviceToken, getDeviceTokenCookieOptions());
|
|
342
|
-
console.log(`[mbkauthe] Trusted device token created for user: ${username}`);
|
|
343
|
-
} catch (deviceErr) {
|
|
344
|
-
console.error("[mbkauthe] Error creating trusted device:", deviceErr);
|
|
345
|
-
// Continue with login even if device trust fails
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
console.log(`[mbkauthe] User "${username}" logged in successfully (last_login updated)`);
|
|
350
|
-
|
|
351
|
-
const responsePayload = {
|
|
352
|
-
success: true,
|
|
353
|
-
message: "Login successful",
|
|
354
|
-
sessionId,
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
if (redirectUrl) {
|
|
358
|
-
responsePayload.redirectUrl = redirectUrl;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
res.status(200).json(responsePayload);
|
|
362
|
-
});
|
|
363
|
-
} catch (err) {
|
|
364
|
-
console.error("[mbkauthe] Error during login completion:", err);
|
|
365
|
-
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
router.use(async (req, res, next) => {
|
|
370
|
-
if (req.session && req.session.user) {
|
|
371
|
-
// Only set cookies if they're missing or different
|
|
372
|
-
if (req.cookies.sessionId !== req.session.user.sessionId) {
|
|
373
|
-
res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
374
|
-
res.cookie("sessionId", req.session.user.sessionId, cachedCookieOptions);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
next();
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
381
|
-
try {
|
|
382
|
-
// Run both operations in parallel for better performance
|
|
383
|
-
await Promise.all([
|
|
384
|
-
dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` }),
|
|
385
|
-
dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' })
|
|
386
|
-
]);
|
|
387
|
-
|
|
388
|
-
req.session.destroy((err) => {
|
|
389
|
-
if (err) {
|
|
390
|
-
console.log("[mbkauthe] Error destroying session:", err);
|
|
391
|
-
return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
clearSessionCookies(res);
|
|
395
|
-
|
|
396
|
-
console.log("[mbkauthe] All sessions terminated successfully");
|
|
397
|
-
res.status(200).json({
|
|
398
|
-
success: true,
|
|
399
|
-
message: "All sessions terminated successfully",
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
} catch (err) {
|
|
403
|
-
console.error("[mbkauthe] Database query error during session termination:", err);
|
|
404
|
-
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
409
|
-
console.log("[mbkauthe] Login request received");
|
|
410
|
-
|
|
411
|
-
const { username, password, redirect } = req.body;
|
|
412
|
-
|
|
413
|
-
// Input validation
|
|
414
|
-
if (!username || !password) {
|
|
415
|
-
console.log("[mbkauthe] Missing username or password");
|
|
416
|
-
return res.status(400).json({
|
|
417
|
-
success: false,
|
|
418
|
-
message: "Username and password are required",
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Validate username format and length
|
|
423
|
-
if (typeof username !== 'string' || username.trim().length === 0 || username.length > 255) {
|
|
424
|
-
console.warn("[mbkauthe] Invalid username format");
|
|
425
|
-
return res.status(400).json({
|
|
426
|
-
success: false,
|
|
427
|
-
message: "Invalid username format",
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Validate password length
|
|
432
|
-
if (typeof password !== 'string' || password.length < 8 || password.length > 255) {
|
|
433
|
-
console.warn("[mbkauthe] Invalid password length");
|
|
434
|
-
return res.status(400).json({
|
|
435
|
-
success: false,
|
|
436
|
-
message: "Password must be at least 8 characters long",
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
console.log(`[mbkauthe] Login attempt for username: ${username.trim()}`);
|
|
441
|
-
|
|
442
|
-
const trimmedUsername = username.trim();
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
// Combined query: fetch user data and 2FA status in one query
|
|
446
|
-
const userQuery = `
|
|
447
|
-
SELECT u.id, u."UserName", u."Password", u."PasswordEnc", u."Active", u."Role", u."AllowedApps",
|
|
448
|
-
tfa."TwoFAStatus"
|
|
449
|
-
FROM "Users" u
|
|
450
|
-
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
451
|
-
WHERE u."UserName" = $1
|
|
452
|
-
`;
|
|
453
|
-
const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
|
|
454
|
-
|
|
455
|
-
if (userResult.rows.length === 0) {
|
|
456
|
-
console.log(`[mbkauthe] Login failed: invalid credentials`);
|
|
457
|
-
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const user = userResult.rows[0];
|
|
461
|
-
|
|
462
|
-
// Validate user has password field
|
|
463
|
-
if (!user.Password && !user.PasswordEnc) {
|
|
464
|
-
console.error("[mbkauthe] User account has no password set");
|
|
465
|
-
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Check password based on EncPass configuration - ALWAYS validate password first
|
|
469
|
-
let passwordMatches = false;
|
|
470
|
-
console.log(`mbkautheVar.EncPass is set to: ${mbkautheVar.EncPass}`);
|
|
471
|
-
console.log(mbkautheVar.EncPass === "true" || mbkautheVar.EncPass === true);
|
|
472
|
-
if (mbkautheVar.EncPass === "true" || mbkautheVar.EncPass === true) {
|
|
473
|
-
// Use encrypted password comparison
|
|
474
|
-
if (user.PasswordEnc) {
|
|
475
|
-
const hashedInputPassword = hashPassword(password, user.UserName);
|
|
476
|
-
passwordMatches = user.PasswordEnc === hashedInputPassword;
|
|
477
|
-
}
|
|
478
|
-
} else {
|
|
479
|
-
// Use raw password comparison
|
|
480
|
-
if (user.Password) {
|
|
481
|
-
passwordMatches = user.Password === password;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (!passwordMatches) {
|
|
486
|
-
console.log("[mbkauthe] Login failed: invalid credentials");
|
|
487
|
-
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (!user.Active) {
|
|
491
|
-
console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
|
|
492
|
-
return res.status(403).json({ success: false, message: "Account is inactive" });
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (user.Role !== "SuperAdmin") {
|
|
496
|
-
const allowedApps = user.AllowedApps;
|
|
497
|
-
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
498
|
-
console.warn(`[mbkauthe] User \"${user.UserName}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
499
|
-
return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Check for trusted device AFTER password validation - trusted devices should only skip 2FA, not password
|
|
504
|
-
const trustedDeviceUser = await checkTrustedDevice(req, trimmedUsername);
|
|
505
|
-
if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
506
|
-
console.log(`[mbkauthe] Trusted device login for user: ${trimmedUsername}, skipping 2FA only`);
|
|
507
|
-
|
|
508
|
-
// Skip only 2FA, password was already validated above
|
|
509
|
-
const userForSession = {
|
|
510
|
-
id: user.id,
|
|
511
|
-
username: user.UserName,
|
|
512
|
-
role: user.Role,
|
|
513
|
-
Role: user.Role,
|
|
514
|
-
allowedApps: user.AllowedApps,
|
|
515
|
-
};
|
|
516
|
-
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
517
|
-
return await completeLoginProcess(req, res, userForSession, requestedRedirect);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
521
|
-
// 2FA is enabled, prompt for token on a separate page
|
|
522
|
-
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
523
|
-
req.session.preAuthUser = {
|
|
524
|
-
id: user.id,
|
|
525
|
-
username: user.UserName,
|
|
526
|
-
role: user.Role,
|
|
527
|
-
Role: user.Role,
|
|
528
|
-
redirectUrl: requestedRedirect
|
|
529
|
-
};
|
|
530
|
-
console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
|
|
531
|
-
return res.json({ success: true, twoFactorRequired: true, redirectUrl: requestedRedirect });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// If 2FA is not enabled, proceed with login
|
|
535
|
-
const userForSession = {
|
|
536
|
-
id: user.id,
|
|
537
|
-
username: user.UserName,
|
|
538
|
-
role: user.Role,
|
|
539
|
-
Role: user.Role,
|
|
540
|
-
allowedApps: user.AllowedApps,
|
|
541
|
-
};
|
|
542
|
-
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
543
|
-
await completeLoginProcess(req, res, userForSession, requestedRedirect);
|
|
544
|
-
|
|
545
|
-
} catch (err) {
|
|
546
|
-
console.error("[mbkauthe] Error during login process:", err);
|
|
547
|
-
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
552
|
-
if (!req.session.preAuthUser) {
|
|
553
|
-
return res.redirect("/mbkauthe/login");
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Prefer explicit redirect from query string, else from session preAuthUser redirectUrl, else fallback value
|
|
557
|
-
let redirectFromQuery = req.query && typeof req.query.redirect === 'string' ? req.query.redirect : null;
|
|
558
|
-
let redirectToUse = redirectFromQuery || req.session.preAuthUser.redirectUrl || (mbkautheVar.loginRedirectURL || '/dashboard');
|
|
559
|
-
|
|
560
|
-
// Validate redirectToUse to prevent open redirect attacks
|
|
561
|
-
if (redirectToUse && !(typeof redirectToUse === 'string' && redirectToUse.startsWith('/') && !redirectToUse.startsWith('//'))) {
|
|
562
|
-
redirectToUse = mbkautheVar.loginRedirectURL || '/dashboard';
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
res.render("2fa.handlebars", {
|
|
566
|
-
layout: false,
|
|
567
|
-
customURL: redirectToUse,
|
|
568
|
-
csrfToken: req.csrfToken(),
|
|
569
|
-
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
570
|
-
DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
|
|
575
|
-
if (!req.session.preAuthUser) {
|
|
576
|
-
return res.status(401).json({ success: false, message: "Not authorized. Please login first." });
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const { token, trustDevice } = req.body;
|
|
580
|
-
const { username, id, role } = req.session.preAuthUser;
|
|
581
|
-
|
|
582
|
-
// Validate 2FA token
|
|
583
|
-
if (!token || typeof token !== 'string') {
|
|
584
|
-
return res.status(400).json({ success: false, message: "2FA token is required" });
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Validate token format (should be 6 digits)
|
|
588
|
-
const sanitizedToken = token.trim();
|
|
589
|
-
if (!/^\d{6}$/.test(sanitizedToken)) {
|
|
590
|
-
return res.status(400).json({ success: false, message: "Invalid 2FA token format" });
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Validate trustDevice parameter if provided
|
|
594
|
-
const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
|
|
595
|
-
|
|
596
|
-
try {
|
|
597
|
-
const query = `SELECT tfa."TwoFASecret", u."AllowedApps" FROM "TwoFA" tfa JOIN "Users" u ON tfa."UserName" = u."UserName" WHERE tfa."UserName" = $1`;
|
|
598
|
-
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
599
|
-
|
|
600
|
-
if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
|
|
601
|
-
return res.status(500).json({ success: false, message: "2FA is not configured correctly." });
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const sharedSecret = twoFAResult.rows[0].TwoFASecret;
|
|
605
|
-
const allowedApps = twoFAResult.rows[0].AllowedApps;
|
|
606
|
-
const tokenValidates = speakeasy.totp.verify({
|
|
607
|
-
secret: sharedSecret,
|
|
608
|
-
encoding: "base32",
|
|
609
|
-
token: sanitizedToken,
|
|
610
|
-
window: 1,
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
if (!tokenValidates) {
|
|
614
|
-
console.log(`[mbkauthe] Invalid 2FA code for username: ${username}`);
|
|
615
|
-
return res.status(401).json({ success: false, message: "Invalid 2FA code" });
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// 2FA successful, complete login with optional device trust
|
|
619
|
-
const userForSession = { id, username, role, allowedApps };
|
|
620
|
-
// Prefer redirect stored in preAuthUser or in query/body, fallback to configured default
|
|
621
|
-
let redirectFromSession = req.session.preAuthUser && req.session.preAuthUser.redirectUrl ? req.session.preAuthUser.redirectUrl : null;
|
|
622
|
-
if (redirectFromSession && (!(typeof redirectFromSession === 'string') || !redirectFromSession.startsWith('/') || redirectFromSession.startsWith('//'))) {
|
|
623
|
-
redirectFromSession = null;
|
|
624
|
-
}
|
|
625
|
-
const redirectUrl = redirectFromSession || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
626
|
-
// Clear preAuthUser after successful login
|
|
627
|
-
if (req.session.preAuthUser) delete req.session.preAuthUser;
|
|
628
|
-
await completeLoginProcess(req, res, userForSession, redirectUrl, shouldTrustDevice);
|
|
629
|
-
|
|
630
|
-
} catch (err) {
|
|
631
|
-
console.error("[mbkauthe] Error during 2FA verification:", err);
|
|
632
|
-
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
633
|
-
}
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
637
|
-
if (req.session.user) {
|
|
638
|
-
try {
|
|
639
|
-
const { id, username } = req.session.user;
|
|
640
|
-
|
|
641
|
-
// Run both database operations in parallel
|
|
642
|
-
const operations = [
|
|
643
|
-
dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
|
|
644
|
-
];
|
|
645
|
-
|
|
646
|
-
if (req.sessionID) {
|
|
647
|
-
operations.push(
|
|
648
|
-
dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
await Promise.all(operations);
|
|
653
|
-
|
|
654
|
-
req.session.destroy((err) => {
|
|
655
|
-
if (err) {
|
|
656
|
-
console.error("[mbkauthe] Error destroying session:", err);
|
|
657
|
-
return res.status(500).json({ success: false, message: "Logout failed" });
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
clearSessionCookies(res);
|
|
661
|
-
|
|
662
|
-
console.log(`[mbkauthe] User "${username}" logged out successfully`);
|
|
663
|
-
res.status(200).json({ success: true, message: "Logout successful" });
|
|
664
|
-
});
|
|
665
|
-
} catch (err) {
|
|
666
|
-
console.error("[mbkauthe] Database query error during logout:", err);
|
|
667
|
-
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
668
|
-
}
|
|
669
|
-
} else {
|
|
670
|
-
res.status(400).json({ success: false, message: "Not logged in" });
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
|
|
675
|
-
return res.render("loginmbkauthe.handlebars", {
|
|
676
|
-
layout: false,
|
|
677
|
-
githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
|
|
678
|
-
customURL: mbkautheVar.loginRedirectURL || '/dashboard',
|
|
679
|
-
userLoggedIn: !!req.session?.user,
|
|
680
|
-
username: req.session?.user?.username || '',
|
|
681
|
-
version: packageJson.version,
|
|
682
|
-
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
683
|
-
csrfToken: req.csrfToken(),
|
|
684
|
-
});
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
async function getLatestVersion() {
|
|
688
|
-
try {
|
|
689
|
-
const response = await fetch('https://raw.githubusercontent.com/MIbnEKhalid/mbkauthe/main/package.json');
|
|
690
|
-
if (!response.ok) {
|
|
691
|
-
console.error(`GitHub API responded with status ${response.status}`);
|
|
692
|
-
return "0.0.0";
|
|
693
|
-
}
|
|
694
|
-
const latestPackageJson = await response.json();
|
|
695
|
-
return latestPackageJson.version;
|
|
696
|
-
} catch (error) {
|
|
697
|
-
console.error('[mbkauthe] Error fetching latest version from GitHub:', error);
|
|
698
|
-
return null;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
|
|
703
|
-
let latestVersion;
|
|
704
|
-
const parameters = req.query;
|
|
705
|
-
let authorized = false;
|
|
706
|
-
|
|
707
|
-
if (parameters.password && mbkautheVar.Main_SECRET_TOKEN) {
|
|
708
|
-
authorized = String(parameters.password) === String(mbkautheVar.Main_SECRET_TOKEN);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
try {
|
|
712
|
-
latestVersion = await getLatestVersion();
|
|
713
|
-
//latestVersion = "Under Development"; // Placeholder for the latest version
|
|
714
|
-
} catch (err) {
|
|
715
|
-
console.error("[mbkauthe] Error fetching package-lock.json:", err);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
res.render("info.handlebars", {
|
|
720
|
-
layout: false,
|
|
721
|
-
mbkautheVar: mbkautheVar,
|
|
722
|
-
version: packageJson.version,
|
|
723
|
-
APP_VERSION: appVersion,
|
|
724
|
-
latestVersion,
|
|
725
|
-
authorized: authorized,
|
|
726
|
-
});
|
|
727
|
-
} catch (err) {
|
|
728
|
-
console.error("[mbkauthe] Error fetching version information:", err);
|
|
729
|
-
res.status(500).send(`
|
|
730
|
-
<html>
|
|
731
|
-
<head>
|
|
732
|
-
<title>Error</title>
|
|
733
|
-
</head>
|
|
734
|
-
<body>
|
|
735
|
-
<h1>Error</h1>
|
|
736
|
-
<p>Failed to fetch version information. Please try again later.</p>
|
|
737
|
-
</body>
|
|
738
|
-
</html>
|
|
739
|
-
`);
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
// Configure GitHub Strategy for login
|
|
744
|
-
passport.use('github-login', new GitHubStrategy({
|
|
745
|
-
clientID: mbkautheVar.GITHUB_CLIENT_ID,
|
|
746
|
-
clientSecret: mbkautheVar.GITHUB_CLIENT_SECRET,
|
|
747
|
-
callbackURL: '/mbkauthe/api/github/login/callback',
|
|
748
|
-
scope: ['user:email']
|
|
749
|
-
},
|
|
750
|
-
async (accessToken, refreshToken, profile, done) => {
|
|
751
|
-
try {
|
|
752
|
-
// Check if this GitHub account is linked to any user
|
|
753
|
-
const githubUser = await dblogin.query({
|
|
754
|
-
name: 'github-login-get-user',
|
|
755
|
-
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',
|
|
756
|
-
values: [profile.id]
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
if (githubUser.rows.length === 0) {
|
|
760
|
-
// GitHub account is not linked to any user
|
|
761
|
-
const error = new Error('GitHub account not linked to any user');
|
|
762
|
-
error.code = 'GITHUB_NOT_LINKED';
|
|
763
|
-
return done(error);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const user = githubUser.rows[0];
|
|
767
|
-
|
|
768
|
-
// Check if the user account is active
|
|
769
|
-
if (!user.Active) {
|
|
770
|
-
const error = new Error('Account is inactive');
|
|
771
|
-
error.code = 'ACCOUNT_INACTIVE';
|
|
772
|
-
return done(error);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Check if user is authorized for this app (same logic as regular login)
|
|
776
|
-
if (user.Role !== "SuperAdmin") {
|
|
777
|
-
const allowedApps = user.AllowedApps;
|
|
778
|
-
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
779
|
-
const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
|
|
780
|
-
error.code = 'NOT_AUTHORIZED';
|
|
781
|
-
return done(error);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Return user data for login
|
|
786
|
-
return done(null, {
|
|
787
|
-
id: user.id, // This should be the user ID from the Users table
|
|
788
|
-
username: user.UserName,
|
|
789
|
-
role: user.Role,
|
|
790
|
-
githubId: user.github_id,
|
|
791
|
-
githubUsername: user.github_username
|
|
792
|
-
});
|
|
793
|
-
} catch (err) {
|
|
794
|
-
console.error('[mbkauthe] GitHub login error:', err);
|
|
795
|
-
err.code = err.code || 'GITHUB_AUTH_ERROR';
|
|
796
|
-
return done(err);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
));
|
|
800
|
-
|
|
801
|
-
// Serialize/Deserialize user for GitHub login
|
|
802
|
-
passport.serializeUser((user, done) => {
|
|
803
|
-
done(null, user);
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
passport.deserializeUser((user, done) => {
|
|
807
|
-
done(null, user);
|
|
808
|
-
});
|
|
29
|
+
// Session restoration
|
|
30
|
+
router.use(sessionRestorationMiddleware);
|
|
809
31
|
|
|
810
32
|
// Initialize passport
|
|
811
33
|
router.use(passport.initialize());
|
|
812
34
|
router.use(passport.session());
|
|
813
35
|
|
|
814
|
-
//
|
|
815
|
-
router.
|
|
816
|
-
if (mbkautheVar.GITHUB_LOGIN_ENABLED) {
|
|
817
|
-
// Store redirect parameter in session before OAuth flow (validate to prevent open redirect)
|
|
818
|
-
const redirect = req.query.redirect;
|
|
819
|
-
if (redirect && typeof redirect === 'string') {
|
|
820
|
-
// Only allow relative URLs or same-origin URLs to prevent open redirect attacks
|
|
821
|
-
if (redirect.startsWith('/') && !redirect.startsWith('//')) {
|
|
822
|
-
req.session.oauthRedirect = redirect;
|
|
823
|
-
} else {
|
|
824
|
-
console.warn(`[mbkauthe] Invalid redirect parameter rejected: ${redirect}`);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
passport.authenticate('github-login')(req, res, next);
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
return renderError(res, {
|
|
831
|
-
code: '403',
|
|
832
|
-
error: 'GitHub Login Disabled',
|
|
833
|
-
message: 'GitHub login is currently disabled. Please use your username and password to log in.',
|
|
834
|
-
page: '/mbkauthe/login',
|
|
835
|
-
pagename: 'Login',
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
// GitHub login callback
|
|
841
|
-
router.get('/mbkauthe/api/github/login/callback',
|
|
842
|
-
GitHubOAuthLimit,
|
|
843
|
-
(req, res, next) => {
|
|
844
|
-
passport.authenticate('github-login', {
|
|
845
|
-
session: false // We'll handle session manually
|
|
846
|
-
}, (err, user, info) => {
|
|
847
|
-
// Custom error handling for passport authentication
|
|
848
|
-
if (err) {
|
|
849
|
-
console.error('[mbkauthe] GitHub authentication error:', err);
|
|
850
|
-
|
|
851
|
-
// Map error codes to user-friendly messages
|
|
852
|
-
switch (err.code) {
|
|
853
|
-
case 'GITHUB_NOT_LINKED':
|
|
854
|
-
return renderError(res, {
|
|
855
|
-
code: '403',
|
|
856
|
-
error: 'GitHub Account Not Linked',
|
|
857
|
-
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.',
|
|
858
|
-
page: '/mbkauthe/login',
|
|
859
|
-
pagename: 'Login'
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
case 'ACCOUNT_INACTIVE':
|
|
863
|
-
return renderError(res, {
|
|
864
|
-
code: '403',
|
|
865
|
-
error: 'Account Inactive',
|
|
866
|
-
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
867
|
-
page: '/mbkauthe/login',
|
|
868
|
-
pagename: 'Login'
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
case 'NOT_AUTHORIZED':
|
|
872
|
-
return renderError(res, {
|
|
873
|
-
code: '403',
|
|
874
|
-
error: 'Not Authorized',
|
|
875
|
-
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
876
|
-
page: '/mbkauthe/login',
|
|
877
|
-
pagename: 'Login'
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
default:
|
|
881
|
-
return renderError(res, {
|
|
882
|
-
code: '500',
|
|
883
|
-
error: 'Authentication Error',
|
|
884
|
-
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
885
|
-
page: '/mbkauthe/login',
|
|
886
|
-
pagename: 'Login',
|
|
887
|
-
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
if (!user) {
|
|
893
|
-
console.error('[mbkauthe] GitHub callback: No user data received');
|
|
894
|
-
return renderError(res, {
|
|
895
|
-
code: '401',
|
|
896
|
-
error: 'Authentication Failed',
|
|
897
|
-
message: 'GitHub authentication failed. Please try again.',
|
|
898
|
-
page: '/mbkauthe/login',
|
|
899
|
-
pagename: 'Login'
|
|
900
|
-
});
|
|
901
|
-
}
|
|
36
|
+
// Session cookie sync
|
|
37
|
+
router.use(sessionCookieSyncMiddleware);
|
|
902
38
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
},
|
|
908
|
-
async (req, res) => {
|
|
909
|
-
try {
|
|
910
|
-
const githubUser = req.user;
|
|
39
|
+
// Mount routes (rate limiting is applied within each route module)
|
|
40
|
+
router.use('/mbkauthe', authRoutes);
|
|
41
|
+
router.use('/mbkauthe', oauthRoutes);
|
|
42
|
+
router.use('/mbkauthe', miscRoutes);
|
|
911
43
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
WHERE u."UserName" = $1
|
|
919
|
-
`;
|
|
920
|
-
const userResult = await dblogin.query({
|
|
921
|
-
name: 'github-callback-get-user',
|
|
922
|
-
text: userQuery,
|
|
923
|
-
values: [githubUser.username]
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
if (userResult.rows.length === 0) {
|
|
927
|
-
console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
|
|
928
|
-
return renderError(res, {
|
|
929
|
-
code: '404',
|
|
930
|
-
error: 'User Not Found',
|
|
931
|
-
message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
|
|
932
|
-
page: '/mbkauthe/login',
|
|
933
|
-
pagename: 'Login',
|
|
934
|
-
details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const user = userResult.rows[0];
|
|
939
|
-
|
|
940
|
-
// Check for trusted device after OAuth authentication - should only skip 2FA, not OAuth validation
|
|
941
|
-
const trustedDeviceUser = await checkTrustedDevice(req, user.UserName);
|
|
942
|
-
if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
943
|
-
console.log(`[mbkauthe] GitHub trusted device login for user: ${user.UserName}, skipping 2FA only`);
|
|
944
|
-
|
|
945
|
-
// Complete login process using the shared function
|
|
946
|
-
const userForSession = {
|
|
947
|
-
id: user.id,
|
|
948
|
-
username: user.UserName,
|
|
949
|
-
UserName: user.UserName,
|
|
950
|
-
role: user.Role,
|
|
951
|
-
Role: user.Role,
|
|
952
|
-
allowedApps: user.AllowedApps,
|
|
953
|
-
};
|
|
954
|
-
|
|
955
|
-
// For OAuth redirect flow, we need to handle redirect differently
|
|
956
|
-
// Store the redirect URL before calling completeLoginProcess
|
|
957
|
-
const oauthRedirect = req.session.oauthRedirect;
|
|
958
|
-
delete req.session.oauthRedirect;
|
|
959
|
-
|
|
960
|
-
// Custom response handler for OAuth flow - wrap the response object
|
|
961
|
-
const originalJson = res.json.bind(res);
|
|
962
|
-
const originalStatus = res.status.bind(res);
|
|
963
|
-
let statusCode = 200;
|
|
964
|
-
|
|
965
|
-
res.status = function (code) {
|
|
966
|
-
statusCode = code;
|
|
967
|
-
return originalStatus(code);
|
|
968
|
-
};
|
|
969
|
-
|
|
970
|
-
res.json = function (data) {
|
|
971
|
-
if (data.success && statusCode === 200) {
|
|
972
|
-
// If login successful, redirect instead of sending JSON
|
|
973
|
-
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
974
|
-
console.log(`[mbkauthe] GitHub trusted device login: Redirecting to ${redirectUrl}`);
|
|
975
|
-
// Restore original methods before redirect
|
|
976
|
-
res.json = originalJson;
|
|
977
|
-
res.status = originalStatus;
|
|
978
|
-
return res.redirect(redirectUrl);
|
|
979
|
-
}
|
|
980
|
-
// Restore original methods for error responses
|
|
981
|
-
res.json = originalJson;
|
|
982
|
-
res.status = originalStatus;
|
|
983
|
-
return originalJson(data);
|
|
984
|
-
};
|
|
985
|
-
|
|
986
|
-
return await completeLoginProcess(req, res, userForSession);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Check 2FA if enabled
|
|
990
|
-
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
991
|
-
// 2FA is enabled, store pre-auth user and redirect to 2FA
|
|
992
|
-
// If this was an oauth flow, use oauthRedirect set earlier
|
|
993
|
-
const oauthRedirect = req.session.oauthRedirect;
|
|
994
|
-
if (oauthRedirect) delete req.session.oauthRedirect;
|
|
995
|
-
req.session.preAuthUser = {
|
|
996
|
-
id: user.id,
|
|
997
|
-
username: user.UserName,
|
|
998
|
-
UserName: user.UserName,
|
|
999
|
-
role: user.Role,
|
|
1000
|
-
Role: user.Role,
|
|
1001
|
-
loginMethod: 'github',
|
|
1002
|
-
redirectUrl: oauthRedirect || null
|
|
1003
|
-
};
|
|
1004
|
-
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
1005
|
-
return res.redirect('/mbkauthe/2fa');
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Complete login process using the shared function
|
|
1009
|
-
const userForSession = {
|
|
1010
|
-
id: user.id,
|
|
1011
|
-
username: user.UserName,
|
|
1012
|
-
UserName: user.UserName,
|
|
1013
|
-
role: user.Role,
|
|
1014
|
-
Role: user.Role,
|
|
1015
|
-
allowedApps: user.AllowedApps,
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
// For OAuth redirect flow, we need to handle redirect differently
|
|
1019
|
-
// Store the redirect URL before calling completeLoginProcess
|
|
1020
|
-
const oauthRedirect = req.session.oauthRedirect;
|
|
1021
|
-
delete req.session.oauthRedirect;
|
|
1022
|
-
|
|
1023
|
-
// Custom response handler for OAuth flow - wrap the response object
|
|
1024
|
-
const originalJson = res.json.bind(res);
|
|
1025
|
-
const originalStatus = res.status.bind(res);
|
|
1026
|
-
let statusCode = 200;
|
|
1027
|
-
|
|
1028
|
-
res.status = function (code) {
|
|
1029
|
-
statusCode = code;
|
|
1030
|
-
return originalStatus(code);
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
res.json = function (data) {
|
|
1034
|
-
if (data.success && statusCode === 200) {
|
|
1035
|
-
// If login successful, redirect instead of sending JSON
|
|
1036
|
-
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
1037
|
-
console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
|
|
1038
|
-
// Restore original methods before redirect
|
|
1039
|
-
res.json = originalJson;
|
|
1040
|
-
res.status = originalStatus;
|
|
1041
|
-
return res.redirect(redirectUrl);
|
|
1042
|
-
}
|
|
1043
|
-
// Restore original methods for error responses
|
|
1044
|
-
res.json = originalJson;
|
|
1045
|
-
res.status = originalStatus;
|
|
1046
|
-
return originalJson(data);
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
|
-
await completeLoginProcess(req, res, userForSession);
|
|
1050
|
-
|
|
1051
|
-
} catch (err) {
|
|
1052
|
-
console.error('[mbkauthe] GitHub login callback error:', err);
|
|
1053
|
-
return renderError(res, {
|
|
1054
|
-
code: '500',
|
|
1055
|
-
error: 'Internal Server Error',
|
|
1056
|
-
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
1057
|
-
page: '/mbkauthe/login',
|
|
1058
|
-
pagename: 'Login',
|
|
1059
|
-
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
);
|
|
44
|
+
// Redirect shortcuts for login
|
|
45
|
+
router.get(["/login", "/signin"], async (req, res) => {
|
|
46
|
+
const queryParams = new URLSearchParams(req.query).toString();
|
|
47
|
+
const redirectUrl = `/mbkauthe/login${queryParams ? `?${queryParams}` : ''}`;
|
|
48
|
+
return res.redirect(redirectUrl);
|
|
49
|
+
});
|
|
1064
50
|
|
|
1065
|
-
export { getLatestVersion };
|
|
51
|
+
export { getLatestVersion } from "./routes/misc.js";
|
|
1066
52
|
export default router;
|