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