mbkauthe 2.3.0 → 2.5.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 CHANGED
@@ -1,3 +1,3 @@
1
- mbkautheVar={"APP_NAME":"mbkauthe","Main_SECRET_TOKEN": 123,"SESSION_SECRET_KEY":"123","IS_DEPLOYED":"true","LOGIN_DB":"postgres://","MBKAUTH_TWO_FA_ENABLE":"true","COOKIE_EXPIRE_TIME":2,"DOMAIN":"mbktech.org","loginRedirectURL":"/mbkauthe/test","GITHUB_LOGIN_ENABLED":"true","GITHUB_CLIENT_ID":"","GITHUB_CLIENT_SECRET":"","DEVICE_TRUST_DURATION_DAYS":7}
1
+ mbkautheVar={"APP_NAME":"mbkauthe","Main_SECRET_TOKEN": 123,"SESSION_SECRET_KEY":"123","IS_DEPLOYED":"true","LOGIN_DB":"postgres://","MBKAUTH_TWO_FA_ENABLE":"true","COOKIE_EXPIRE_TIME":2,"DOMAIN":"mbktech.org","loginRedirectURL":"/mbkauthe/test","GITHUB_LOGIN_ENABLED":"true","GITHUB_CLIENT_ID":"","GITHUB_CLIENT_SECRET":"","DEVICE_TRUST_DURATION_DAYS":7,"EncPass":"false"}
2
2
 
3
3
  # See docs/env.md for more details
