mbkauthe 2.2.0 → 2.4.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/index.js +1 -1
- package/lib/config.js +1 -0
- package/lib/main.js +52 -26
- package/lib/validateSessionAndRole.js +22 -2
- package/package.json +1 -1
- package/views/2fa.handlebars +1 -1
- package/views/loginmbkauthe.handlebars +7 -2
- package/views/sharedStyles.handlebars +6 -7
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ if (process.env.test === "dev") {
|
|
|
16
16
|
app.use(router);
|
|
17
17
|
app.use((req, res) => {
|
|
18
18
|
console.log(`Path not found: ${req.method} ${req.url}`);
|
|
19
|
-
return res
|
|
19
|
+
return renderError(res, {
|
|
20
20
|
layout: false,
|
|
21
21
|
code: 404,
|
|
22
22
|
error: "Not Found",
|
package/lib/config.js
CHANGED
package/lib/main.js
CHANGED
|
@@ -328,7 +328,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
|
|
|
328
328
|
router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
329
329
|
console.log("[mbkauthe] Login request received");
|
|
330
330
|
|
|
331
|
-
const { username, password } = req.body;
|
|
331
|
+
const { username, password, redirect } = req.body;
|
|
332
332
|
|
|
333
333
|
// Input validation
|
|
334
334
|
if (!username || !password) {
|
|
@@ -401,7 +401,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
401
401
|
|
|
402
402
|
if (deviceUser.Role !== "SuperAdmin") {
|
|
403
403
|
const allowedApps = deviceUser.AllowedApps;
|
|
404
|
-
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
404
|
+
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
405
405
|
console.warn(`[mbkauthe] User \"${trimmedUsername}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
406
406
|
return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
|
|
407
407
|
}
|
|
@@ -424,7 +424,9 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
424
424
|
Role: deviceUser.Role,
|
|
425
425
|
allowedApps: deviceUser.AllowedApps,
|
|
426
426
|
};
|
|
427
|
-
|
|
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);
|
|
428
430
|
}
|
|
429
431
|
} catch (deviceErr) {
|
|
430
432
|
console.error("[mbkauthe] Error checking trusted device:", deviceErr);
|
|
@@ -481,7 +483,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
481
483
|
|
|
482
484
|
if (user.Role !== "SuperAdmin") {
|
|
483
485
|
const allowedApps = user.AllowedApps;
|
|
484
|
-
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
486
|
+
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
485
487
|
console.warn(`[mbkauthe] User \"${user.UserName}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
486
488
|
return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
|
|
487
489
|
}
|
|
@@ -489,14 +491,16 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
489
491
|
|
|
490
492
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
491
493
|
// 2FA is enabled, prompt for token on a separate page
|
|
494
|
+
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
492
495
|
req.session.preAuthUser = {
|
|
493
496
|
id: user.id,
|
|
494
497
|
username: user.UserName,
|
|
495
498
|
role: user.Role,
|
|
496
499
|
Role: user.Role,
|
|
500
|
+
redirectUrl: requestedRedirect
|
|
497
501
|
};
|
|
498
502
|
console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
|
|
499
|
-
return res.json({ success: true, twoFactorRequired: true });
|
|
503
|
+
return res.json({ success: true, twoFactorRequired: true, redirectUrl: requestedRedirect });
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
// If 2FA is not enabled, proceed with login
|
|
@@ -507,7 +511,8 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
507
511
|
Role: user.Role,
|
|
508
512
|
allowedApps: user.AllowedApps,
|
|
509
513
|
};
|
|
510
|
-
|
|
514
|
+
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
515
|
+
await completeLoginProcess(req, res, userForSession, requestedRedirect);
|
|
511
516
|
|
|
512
517
|
} catch (err) {
|
|
513
518
|
console.error("[mbkauthe] Error during login process:", err);
|
|
@@ -519,9 +524,19 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
519
524
|
if (!req.session.preAuthUser) {
|
|
520
525
|
return res.redirect("/mbkauthe/login");
|
|
521
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
|
+
|
|
522
537
|
res.render("2fa.handlebars", {
|
|
523
538
|
layout: false,
|
|
524
|
-
customURL:
|
|
539
|
+
customURL: redirectToUse,
|
|
525
540
|
csrfToken: req.csrfToken(),
|
|
526
541
|
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
527
542
|
DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
|
|
@@ -574,7 +589,14 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
574
589
|
|
|
575
590
|
// 2FA successful, complete login with optional device trust
|
|
576
591
|
const userForSession = { id, username, role, allowedApps };
|
|
577
|
-
|
|
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;
|
|
578
600
|
await completeLoginProcess(req, res, userForSession, redirectUrl, shouldTrustDevice);
|
|
579
601
|
|
|
580
602
|
} catch (err) {
|
|
@@ -724,7 +746,7 @@ passport.use('github-login', new GitHubStrategy({
|
|
|
724
746
|
// Check if user is authorized for this app (same logic as regular login)
|
|
725
747
|
if (user.Role !== "SuperAdmin") {
|
|
726
748
|
const allowedApps = user.AllowedApps;
|
|
727
|
-
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
749
|
+
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
728
750
|
const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
|
|
729
751
|
error.code = 'NOT_AUTHORIZED';
|
|
730
752
|
return done(error);
|
|
@@ -776,13 +798,13 @@ router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
|
|
|
776
798
|
passport.authenticate('github-login')(req, res, next);
|
|
777
799
|
}
|
|
778
800
|
else {
|
|
779
|
-
return
|
|
801
|
+
return renderError(res, {
|
|
780
802
|
code: '403',
|
|
781
803
|
error: 'GitHub Login Disabled',
|
|
782
804
|
message: 'GitHub login is currently disabled. Please use your username and password to log in.',
|
|
783
805
|
page: '/mbkauthe/login',
|
|
784
806
|
pagename: 'Login',
|
|
785
|
-
})
|
|
807
|
+
});
|
|
786
808
|
}
|
|
787
809
|
});
|
|
788
810
|
|
|
@@ -800,53 +822,53 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
800
822
|
// Map error codes to user-friendly messages
|
|
801
823
|
switch (err.code) {
|
|
802
824
|
case 'GITHUB_NOT_LINKED':
|
|
803
|
-
return
|
|
825
|
+
return renderError(res, {
|
|
804
826
|
code: '403',
|
|
805
827
|
error: 'GitHub Account Not Linked',
|
|
806
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.',
|
|
807
829
|
page: '/mbkauthe/login',
|
|
808
830
|
pagename: 'Login'
|
|
809
|
-
})
|
|
831
|
+
});
|
|
810
832
|
|
|
811
833
|
case 'ACCOUNT_INACTIVE':
|
|
812
|
-
return
|
|
834
|
+
return renderError(res, {
|
|
813
835
|
code: '403',
|
|
814
836
|
error: 'Account Inactive',
|
|
815
837
|
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
816
838
|
page: '/mbkauthe/login',
|
|
817
839
|
pagename: 'Login'
|
|
818
|
-
})
|
|
840
|
+
});
|
|
819
841
|
|
|
820
842
|
case 'NOT_AUTHORIZED':
|
|
821
|
-
return
|
|
843
|
+
return renderError(res, {
|
|
822
844
|
code: '403',
|
|
823
845
|
error: 'Not Authorized',
|
|
824
846
|
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
825
847
|
page: '/mbkauthe/login',
|
|
826
848
|
pagename: 'Login'
|
|
827
|
-
})
|
|
849
|
+
});
|
|
828
850
|
|
|
829
851
|
default:
|
|
830
|
-
return
|
|
852
|
+
return renderError(res, {
|
|
831
853
|
code: '500',
|
|
832
854
|
error: 'Authentication Error',
|
|
833
855
|
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
834
856
|
page: '/mbkauthe/login',
|
|
835
857
|
pagename: 'Login',
|
|
836
858
|
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
837
|
-
})
|
|
859
|
+
});
|
|
838
860
|
}
|
|
839
861
|
}
|
|
840
862
|
|
|
841
863
|
if (!user) {
|
|
842
864
|
console.error('[mbkauthe] GitHub callback: No user data received');
|
|
843
|
-
return
|
|
865
|
+
return renderError(res, {
|
|
844
866
|
code: '401',
|
|
845
867
|
error: 'Authentication Failed',
|
|
846
868
|
message: 'GitHub authentication failed. Please try again.',
|
|
847
869
|
page: '/mbkauthe/login',
|
|
848
870
|
pagename: 'Login'
|
|
849
|
-
})
|
|
871
|
+
});
|
|
850
872
|
}
|
|
851
873
|
|
|
852
874
|
// Authentication successful, attach user to request
|
|
@@ -874,14 +896,14 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
874
896
|
|
|
875
897
|
if (userResult.rows.length === 0) {
|
|
876
898
|
console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
|
|
877
|
-
return
|
|
899
|
+
return renderError(res, {
|
|
878
900
|
code: '404',
|
|
879
901
|
error: 'User Not Found',
|
|
880
902
|
message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
|
|
881
903
|
page: '/mbkauthe/login',
|
|
882
904
|
pagename: 'Login',
|
|
883
905
|
details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
|
|
884
|
-
})
|
|
906
|
+
});
|
|
885
907
|
}
|
|
886
908
|
|
|
887
909
|
const user = userResult.rows[0];
|
|
@@ -889,13 +911,17 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
889
911
|
// Check 2FA if enabled
|
|
890
912
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
891
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;
|
|
892
917
|
req.session.preAuthUser = {
|
|
893
918
|
id: user.id,
|
|
894
919
|
username: user.UserName,
|
|
895
920
|
UserName: user.UserName,
|
|
896
921
|
role: user.Role,
|
|
897
922
|
Role: user.Role,
|
|
898
|
-
loginMethod: 'github'
|
|
923
|
+
loginMethod: 'github',
|
|
924
|
+
redirectUrl: oauthRedirect || null
|
|
899
925
|
};
|
|
900
926
|
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
901
927
|
return res.redirect('/mbkauthe/2fa');
|
|
@@ -946,14 +972,14 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
946
972
|
|
|
947
973
|
} catch (err) {
|
|
948
974
|
console.error('[mbkauthe] GitHub login callback error:', err);
|
|
949
|
-
return
|
|
975
|
+
return renderError(res, {
|
|
950
976
|
code: '500',
|
|
951
977
|
error: 'Internal Server Error',
|
|
952
978
|
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
953
979
|
page: '/mbkauthe/login',
|
|
954
980
|
pagename: 'Login',
|
|
955
981
|
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
956
|
-
})
|
|
982
|
+
});
|
|
957
983
|
}
|
|
958
984
|
}
|
|
959
985
|
);
|
|
@@ -17,6 +17,20 @@ async function validateSession(req, res, next) {
|
|
|
17
17
|
try {
|
|
18
18
|
const { id, sessionId, role, allowedApps } = req.session.user;
|
|
19
19
|
|
|
20
|
+
// Defensive checks for sessionId and allowedApps
|
|
21
|
+
if (!sessionId) {
|
|
22
|
+
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
23
|
+
req.session.destroy();
|
|
24
|
+
clearSessionCookies(res);
|
|
25
|
+
return renderError(res, {
|
|
26
|
+
code: 401,
|
|
27
|
+
error: "Session Expired",
|
|
28
|
+
message: "Your Session Has Expired. Please Log In Again.",
|
|
29
|
+
pagename: "Login",
|
|
30
|
+
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
// Normalize sessionId to lowercase for consistent comparison
|
|
21
35
|
const normalizedSessionId = sessionId.toLowerCase();
|
|
22
36
|
|
|
@@ -24,7 +38,11 @@ async function validateSession(req, res, next) {
|
|
|
24
38
|
const query = `SELECT "SessionId", "Active", "Role" FROM "Users" WHERE "id" = $1`;
|
|
25
39
|
const result = await dblogin.query({ name: 'validate-user-session', text: query, values: [id] });
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
const dbSessionId = result.rows.length > 0 && result.rows[0].SessionId ? String(result.rows[0].SessionId).toLowerCase() : null;
|
|
42
|
+
if (!dbSessionId || dbSessionId !== normalizedSessionId) {
|
|
43
|
+
if (result.rows.length > 0 && !result.rows[0].SessionId) {
|
|
44
|
+
console.warn(`[mbkauthe] DB sessionId is null for user "${req.session.user.username}"`);
|
|
45
|
+
}
|
|
28
46
|
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
29
47
|
req.session.destroy();
|
|
30
48
|
clearSessionCookies(res);
|
|
@@ -51,7 +69,9 @@ async function validateSession(req, res, next) {
|
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
if (role !== "SuperAdmin") {
|
|
54
|
-
|
|
72
|
+
// If allowedApps is not provided or not an array, treat as no access
|
|
73
|
+
const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
|
|
74
|
+
if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
55
75
|
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
56
76
|
req.session.destroy();
|
|
57
77
|
clearSessionCookies(res);
|
package/package.json
CHANGED
package/views/2fa.handlebars
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
const data = await response.json();
|
|
88
88
|
if (data.success) {
|
|
89
89
|
loginButtonText.textContent = 'Success! Redirecting...';
|
|
90
|
-
window.location.href = data.redirectUrl || '{{
|
|
90
|
+
window.location.href = data.redirectUrl || '{{ customURL }}';
|
|
91
91
|
} else {
|
|
92
92
|
loginButton.disabled = false;
|
|
93
93
|
loginButtonText.textContent = 'Verify';
|
|
@@ -130,6 +130,8 @@
|
|
|
130
130
|
loginButton.disabled = true;
|
|
131
131
|
loginButtonText.textContent = 'Authenticating...';
|
|
132
132
|
|
|
133
|
+
// Pass redirect query param through to server so it can be used by 2FA flow
|
|
134
|
+
const pageRedirect = new URLSearchParams(window.location.search).get('redirect');
|
|
133
135
|
fetch('/mbkauthe/api/login', {
|
|
134
136
|
method: 'POST',
|
|
135
137
|
headers: {
|
|
@@ -138,14 +140,17 @@
|
|
|
138
140
|
body: JSON.stringify({
|
|
139
141
|
username,
|
|
140
142
|
password
|
|
143
|
+
, redirect: pageRedirect || null
|
|
141
144
|
})
|
|
142
145
|
})
|
|
143
146
|
.then(response => response.json())
|
|
144
147
|
.then(data => {
|
|
145
148
|
if (data.success) {
|
|
146
149
|
if (data.twoFactorRequired) {
|
|
147
|
-
// Redirect to 2FA page
|
|
148
|
-
|
|
150
|
+
// Redirect to 2FA page (= pass along redirect target)
|
|
151
|
+
const redirectTarget = data.redirectUrl || pageRedirect;
|
|
152
|
+
const redirectQuery = redirectTarget ? `?redirect=${encodeURIComponent(redirectTarget)}` : '';
|
|
153
|
+
window.location.href = `/mbkauthe/2fa${redirectQuery}`;
|
|
149
154
|
} else {
|
|
150
155
|
loginButtonText.textContent = 'Success! Redirecting...';
|
|
151
156
|
sessionStorage.setItem('sessionId', data.sessionId);
|
|
@@ -279,19 +279,18 @@
|
|
|
279
279
|
width: 100%;
|
|
280
280
|
padding: 12px 20px;
|
|
281
281
|
border-radius: var(--border-radius);
|
|
282
|
-
background: #
|
|
282
|
+
background: #262b30;
|
|
283
283
|
color: white;
|
|
284
|
-
|
|
285
|
-
font-
|
|
284
|
+
border: solid 0.13rem #262b30;
|
|
285
|
+
font-weight: 700;
|
|
286
|
+
font-size: 1rem;
|
|
286
287
|
text-decoration: none;
|
|
287
288
|
transition: var(--transition);
|
|
288
|
-
box-shadow: var(--box-shadow);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
.btn-github:hover {
|
|
292
|
-
background:
|
|
293
|
-
box-shadow:
|
|
294
|
-
transform: translateY(-2px);
|
|
292
|
+
background: transparent;
|
|
293
|
+
box-shadow: var(--box-shadow);
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
.btn-github i {
|