mbkauthe 2.4.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,
@@ -362,81 +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 && 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
445
  // Combined query: fetch user data and 2FA status in one query
438
446
  const userQuery = `
439
- 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",
440
448
  tfa."TwoFAStatus"
441
449
  FROM "Users" u
442
450
  LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
@@ -452,30 +460,33 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
452
460
  const user = userResult.rows[0];
453
461
 
454
462
  // Validate user has password field
455
- if (!user.Password) {
463
+ if (!user.Password && !user.PasswordEnc) {
456
464
  console.error("[mbkauthe] User account has no password set");
457
465
  return res.status(500).json({ success: false, message: "Internal Server Error" });
458
466
  }
459
467
 
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` });
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;
471
477
  }
472
478
  } 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" });
479
+ // Use raw password comparison
480
+ if (user.Password) {
481
+ passwordMatches = user.Password === password;
476
482
  }
477
483
  }
478
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
+
479
490
  if (!user.Active) {
480
491
  console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
481
492
  return res.status(403).json({ success: false, message: "Account is inactive" });
@@ -489,6 +500,23 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
489
500
  }
490
501
  }
491
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
+
492
520
  if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
493
521
  // 2FA is enabled, prompt for token on a separate page
494
522
  const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
@@ -614,13 +642,13 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
614
642
  const operations = [
615
643
  dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
616
644
  ];
617
-
645
+
618
646
  if (req.sessionID) {
619
647
  operations.push(
620
648
  dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
621
649
  );
622
650
  }
623
-
651
+
624
652
  await Promise.all(operations);
625
653
 
626
654
  req.session.destroy((err) => {
@@ -692,6 +720,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
692
720
  layout: false,
693
721
  mbkautheVar: mbkautheVar,
694
722
  version: packageJson.version,
723
+ APP_VERSION: appVersion,
695
724
  latestVersion,
696
725
  authorized: authorized,
697
726
  });
@@ -908,6 +937,55 @@ router.get('/mbkauthe/api/github/login/callback',
908
937
 
909
938
  const user = userResult.rows[0];
910
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
+
911
989
  // Check 2FA if enabled
912
990
  if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
913
991
  // 2FA is enabled, store pre-auth user and redirect to 2FA
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "2.4.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
+ }
@@ -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() {
@@ -245,39 +244,12 @@
245
244
 
246
245
  {{#if githubLoginEnabled }}
247
246
 
248
- // GitHub login: Attempt to POST redirect to backend, fallback to direct navigation
247
+ // GitHub login: Navigate directly to GitHub OAuth flow
249
248
  async function startGithubLogin() {
250
249
  const urlParams = new URLSearchParams(window.location.search);
251
250
  const redirect = urlParams.get('redirect') || '{{customURL}}';
252
251
 
253
- try {
254
- // Try POSTing to the backend so it can establish any session state
255
- const resp = await fetch('/mbkauthe/api/github/login', {
256
- method: 'POST',
257
- headers: { 'Content-Type': 'application/json' },
258
- credentials: 'include',
259
- body: JSON.stringify({ redirect })
260
- });
261
-
262
- // If backend responds with a JSON containing redirectUrl, navigate there
263
- if (resp.ok) {
264
- // If server redirected directly (resp.redirected), follow the final URL
265
- if (resp.redirected) {
266
- window.location.href = resp.url;
267
- return;
268
- }
269
- const data = await resp.json().catch(() => null);
270
- if (data && data.redirectUrl) {
271
- window.location.href = data.redirectUrl;
272
- return;
273
- }
274
- }
275
- } catch (error) {
276
- // swallow and fallback to direct navigation
277
- console.warn('[mbkauthe] GitHub login POST failed, falling back to direct redirect', error);
278
- }
279
-
280
- // Fallback: navigate to the backend GET endpoint with redirect query
252
+ // Navigate directly to the backend GET endpoint with redirect query
281
253
  window.location.href = `/mbkauthe/api/github/login?redirect=${encodeURIComponent(redirect)}`;
282
254
  }
283
255
 
@@ -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>