package/README.md CHANGED
@@ -6,11 +6,21 @@
6
6
  [![Publish to npm](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml)
7
7
  [![CodeQL Advanced](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/codeql.yml)
8
8
 
9
+
10
+ <p align="center">
11
+ <img height="64px" src="./public/icon.svg" alt="MBK Chat Platform" />
12
+ </p>
13
+
14
+ <p align="center">
15
+ <img src="https://skillicons.dev/icons?i=nodejs,express,postgres" />
16
+ <img height="48px" src="https://handlebarsjs.com/handlebars-icon.svg" alt="Handlebars" />
17
+ </p>
18
+
9
19
  **MBKAuth** is a reusable, production-ready authentication system for Node.js applications built by MBKTech.org. It provides secure session management, two-factor authentication (2FA), role-based access control, and multi-application support out of the box.
10
20
 
11
21
  ## ✨ Features
12
22
 
13
- - 🔐 **Secure Authentication** - Password hashing with bcrypt
23
+ - 🔐 **Secure Authentication** - Configurable password encryption (PBKDF2) or raw password support
14
24
  - 🔑 **Session Management** - PostgreSQL-backed session storage
15
25
  - 📱 **Two-Factor Authentication (2FA)** - Optional TOTP-based 2FA with speakeasy
16
26
  - 🔄 **GitHub OAuth Integration** - Login with GitHub accounts (passport-github2)
@@ -199,9 +209,6 @@ MBKAuth automatically adds these routes to your app:
199
209
  ### CSRF Protection
200
210
  All POST routes are protected with CSRF tokens. CSRF tokens are automatically included in rendered forms.
201
211
 
202
- ### Password Hashing
203
- Passwords are hashed using bcrypt with a secure salt. Set `EncryptedPassword: "true"` in `mbkautheVar` to enable.
204
-
205
212
  ### Secure Cookies
206
213
  - `httpOnly` flag prevents XSS attacks
207
214
  - `sameSite: 'lax'` prevents CSRF attacks
package/docs/db.md CHANGED
@@ -138,13 +138,14 @@ The GitHub login feature is now fully integrated into your mbkauthe system and r
138
138
 
139
139
  - `id` (INTEGER, auto-increment, primary key): Unique identifier for each user.
140
140
  - `UserName` (TEXT): The username of the user.
141
- - `Password` (TEXT): The hashed password of the user.
141
+ - `Password` (TEXT): The raw password of the user (used when EncPass=false).
142
+ - `PasswordEnc` (TEXT): The encrypted/hashed password of the user (used when EncPass=true).
142
143
  - `Role` (ENUM): The role of the user. Possible values: `SuperAdmin`, `NormalUser`, `Guest`.
143
144
  - `Active` (BOOLEAN): Indicates whether the user account is active.
144
145
  - `HaveMailAccount` (BOOLEAN)(optional): Indicates if the user has a linked mail account.
145
146
  - `SessionId` (TEXT): The session ID associated with the user.
146
147
  - `GuestRole` (JSONB): Stores additional guest-specific role information in binary JSON format.
147
- - `AllowedApps`(JSONB):
148
+ - `AllowedApps`(JSONB): Array of applications the user is authorized to access.
148
149
 
149
150
  - **Schema:**
150
151
  ```sql
@@ -153,7 +154,8 @@ CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
153
154
  CREATE TABLE "Users" (
154
155
  id INTEGER PRIMARY KEY AUTOINCREMENT,
155
156
  "UserName" VARCHAR(50) NOT NULL UNIQUE,
156
- "Password" VARCHAR(61) NOT NULL, -- For bcrypt hash
157
+ "Password" VARCHAR(61), -- For raw passwords (when EncPass=false)
158
+ "PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
157
159
  "Role" role DEFAULT 'NormalUser' NOT NULL,
158
160
  "Active" BOOLEAN DEFAULT FALSE,
159
161
  "HaveMailAccount" BOOLEAN DEFAULT FALSE,
@@ -173,6 +175,12 @@ CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" (last_login);
173
175
  CREATE INDEX IF NOT EXISTS idx_users_id_sessionid_active_role ON "Users" ("id", LOWER("SessionId"), "Active", "Role");
174
176
  ```
175
177
 
178
+ **Password Storage Notes:**
179
+ - When `EncPass=false` (default): The system uses the `Password` column to store and validate raw passwords
180
+ - When `EncPass=true` (recommended for production): The system uses the `PasswordEnc` column to store hashed passwords using PBKDF2 with the username as salt
181
+ - Only one password column should be populated based on your EncPass configuration
182
+ - The PasswordEnc field stores 128-character hex strings when using PBKDF2 hashing
183
+
176
184
  ### Session Table
177
185
 
178
186
  - **Columns:**
@@ -255,6 +263,7 @@ CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices"("Expi
255
263
 
256
264
  To add new users to the `Users` table, use the following SQL queries:
257
265
 
266
+ **For Raw Password Storage (EncPass=false):**
258
267
  ```sql
259
268
  INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
260
269
  VALUES ('support', '12345678', 'SuperAdmin', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
@@ -263,8 +272,29 @@ To add new users to the `Users` table, use the following SQL queries:
263
272
  VALUES ('test', '12345678', 'NormalUser', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
264
273
  ```
265
274
 
275
+ **For Encrypted Password Storage (EncPass=true):**
276
+ ```sql
277
+ -- Note: You'll need to hash the password using the hashPassword function
278
+ -- Example with pre-hashed password (PBKDF2 with username as salt)
279
+ INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
280
+ VALUES ('support', 'your_hashed_password_here', 'SuperAdmin', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
281
+
282
+ INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
283
+ VALUES ('test', 'your_hashed_password_here', 'NormalUser', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
284
+ ```
285
+
286
+ **Configuration Notes:**
266
287
  - Replace `support` and `test` with the desired usernames.
267
- - Replace `12345678` with the actual passwords.
288
+ - For raw passwords: Replace `12345678` with the actual plain text passwords.
289
+ - For encrypted passwords: Use the hashPassword function to generate the hash before inserting.
268
290
  - Adjust the `Role` values as needed (`SuperAdmin`, `NormalUser`, or `Guest`).
269
291
  - Modify the `Active` and `HaveMailAccount` values as required.
270
- - Update the `GuestRole` JSON object if specific permissions are required(this functionality is under construction).
292
+ - Update the `GuestRole` JSON object if specific permissions are required (this functionality is under construction).
293
+
294
+ **Generating Encrypted Passwords:**
295
+ If you're using `EncPass=true`, you can generate encrypted passwords using the hashPassword function:
296
+ ```javascript
297
+ import { hashPassword } from './lib/config.js';
298
+ const encryptedPassword = hashPassword('12345678', 'support');
299
+ console.log(encryptedPassword); // Use this value for PasswordEnc column
300
+ ```
package/docs/env.md CHANGED
@@ -29,6 +29,7 @@ Main_SECRET_TOKEN=your-secure-token-number
29
29
  SESSION_SECRET_KEY=your-secure-random-key-here
30
30
  IS_DEPLOYED=false
31
31
  DOMAIN=localhost
32
+ EncPass=false
32
33
  ```
33
34
 
34
35
  #### Main_SECRET_TOKEN
@@ -80,6 +81,35 @@ DOMAIN=localhost
80
81
  IS_DEPLOYED=false
81
82
  ```
82
83
 
84
+ #### EncPass
85
+ **Description:** Controls whether passwords are stored and validated in encrypted format.
86
+
87
+ **Values:**
88
+ - `true` - Use encrypted password validation
89
+ - Passwords are hashed using PBKDF2 with the username as salt
90
+ - Compares against `PasswordEnc` column in Users table
91
+ - `false` - Use raw password validation (default)
92
+ - Passwords are stored and compared in plain text
93
+ - Compares against `Password` column in Users table
94
+
95
+ **Default:** `false`
96
+
97
+ **Security Note:** Setting `EncPass=true` is recommended for production environments as it provides better security by storing hashed passwords instead of plain text.
98
+
99
+ **Examples:**
100
+ ```env
101
+ # Production (recommended)
102
+ EncPass=true
103
+
104
+ # Development
105
+ EncPass=false
106
+ ```
107
+
108
+ **Database Implications:**
109
+ - When `EncPass=true`: The system uses the `PasswordEnc` column
110
+ - When `EncPass=false`: The system uses the `Password` column
111
+ - Ensure your database schema includes the appropriate column based on your configuration
112
+
83
113
  ---
84
114
 
85
115
  ## 🗄️ Database Configuration
@@ -278,6 +308,7 @@ Main_SECRET_TOKEN=dev-token-123
278
308
  SESSION_SECRET_KEY=dev-secret-key-change-in-production
279
309
  IS_DEPLOYED=false
280
310
  DOMAIN=localhost
311
+ EncPass=false
281
312
  LOGIN_DB=postgresql://admin:password@localhost:5432/mbkauth_dev
282
313
  MBKAUTH_TWO_FA_ENABLE=false
283
314
  COOKIE_EXPIRE_TIME=7
@@ -296,6 +327,7 @@ Main_SECRET_TOKEN=your-secure-production-token
296
327
  SESSION_SECRET_KEY=your-super-secure-production-key-here
297
328
  IS_DEPLOYED=true
298
329
  DOMAIN=yourdomain.com
330
+ EncPass=true
299
331
  LOGIN_DB=postgresql://dbuser:securepass@prod-db.example.com:5432/mbkauth_prod
300
332
  MBKAUTH_TWO_FA_ENABLE=true
301
333
  COOKIE_EXPIRE_TIME=2
package/lib/config.js CHANGED
@@ -56,6 +56,11 @@ function validateConfiguration() {
56
56
  errors.push("mbkautheVar.GITHUB_LOGIN_ENABLED must be either 'true' or 'false' or 'f'");
57
57
  }
58
58
 
59
+ // Validate EncPass value if provided
60
+ if (mbkautheVar.EncPass && !['true', 'false', 'f'].includes(mbkautheVar.EncPass.toLowerCase())) {
61
+ errors.push("mbkautheVar.EncPass must be either 'true' or 'false' or 'f'");
62
+ }
63
+
59
64
  // Validate GitHub login configuration
60
65
  if (mbkautheVar.GITHUB_LOGIN_ENABLED === "true") {
61
66
  if (!mbkautheVar.GITHUB_CLIENT_ID || mbkautheVar.GITHUB_CLIENT_ID.trim() === '') {
@@ -156,6 +161,9 @@ try {
156
161
  packageJson = require("../package.json");
157
162
  }
158
163
 
164
+ // Parent project version
165
+ const appVersion = require("../package.json") ?.version || "unknown";
166
+
159
167
  // Helper function to render error pages consistently
160
168
  const renderError = (res, { code, error, message, page, pagename, details }) => {
161
169
  res.status(code);
@@ -184,6 +192,12 @@ const clearSessionCookies = (res) => {
184
192
  res.clearCookie("device_token", cachedClearCookieOptions);
185
193
  };
186
194
 
195
+ const hashPassword = (password, username) => {
196
+ const salt = username;
197
+ // 128 characters returned
198
+ return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
199
+ };
200
+
187
201
  export {
188
202
  mbkautheVar,
189
203
  getCookieOptions,
@@ -193,8 +207,10 @@ export {
193
207
  renderError,
194
208
  clearSessionCookies,
195
209
  packageJson,
210
+ appVersion,
196
211
  DEVICE_TRUST_DURATION_DAYS,
197
212
  DEVICE_TRUST_DURATION_MS,
198
213
  generateDeviceToken,
199
- getDeviceTokenCookieOptions
200
- };
214
+ getDeviceTokenCookieOptions,
215
+ hashPassword
216
+ };
package/lib/main.js CHANGED
@@ -8,7 +8,6 @@ import { dblogin } from "./pool.js";
8
8
  import { authenticate, validateSession, validateSessionAndRole } from "./validateSessionAndRole.js";
9
9
  import fetch from 'node-fetch';
10
10
  import cookieParser from "cookie-parser";
11
- import bcrypt from 'bcrypt';
12
11
  import rateLimit from 'express-rate-limit';
13
12
  import speakeasy from "speakeasy";
14
13
  import passport from 'passport';
@@ -17,7 +16,11 @@ import GitHubStrategy from 'passport-github2';
17
16
  import { fileURLToPath } from "url";
18
17
  import fs from "fs";
19
18
  import path from "path";
20
- import { mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS } from "./config.js";
19
+ import {
20
+ mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, appVersion,
21
+ renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS,
22
+ hashPassword
23
+ } from "./config.js";
21
24
 
22
25
  const router = express.Router();
23
26
 
@@ -37,7 +40,7 @@ router.get('/icon.svg', (req, res) => {
37
40
  res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
38
41
  });
39
42
 
40
- router.get(['/favicon.ico','/icon.ico'], (req, res) => {
43
+ router.get(['/favicon.ico', '/icon.ico'], (req, res) => {
41
44
  res.setHeader('Cache-Control', 'public, max-age=31536000');
42
45
  res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
43
46
  });
@@ -133,14 +136,14 @@ router.use(async (req, res, next) => {
133
136
  // Only restore session if not already present and sessionId cookie exists
134
137
  if (!req.session.user && req.cookies.sessionId) {
135
138
  const sessionId = req.cookies.sessionId;
136
-
139
+
137
140
  // Early validation to avoid unnecessary processing
138
141
  if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
139
142
  // Clear invalid cookie to prevent repeated attempts
140
143
  res.clearCookie('sessionId', cachedClearCookieOptions);
141
144
  return next();
142
145
  }
143
-
146
+
144
147
  try {
145
148
 
146
149
  const normalizedSessionId = sessionId.toLowerCase();
@@ -170,16 +173,93 @@ router.use(async (req, res, next) => {
170
173
  router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
171
174
  if (req.session?.user) {
172
175
  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>
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>
179
200
  `);
180
201
  }
181
202
  });
182
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
+
183
263
  async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
184
264
  try {
185
265
  // Ensure both username formats are available for compatibility
@@ -209,10 +289,10 @@ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDev
209
289
  text: 'DELETE FROM "session" WHERE (sess->\'user\'->>\'id\')::int = $1',
210
290
  values: [user.id]
211
291
  }),
212
- // Update session ID in Users table
292
+ // Update session ID and last login time in Users table
213
293
  dblogin.query({
214
- name: 'login-update-session-id',
215
- text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`,
294
+ name: 'login-update-session-and-last-login',
295
+ text: `UPDATE "Users" SET "SessionId" = $1, "last_login" = NOW() WHERE "id" = $2`,
216
296
  values: [sessionId, user.id]
217
297
  })
218
298
  ]);
@@ -245,7 +325,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDev
245
325
  if (trustDevice) {
246
326
  try {
247
327
  const deviceToken = generateDeviceToken();
248
- const deviceName = req.headers['user-agent'] ?
328
+ const deviceName = req.headers['user-agent'] ?
249
329
  req.headers['user-agent'].substring(0, 255) : 'Unknown Device';
250
330
  const userAgent = req.headers['user-agent'] || 'Unknown';
251
331
  const ipAddress = req.ip || req.connection.remoteAddress || 'Unknown';
@@ -266,7 +346,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDev
266
346
  }
267
347
  }
268
348
 
269
- console.log(`[mbkauthe] User "${username}" logged in successfully`);
349
+ console.log(`[mbkauthe] User "${username}" logged in successfully (last_login updated)`);
270
350
 
271
351
  const responsePayload = {
272
352
  success: true,
@@ -328,7 +408,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
328
408
  router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
329
409
  console.log("[mbkauthe] Login request received");
330
410
 
331
- const { username, password } = req.body;
411
+ const { username, password, redirect } = req.body;
332
412
 
333
413
  // Input validation
334
414
  if (!username || !password) {
@@ -362,79 +442,9 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
362
442
  const trimmedUsername = username.trim();
363
443
 
364
444
  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.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
- return await completeLoginProcess(req, res, userForSession);
428
- }
429
- } catch (deviceErr) {
430
- console.error("[mbkauthe] Error checking trusted device:", deviceErr);
431
- // Continue with normal login flow if device check fails
432
- }
433
- }
434
-
435
445
  // Combined query: fetch user data and 2FA status in one query
436
446
  const userQuery = `
437
- SELECT u.id, u."UserName", u."Password", u."Active", u."Role", u."AllowedApps",
447
+ SELECT u.id, u."UserName", u."Password", u."PasswordEnc", u."Active", u."Role", u."AllowedApps",
438
448
  tfa."TwoFAStatus"
439
449
  FROM "Users" u
440
450
  LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
@@ -450,30 +460,33 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
450
460
  const user = userResult.rows[0];
451
461
 
452
462
  // Validate user has password field
453
- if (!user.Password) {
463
+ if (!user.Password && !user.PasswordEnc) {
454
464
  console.error("[mbkauthe] User account has no password set");
455
465
  return res.status(500).json({ success: false, message: "Internal Server Error" });
456
466
  }
457
467
 
458
- if (mbkautheVar.EncryptedPassword === "true") {
459
- try {
460
- const result = await bcrypt.compare(password, user.Password);
461
- if (!result) {
462
- console.log("[mbkauthe] Login failed: invalid credentials");
463
- return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
464
- }
465
- console.log("[mbkauthe] Password validated successfully");
466
- } catch (err) {
467
- console.error("[mbkauthe] Error comparing password:", err);
468
- return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` });
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;
469
477
  }
470
478
  } else {
471
- if (user.Password !== password) {
472
- console.log(`[mbkauthe] Login failed: invalid credentials`);
473
- return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
479
+ // Use raw password comparison
480
+ if (user.Password) {
481
+ passwordMatches = user.Password === password;
474
482
  }
475
483
  }
476
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
+
477
490
  if (!user.Active) {
478
491
  console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
479
492
  return res.status(403).json({ success: false, message: "Account is inactive" });
@@ -481,22 +494,41 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
481
494
 
482
495
  if (user.Role !== "SuperAdmin") {
483
496
  const allowedApps = user.AllowedApps;
484
- if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
497
+ if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
485
498
  console.warn(`[mbkauthe] User \"${user.UserName}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
486
499
  return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
487
500
  }
488
501
  }
489
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
+
490
520
  if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
491
521
  // 2FA is enabled, prompt for token on a separate page
522
+ const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
492
523
  req.session.preAuthUser = {
493
524
  id: user.id,
494
525
  username: user.UserName,
495
526
  role: user.Role,
496
527
  Role: user.Role,
528
+ redirectUrl: requestedRedirect
497
529
  };
498
530
  console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
499
- return res.json({ success: true, twoFactorRequired: true });
531
+ return res.json({ success: true, twoFactorRequired: true, redirectUrl: requestedRedirect });
500
532
  }
501
533
 
502
534
  // If 2FA is not enabled, proceed with login
@@ -507,7 +539,8 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
507
539
  Role: user.Role,
508
540
  allowedApps: user.AllowedApps,
509
541
  };
510
- await completeLoginProcess(req, res, userForSession);
542
+ const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
543
+ await completeLoginProcess(req, res, userForSession, requestedRedirect);
511
544
 
512
545
  } catch (err) {
513
546
  console.error("[mbkauthe] Error during login process:", err);
@@ -519,9 +552,19 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
519
552
  if (!req.session.preAuthUser) {
520
553
  return res.redirect("/mbkauthe/login");
521
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
+
522
565
  res.render("2fa.handlebars", {
523
566
  layout: false,
524
- customURL: mbkautheVar.loginRedirectURL || '/dashboard',
567
+ customURL: redirectToUse,
525
568
  csrfToken: req.csrfToken(),
526
569
  appName: mbkautheVar.APP_NAME.toLowerCase(),
527
570
  DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
@@ -574,7 +617,14 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
574
617
 
575
618
  // 2FA successful, complete login with optional device trust
576
619
  const userForSession = { id, username, role, allowedApps };
577
- const redirectUrl = mbkautheVar.loginRedirectURL || '/dashboard';
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;
578
628
  await completeLoginProcess(req, res, userForSession, redirectUrl, shouldTrustDevice);
579
629
 
580
630
  } catch (err) {
@@ -592,13 +642,13 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
592
642
  const operations = [
593
643
  dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
594
644
  ];
595
-
645
+
596
646
  if (req.sessionID) {
597
647
  operations.push(
598
648
  dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
599
649
  );
600
650
  }
601
-
651
+
602
652
  await Promise.all(operations);
603
653
 
604
654
  req.session.destroy((err) => {
@@ -670,6 +720,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
670
720
  layout: false,
671
721
  mbkautheVar: mbkautheVar,
672
722
  version: packageJson.version,
723
+ APP_VERSION: appVersion,
673
724
  latestVersion,
674
725
  authorized: authorized,
675
726
  });
@@ -724,7 +775,7 @@ passport.use('github-login', new GitHubStrategy({
724
775
  // Check if user is authorized for this app (same logic as regular login)
725
776
  if (user.Role !== "SuperAdmin") {
726
777
  const allowedApps = user.AllowedApps;
727
- if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
778
+ if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
728
779
  const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
729
780
  error.code = 'NOT_AUTHORIZED';
730
781
  return done(error);
@@ -886,16 +937,69 @@ router.get('/mbkauthe/api/github/login/callback',
886
937
 
887
938
  const user = userResult.rows[0];
888
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
+
889
989
  // Check 2FA if enabled
890
990
  if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
891
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;
892
995
  req.session.preAuthUser = {
893
996
  id: user.id,
894
997
  username: user.UserName,
895
998
  UserName: user.UserName,
896
999
  role: user.Role,
897
1000
  Role: user.Role,
898
- loginMethod: 'github'
1001
+ loginMethod: 'github',
1002
+ redirectUrl: oauthRedirect || null
899
1003
  };
900
1004
  console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
901
1005
  return res.redirect('/mbkauthe/2fa');
@@ -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.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -52,4 +52,4 @@
52
52
  "cross-env": "^7.0.3",
53
53
  "nodemon": "^3.1.11"
54
54
  }
55
- }
55
+ }
@@ -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';
@@ -201,6 +201,10 @@
201
201
  <div class="info-label">APP_NAME:</div>
202
202
  <div class="info-value">{{mbkautheVar.APP_NAME}}</div>
203
203
  </div>
204
+ <div class="info-row">
205
+ <div class="info-label">APP_Version:</div>
206
+ <div class="info-value">{{APP_VERSION}}</div>
207
+ </div>
204
208
  <div class="info-row">
205
209
  <div class="info-label">Domain:</div>
206
210
  <div class="info-value">{{mbkautheVar.DOMAIN}}</div>
@@ -98,8 +98,7 @@
98
98
 
99
99
  // Info dialogs
100
100
  function usernameinfo() {
101
- showMessage(`Your username is the part of your MBKTech.org email before the @ (e.g., abc.xyz@mbktech.org
102
- → abc.xyz). For guests or if you’ve forgotten your credentials, contact <a href="https://mbktech.org/Support">Support</a>.`, `What is my username?`);
101
+ showMessage(`Your username is the part of your MBKTech.org email before the @ (e.g., abc.xyz@mbktech.org → abc.xyz). For guests or if you’ve forgotten your credentials, contact <a href="https://mbktech.org/Support">Support</a>.`, `What is my username?`);
103
102
  }
104
103
 
105
104
  function tokeninfo() {
@@ -130,6 +129,8 @@
130
129
  loginButton.disabled = true;
131
130
  loginButtonText.textContent = 'Authenticating...';
132
131
 
132
+ // Pass redirect query param through to server so it can be used by 2FA flow
133
+ const pageRedirect = new URLSearchParams(window.location.search).get('redirect');
133
134
  fetch('/mbkauthe/api/login', {
134
135
  method: 'POST',
135
136
  headers: {
@@ -138,14 +139,17 @@
138
139
  body: JSON.stringify({
139
140
  username,
140
141
  password
142
+ , redirect: pageRedirect || null
141
143
  })
142
144
  })
143
145
  .then(response => response.json())
144
146
  .then(data => {
145
147
  if (data.success) {
146
148
  if (data.twoFactorRequired) {
147
- // Redirect to 2FA page
148
- window.location.href = '/mbkauthe/2fa';
149
+ // Redirect to 2FA page (= pass along redirect target)
150
+ const redirectTarget = data.redirectUrl || pageRedirect;
151
+ const redirectQuery = redirectTarget ? `?redirect=${encodeURIComponent(redirectTarget)}` : '';
152
+ window.location.href = `/mbkauthe/2fa${redirectQuery}`;
149
153
  } else {
150
154
  loginButtonText.textContent = 'Success! Redirecting...';
151
155
  sessionStorage.setItem('sessionId', data.sessionId);
@@ -240,39 +244,12 @@
240
244
 
241
245
  {{#if githubLoginEnabled }}
242
246
 
243
- // GitHub login: Attempt to POST redirect to backend, fallback to direct navigation
247
+ // GitHub login: Navigate directly to GitHub OAuth flow
244
248
  async function startGithubLogin() {
245
249
  const urlParams = new URLSearchParams(window.location.search);
246
250
  const redirect = urlParams.get('redirect') || '{{customURL}}';
247
251
 
248
- try {
249
- // Try POSTing to the backend so it can establish any session state
250
- const resp = await fetch('/mbkauthe/api/github/login', {
251
- method: 'POST',
252
- headers: { 'Content-Type': 'application/json' },
253
- credentials: 'include',
254
- body: JSON.stringify({ redirect })
255
- });
256
-
257
- // If backend responds with a JSON containing redirectUrl, navigate there
258
- if (resp.ok) {
259
- // If server redirected directly (resp.redirected), follow the final URL
260
- if (resp.redirected) {
261
- window.location.href = resp.url;
262
- return;
263
- }
264
- const data = await resp.json().catch(() => null);
265
- if (data && data.redirectUrl) {
266
- window.location.href = data.redirectUrl;
267
- return;
268
- }
269
- }
270
- } catch (error) {
271
- // swallow and fallback to direct navigation
272
- console.warn('[mbkauthe] GitHub login POST failed, falling back to direct redirect', error);
273
- }
274
-
275
- // Fallback: navigate to the backend GET endpoint with redirect query
252
+ // Navigate directly to the backend GET endpoint with redirect query
276
253
  window.location.href = `/mbkauthe/api/github/login?redirect=${encodeURIComponent(redirect)}`;
277
254
  }
278
255
 
@@ -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 {
@@ -22,24 +22,15 @@
22
22
 
23
23
  document.querySelector(".showmessageWindow .error-code").href = `https://mbktech.org/ErrorCode/#${errorCode}`;
24
24
  */
25
- document
26
- .querySelector(".showMessageblurWindow")
27
- .classList
28
- .add("active");
29
- document
30
- .body
31
- .classList
32
- .add("blur-active");
25
+ document.querySelector(".showMessageblurWindow").classList.add("active");
26
+ document.body.classList.add("blur-active");
33
27
  }
34
28
  function hideMessage() {
35
29
  const blurWindow = document.querySelector(".showMessageblurWindow");
36
30
  blurWindow.classList.add("fade-out");
37
31
  setTimeout(() => {
38
32
  blurWindow.classList.remove("active", "fade-out");
39
- document
40
- .body
41
- .classList
42
- .remove("blur-active");
33
+ document.body.classList.remove("blur-active");
43
34
  }, 500);
44
35
  }
45
36
  </script>