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 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.status(404).render("Error/dError.handlebars", {
19
+ return renderError(res, {
20
20
  layout: false,
21
21
  code: 404,
22
22
  error: "Not Found",
package/lib/config.js CHANGED
@@ -158,6 +158,7 @@ try {
158
158
 
159
159
  // Helper function to render error pages consistently
160
160
  const renderError = (res, { code, error, message, page, pagename, details }) => {
161
+ res.status(code);
161
162
  const renderData = {
162
163
  layout: false,
163
164
  code,
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
- return await completeLoginProcess(req, res, userForSession);
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
- await completeLoginProcess(req, res, userForSession);
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: mbkautheVar.loginRedirectURL || '/dashboard',
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
- const redirectUrl = mbkautheVar.loginRedirectURL || '/dashboard';
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 res.status(403).send(renderError(res, {
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
- }).render());
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 res.status(403).send(renderError(res, {
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
- }).render());
831
+ });
810
832
 
811
833
  case 'ACCOUNT_INACTIVE':
812
- return res.status(403).send(renderError(res, {
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
- }).render());
840
+ });
819
841
 
820
842
  case 'NOT_AUTHORIZED':
821
- return res.status(403).send(renderError(res, {
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
- }).render());
849
+ });
828
850
 
829
851
  default:
830
- return res.status(500).send(renderError(res, {
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
- }).render());
859
+ });
838
860
  }
839
861
  }
840
862
 
841
863
  if (!user) {
842
864
  console.error('[mbkauthe] GitHub callback: No user data received');
843
- return res.status(401).send(renderError(res, {
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
- }).render());
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 res.status(404).send(renderError(res, {
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
- }).render());
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 res.status(500).send(renderError(res, {
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
- }).render());
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
- if (result.rows.length === 0 || result.rows[0].SessionId.toLowerCase() !== normalizedSessionId) {
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
- if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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 || '{{{ customURL }}}';
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
- window.location.href = '/mbkauthe/2fa';
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: #24292e;
282
+ background: #262b30;
283
283
  color: white;
284
- font-weight: 500;
285
- font-size: 0.95rem;
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: #3b4045;
293
- box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
294
- transform: translateY(-2px);
292
+ background: transparent;
293
+ box-shadow: var(--box-shadow);
295
294
  }
296
295
 
297
296
  .btn-github i {