mbkauthe 4.0.0 → 4.1.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/README.md +2 -0
- package/docs/api.md +152 -13
- package/docs/db.md +28 -10
- package/docs/db.sql +26 -10
- package/index.d.ts +3 -0
- package/index.js +5 -2
- package/lib/config/cookies.js +157 -0
- package/lib/main.js +0 -5
- package/lib/middleware/auth.js +21 -16
- package/lib/middleware/index.js +2 -2
- package/lib/routes/auth.js +248 -9
- package/lib/routes/misc.js +92 -1
- package/lib/routes/oauth.js +8 -8
- package/lib/utils/response.js +8 -2
- package/package.json +1 -1
- package/public/icon.svg +1 -1
- package/test.spec.js +0 -8
- package/views/Error/dError.handlebars +175 -88
- package/views/accountSwitch.handlebars +423 -0
- package/views/head.handlebars +1 -1
- package/views/header.handlebars +97 -0
- package/views/loginmbkauthe.handlebars +135 -74
- package/views/sharedStyles.handlebars +360 -5
- package/views/showmessage.handlebars +248 -146
- package/public/icon.ico +0 -0
package/README.md
CHANGED
package/docs/api.md
CHANGED
|
@@ -356,6 +356,129 @@ fetch('/mbkauthe/api/logout', {
|
|
|
356
356
|
|
|
357
357
|
---
|
|
358
358
|
|
|
359
|
+
### Multi-Account Endpoints
|
|
360
|
+
|
|
361
|
+
#### `GET /mbkauthe/accounts`
|
|
362
|
+
|
|
363
|
+
Renders the account switching page, allowing users to switch between remembered accounts on the device.
|
|
364
|
+
|
|
365
|
+
**Rate Limit:** 8 requests per minute
|
|
366
|
+
|
|
367
|
+
**CSRF Protection:** Required
|
|
368
|
+
|
|
369
|
+
**Response:** HTML page with account list
|
|
370
|
+
|
|
371
|
+
**Template Variables:**
|
|
372
|
+
- `customURL` - Redirect URL after switch
|
|
373
|
+
- `userLoggedIn` - Whether a user is currently logged in
|
|
374
|
+
- `username` - Current username
|
|
375
|
+
- `fullname` - Current user's full name
|
|
376
|
+
- `role` - Current user's role
|
|
377
|
+
|
|
378
|
+
**Usage:**
|
|
379
|
+
```
|
|
380
|
+
GET /mbkauthe/accounts
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
#### `GET /mbkauthe/api/account-sessions`
|
|
386
|
+
|
|
387
|
+
Retrieves the list of remembered accounts for the current device.
|
|
388
|
+
|
|
389
|
+
**Rate Limit:** 8 requests per minute
|
|
390
|
+
|
|
391
|
+
**Response (200 OK):**
|
|
392
|
+
```json
|
|
393
|
+
{
|
|
394
|
+
"accounts": [
|
|
395
|
+
{
|
|
396
|
+
"sessionId": "64-char-session-id",
|
|
397
|
+
"username": "john.doe",
|
|
398
|
+
"fullName": "John Doe",
|
|
399
|
+
"isCurrent": true
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
"sessionId": "another-session-id",
|
|
403
|
+
"username": "jane.smith",
|
|
404
|
+
"fullName": "Jane Smith",
|
|
405
|
+
"isCurrent": false
|
|
406
|
+
}
|
|
407
|
+
],
|
|
408
|
+
"currentSessionId": "64-char-session-id"
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Behavior:**
|
|
413
|
+
- Validates each stored session against the database
|
|
414
|
+
- Automatically removes invalid/expired sessions from the cookie
|
|
415
|
+
- Returns only valid, active sessions
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
#### `POST /mbkauthe/api/switch-session`
|
|
420
|
+
|
|
421
|
+
Switches the active session to another remembered account.
|
|
422
|
+
|
|
423
|
+
**Rate Limit:** 8 requests per minute
|
|
424
|
+
|
|
425
|
+
**Request Body:**
|
|
426
|
+
```json
|
|
427
|
+
{
|
|
428
|
+
"sessionId": "target-session-id (required)",
|
|
429
|
+
"redirect": "/dashboard (optional)"
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Success Response (200 OK):**
|
|
434
|
+
```json
|
|
435
|
+
{
|
|
436
|
+
"success": true,
|
|
437
|
+
"username": "jane.smith",
|
|
438
|
+
"fullName": "Jane Smith",
|
|
439
|
+
"redirect": "/dashboard"
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Error Responses:**
|
|
444
|
+
|
|
445
|
+
| Status Code | Message |
|
|
446
|
+
|------------|---------|
|
|
447
|
+
| 400 | Invalid session ID format |
|
|
448
|
+
| 401 | Session expired |
|
|
449
|
+
| 403 | Account not available on this device |
|
|
450
|
+
| 500 | Internal Server Error |
|
|
451
|
+
|
|
452
|
+
**Behavior:**
|
|
453
|
+
- Verifies the target session exists in the device's remembered list
|
|
454
|
+
- Validates the session against the database
|
|
455
|
+
- Regenerates the session ID to prevent fixation
|
|
456
|
+
- Updates session cookies and current user context
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
#### `POST /mbkauthe/api/logout-all`
|
|
461
|
+
|
|
462
|
+
Logs out all remembered accounts on the current device.
|
|
463
|
+
|
|
464
|
+
**Rate Limit:** 8 requests per minute
|
|
465
|
+
|
|
466
|
+
**Response (200 OK):**
|
|
467
|
+
```json
|
|
468
|
+
{
|
|
469
|
+
"success": true,
|
|
470
|
+
"message": "All accounts logged out"
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Behavior:**
|
|
475
|
+
- Deletes all session records associated with the device's remembered accounts from the database
|
|
476
|
+
- Clears the account list cookie
|
|
477
|
+
- Destroys the current session
|
|
478
|
+
- Clears all session cookies
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
359
482
|
#### `POST /mbkauthe/api/terminateAllSessions`
|
|
360
483
|
|
|
361
484
|
Terminates all active sessions across all users (admin only).
|
|
@@ -523,34 +646,50 @@ Serves the application's SVG icon file from the root level.
|
|
|
523
646
|
|
|
524
647
|
---
|
|
525
648
|
|
|
526
|
-
#### `GET /
|
|
527
|
-
|
|
528
|
-
Serves the application's favicon.
|
|
649
|
+
#### `GET /mbkauthe/bg.webp`
|
|
529
650
|
|
|
530
|
-
|
|
651
|
+
Serves the background image for authentication pages.
|
|
531
652
|
|
|
532
|
-
**Response:**
|
|
653
|
+
**Response:** WEBP image file (Content-Type: image/webp)
|
|
533
654
|
|
|
534
655
|
**Cache:** Cached for 1 year (max-age=31536000)
|
|
535
656
|
|
|
536
657
|
**Usage:**
|
|
537
|
-
```
|
|
538
|
-
|
|
658
|
+
```css
|
|
659
|
+
background-image: url('/mbkauthe/bg.webp');
|
|
539
660
|
```
|
|
540
661
|
|
|
541
662
|
---
|
|
542
663
|
|
|
543
|
-
#### `GET /mbkauthe/
|
|
664
|
+
#### `GET /mbkauthe/user/profilepic`
|
|
544
665
|
|
|
545
|
-
Serves the
|
|
666
|
+
Serves the current user's profile picture or a default icon.
|
|
546
667
|
|
|
547
|
-
**
|
|
668
|
+
**Authentication:** Optional (returns default icon if not logged in)
|
|
548
669
|
|
|
549
|
-
**
|
|
670
|
+
**Response:**
|
|
671
|
+
- If logged in with valid profile picture URL: 302 redirect to the user's profile picture URL (from `Users.Image` column)
|
|
672
|
+
- If not logged in or no profile picture: SVG image file (Content-Type: image/svg+xml) streaming `/icon.svg`
|
|
673
|
+
|
|
674
|
+
**Cache:**
|
|
675
|
+
- Profile picture URL is cached in session for performance
|
|
676
|
+
- Cache is automatically cleared on login, logout, or account switch
|
|
677
|
+
|
|
678
|
+
**Behavior:**
|
|
679
|
+
1. First request: Queries `Users` table for `Image` column value
|
|
680
|
+
2. Subsequent requests: Returns cached value from session
|
|
681
|
+
3. On login/logout/switch: Cache is invalidated and fresh data is fetched
|
|
550
682
|
|
|
551
683
|
**Usage:**
|
|
552
|
-
```
|
|
553
|
-
|
|
684
|
+
```html
|
|
685
|
+
<img src="/mbkauthe/user/profilepic" alt="Profile Picture">
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
**Example Response Flow:**
|
|
689
|
+
```
|
|
690
|
+
User logged in → Query DB → Cache URL → Redirect to URL
|
|
691
|
+
User not logged in → Stream /icon.svg
|
|
692
|
+
Empty Image value → Stream /icon.svg
|
|
554
693
|
```
|
|
555
694
|
|
|
556
695
|
---
|
package/docs/db.md
CHANGED
|
@@ -196,30 +196,48 @@ The GitHub login feature is now fully integrated into your mbkauthe system and r
|
|
|
196
196
|
- (SessionId removed) The application now stores multiple concurrent sessions in the `Sessions` table.
|
|
197
197
|
- `GuestRole` (JSONB): Stores additional guest-specific role information in binary JSON format.
|
|
198
198
|
- `AllowedApps`(JSONB): Array of applications the user is authorized to access.
|
|
199
|
+
- `Image` (TEXT): URL to the user's profile picture. Used by the `/mbkauthe/user/profilepic` route to serve profile images. If empty, the route returns the default icon.svg. The URL is cached in the session for performance and automatically refreshed on login/logout/account switch.
|
|
200
|
+
- `FullName` (VARCHAR): The full name/display name of the user.
|
|
201
|
+
- `email` (TEXT): The user's email address.
|
|
199
202
|
|
|
200
203
|
- **Schema:**
|
|
201
204
|
```sql
|
|
202
205
|
CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
|
|
203
206
|
|
|
204
207
|
CREATE TABLE "Users" (
|
|
205
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
208
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT AS IDENTITY,
|
|
206
209
|
"UserName" VARCHAR(50) NOT NULL UNIQUE,
|
|
207
|
-
"Password" VARCHAR(
|
|
208
|
-
"PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
|
|
209
|
-
"Role" role DEFAULT 'NormalUser' NOT NULL,
|
|
210
|
+
"Password" VARCHAR(255) NOT NULL,
|
|
210
211
|
"Active" BOOLEAN DEFAULT FALSE,
|
|
212
|
+
"Role" role DEFAULT 'NormalUser' NOT NULL,
|
|
211
213
|
"HaveMailAccount" BOOLEAN DEFAULT FALSE,
|
|
212
214
|
"AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
|
|
213
215
|
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
214
216
|
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
215
|
-
"last_login" TIMESTAMP WITH TIME ZONE
|
|
217
|
+
"last_login" TIMESTAMP WITH TIME ZONE,
|
|
218
|
+
"PasswordEnc" VARCHAR(128),
|
|
219
|
+
|
|
220
|
+
"FullName" VARCHAR(255),
|
|
221
|
+
"email" TEXT DEFAULT 'support@mbktech.org',
|
|
222
|
+
"Image" TEXT DEFAULT 'https://portal.mbktech.org/icon.svg', -- Profile picture URL (used by /mbkauthe/user/profilepic route)
|
|
223
|
+
"Bio" TEXT DEFAULT 'I am ....',
|
|
224
|
+
"SocialAccounts" TEXT DEFAULT '{}',
|
|
225
|
+
"Positions" jsonb DEFAULT '{"Not_Permanent":"Member Is Not Permanent"}',
|
|
226
|
+
"resetToken" TEXT,
|
|
227
|
+
"resetTokenExpires" TimeStamp,
|
|
228
|
+
"resetAttempts" INTEGER DEFAULT '0',
|
|
229
|
+
"lastResetAttempt" TimeStamp WITH TIME ZONE
|
|
216
230
|
);
|
|
217
231
|
|
|
218
|
-
|
|
219
|
-
CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" ("UserName");
|
|
220
|
-
CREATE INDEX IF NOT EXISTS
|
|
221
|
-
CREATE INDEX IF NOT EXISTS
|
|
222
|
-
CREATE INDEX IF NOT EXISTS
|
|
232
|
+
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" USING BTREE ("UserName");
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" USING BTREE ("Role");
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" USING BTREE ("Active");
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON "Users" USING BTREE ("email");
|
|
237
|
+
CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" USING BTREE (last_login);
|
|
238
|
+
-- JSONB GIN indexes for common filters/queries on JSON fields
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_users_allowedapps_gin ON "Users" USING GIN ("AllowedApps");
|
|
240
|
+
CREATE INDEX IF NOT EXISTS idx_users_positions_gin ON "Users" USING GIN ("Positions");
|
|
223
241
|
|
|
224
242
|
-- Application Sessions table (stores multiple concurrent sessions per user)
|
|
225
243
|
-- Note: this is separate from the express-session store table named "session"
|
package/docs/db.sql
CHANGED
|
@@ -34,25 +34,41 @@ CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
|
|
|
34
34
|
|
|
35
35
|
CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
|
|
36
36
|
|
|
37
|
+
|
|
37
38
|
CREATE TABLE "Users" (
|
|
38
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT AS IDENTITY,
|
|
39
40
|
"UserName" VARCHAR(50) NOT NULL UNIQUE,
|
|
40
|
-
"Password" VARCHAR(
|
|
41
|
-
"PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
|
|
42
|
-
"Role" role DEFAULT 'NormalUser' NOT NULL,
|
|
41
|
+
"Password" VARCHAR(255) NOT NULL,
|
|
43
42
|
"Active" BOOLEAN DEFAULT FALSE,
|
|
43
|
+
"Role" role DEFAULT 'NormalUser' NOT NULL,
|
|
44
44
|
"HaveMailAccount" BOOLEAN DEFAULT FALSE,
|
|
45
45
|
"AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
|
|
46
46
|
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
47
47
|
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
48
|
-
"last_login" TIMESTAMP WITH TIME ZONE
|
|
48
|
+
"last_login" TIMESTAMP WITH TIME ZONE,
|
|
49
|
+
"PasswordEnc" VARCHAR(128),
|
|
50
|
+
|
|
51
|
+
"FullName" VARCHAR(255),
|
|
52
|
+
"email" TEXT DEFAULT 'support@mbktech.org',
|
|
53
|
+
"Image" TEXT DEFAULT 'https://portal.mbktech.org/icon.svg',
|
|
54
|
+
"Bio" TEXT DEFAULT 'I am ....',
|
|
55
|
+
"SocialAccounts" TEXT DEFAULT '{}',
|
|
56
|
+
"Positions" jsonb DEFAULT '{"Not_Permanent":"Member Is Not Permanent"}',
|
|
57
|
+
"resetToken" TEXT,
|
|
58
|
+
"resetTokenExpires" TimeStamp,
|
|
59
|
+
"resetAttempts" INTEGER DEFAULT '0',
|
|
60
|
+
"lastResetAttempt" TimeStamp WITH TIME ZONE
|
|
49
61
|
);
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" ("UserName");
|
|
53
|
-
CREATE INDEX IF NOT EXISTS
|
|
54
|
-
CREATE INDEX IF NOT EXISTS
|
|
55
|
-
CREATE INDEX IF NOT EXISTS
|
|
63
|
+
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" USING BTREE ("UserName");
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" USING BTREE ("Role");
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" USING BTREE ("Active");
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON "Users" USING BTREE ("email");
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" USING BTREE (last_login);
|
|
69
|
+
-- JSONB GIN indexes for common filters/queries on JSON fields
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_users_allowedapps_gin ON "Users" USING GIN ("AllowedApps");
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_users_positions_gin ON "Users" USING GIN ("Positions");
|
|
56
72
|
|
|
57
73
|
-- Application Sessions table (stores multiple concurrent sessions per user)
|
|
58
74
|
-- Note: this is separate from the express-session store table named "session"
|
package/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ declare global {
|
|
|
36
36
|
};
|
|
37
37
|
oauthRedirect?: string;
|
|
38
38
|
oauthCsrfToken?: string;
|
|
39
|
+
[key: string]: any;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -254,6 +255,8 @@ declare module 'mbkauthe' {
|
|
|
254
255
|
|
|
255
256
|
export function clearSessionCookies(res: Response): void;
|
|
256
257
|
|
|
258
|
+
export function getLatestVersion(): Promise<string>;
|
|
259
|
+
|
|
257
260
|
// Exports
|
|
258
261
|
export const dblogin: Pool;
|
|
259
262
|
export const mbkautheVar: MBKAuthConfig;
|
package/index.js
CHANGED
|
@@ -71,7 +71,7 @@ if (process.env.test === "dev") {
|
|
|
71
71
|
app.use(router);
|
|
72
72
|
app.use((req, res) => {
|
|
73
73
|
console.log(`[mbkauthe] Path not found: ${req.method} ${req.url}`);
|
|
74
|
-
return renderError(res, {
|
|
74
|
+
return renderError(res, req, {
|
|
75
75
|
layout: false,
|
|
76
76
|
code: 404,
|
|
77
77
|
error: "Not Found",
|
|
@@ -95,5 +95,8 @@ export {
|
|
|
95
95
|
} from "./lib/middleware/auth.js";
|
|
96
96
|
export { renderError } from "./lib/utils/response.js";
|
|
97
97
|
export { dblogin } from "./lib/database/pool.js";
|
|
98
|
-
export {
|
|
98
|
+
export {
|
|
99
|
+
ErrorCodes, ErrorMessages, getErrorByCode,
|
|
100
|
+
createErrorResponse, logError
|
|
101
|
+
} from "./lib/utils/errors.js";
|
|
99
102
|
export default router;
|
package/lib/config/cookies.js
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import { mbkautheVar } from "./index.js";
|
|
3
3
|
|
|
4
|
+
// Maximum number of remembered accounts per device
|
|
5
|
+
const MAX_REMEMBERED_ACCOUNTS = 5;
|
|
6
|
+
const ACCOUNT_LIST_COOKIE = 'mbkauthe_accounts';
|
|
7
|
+
|
|
8
|
+
// Cookie security: encryption and signing
|
|
9
|
+
const COOKIE_ENCRYPTION_KEY = mbkautheVar.SESSION_SECRET || 'fallback-secret-key-change-this';
|
|
10
|
+
const ENCRYPTION_ALGORITHM = 'aes-256-gcm';
|
|
11
|
+
|
|
12
|
+
// Derive encryption key from session secret
|
|
13
|
+
const getEncryptionKey = () => {
|
|
14
|
+
return crypto.createHash('sha256').update(COOKIE_ENCRYPTION_KEY).digest();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Encrypt and sign cookie payload
|
|
18
|
+
const encryptCookiePayload = (data) => {
|
|
19
|
+
try {
|
|
20
|
+
const iv = crypto.randomBytes(16);
|
|
21
|
+
const key = getEncryptionKey();
|
|
22
|
+
const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
|
|
23
|
+
|
|
24
|
+
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
|
|
25
|
+
encrypted += cipher.final('hex');
|
|
26
|
+
|
|
27
|
+
const authTag = cipher.getAuthTag();
|
|
28
|
+
|
|
29
|
+
// Combine iv + authTag + encrypted data
|
|
30
|
+
return {
|
|
31
|
+
iv: iv.toString('hex'),
|
|
32
|
+
authTag: authTag.toString('hex'),
|
|
33
|
+
data: encrypted
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[mbkauthe] Cookie encryption error:', error);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Decrypt and verify cookie payload
|
|
42
|
+
const decryptCookiePayload = (payload) => {
|
|
43
|
+
try {
|
|
44
|
+
if (!payload || !payload.iv || !payload.authTag || !payload.data) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const key = getEncryptionKey();
|
|
49
|
+
const decipher = crypto.createDecipheriv(
|
|
50
|
+
ENCRYPTION_ALGORITHM,
|
|
51
|
+
key,
|
|
52
|
+
Buffer.from(payload.iv, 'hex')
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
decipher.setAuthTag(Buffer.from(payload.authTag, 'hex'));
|
|
56
|
+
|
|
57
|
+
let decrypted = decipher.update(payload.data, 'hex', 'utf8');
|
|
58
|
+
decrypted += decipher.final('utf8');
|
|
59
|
+
|
|
60
|
+
return JSON.parse(decrypted);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[mbkauthe] Cookie decryption error:', error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Generate fingerprint from user-agent only (salted)
|
|
68
|
+
const generateFingerprint = (req) => {
|
|
69
|
+
const userAgent = req.headers['user-agent'] || '';
|
|
70
|
+
// Use SESSION_SECRET_KEY as salt if available, otherwise fallback to encryption key
|
|
71
|
+
const salt = mbkautheVar.SESSION_SECRET_KEY || COOKIE_ENCRYPTION_KEY;
|
|
72
|
+
|
|
73
|
+
// Hash user-agent with salt to prevent rainbow table attacks on UAs
|
|
74
|
+
return crypto
|
|
75
|
+
.createHash('sha256')
|
|
76
|
+
.update(`${userAgent}:${salt}`)
|
|
77
|
+
.digest('hex')
|
|
78
|
+
.substring(0, 32);
|
|
79
|
+
};
|
|
80
|
+
|
|
4
81
|
// Shared cookie options functions
|
|
5
82
|
const getCookieOptions = () => ({
|
|
6
83
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
@@ -57,3 +134,83 @@ export const clearSessionCookies = (res) => {
|
|
|
57
134
|
};
|
|
58
135
|
|
|
59
136
|
export { getCookieOptions, getClearCookieOptions };
|
|
137
|
+
|
|
138
|
+
// ---- Multi-account helpers ----
|
|
139
|
+
const parseAccountList = (raw, req) => {
|
|
140
|
+
if (!raw) return [];
|
|
141
|
+
try {
|
|
142
|
+
// First, decrypt the cookie payload
|
|
143
|
+
const parsed = JSON.parse(raw);
|
|
144
|
+
const decrypted = decryptCookiePayload(parsed);
|
|
145
|
+
|
|
146
|
+
if (!decrypted || !decrypted.accounts || !decrypted.fingerprint) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Verify fingerprint matches current request
|
|
151
|
+
const currentFingerprint = generateFingerprint(req);
|
|
152
|
+
if (decrypted.fingerprint !== currentFingerprint) {
|
|
153
|
+
console.warn('[mbkauthe] Cookie fingerprint mismatch - possible cookie theft attempt');
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const accounts = decrypted.accounts;
|
|
158
|
+
if (!Array.isArray(accounts)) return [];
|
|
159
|
+
|
|
160
|
+
// Accept only minimal safe fields
|
|
161
|
+
return accounts
|
|
162
|
+
.filter(item => item && typeof item === 'object')
|
|
163
|
+
.map(item => ({
|
|
164
|
+
sessionId: typeof item.sessionId === 'string' ? item.sessionId : null,
|
|
165
|
+
username: typeof item.username === 'string' ? item.username : null,
|
|
166
|
+
fullName: typeof item.fullName === 'string' ? item.fullName : null
|
|
167
|
+
}))
|
|
168
|
+
.filter(item => item.sessionId && item.username)
|
|
169
|
+
.slice(0, MAX_REMEMBERED_ACCOUNTS);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('[mbkauthe] Error parsing account list:', error);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const writeAccountList = (res, list, req) => {
|
|
177
|
+
const sanitized = Array.isArray(list) ? list.slice(0, MAX_REMEMBERED_ACCOUNTS) : [];
|
|
178
|
+
|
|
179
|
+
// Create payload with fingerprint
|
|
180
|
+
const payload = {
|
|
181
|
+
accounts: sanitized,
|
|
182
|
+
fingerprint: generateFingerprint(req)
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Encrypt the payload
|
|
186
|
+
const encrypted = encryptCookiePayload(payload);
|
|
187
|
+
if (!encrypted) {
|
|
188
|
+
console.error('[mbkauthe] Failed to encrypt account list cookie');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
res.cookie(ACCOUNT_LIST_COOKIE, JSON.stringify(encrypted), cachedCookieOptions);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const readAccountListFromCookie = (req) => {
|
|
196
|
+
const raw = req?.cookies ? req.cookies[ACCOUNT_LIST_COOKIE] : null;
|
|
197
|
+
return parseAccountList(raw, req);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const upsertAccountListCookie = (req, res, entry) => {
|
|
201
|
+
if (!entry || !entry.sessionId || !entry.username) return;
|
|
202
|
+
const current = readAccountListFromCookie(req);
|
|
203
|
+
const filtered = current.filter(item => item.sessionId !== entry.sessionId && item.username !== entry.username);
|
|
204
|
+
const next = [{ sessionId: entry.sessionId, username: entry.username, fullName: entry.fullName || entry.username }, ...filtered];
|
|
205
|
+
writeAccountList(res, next, req);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const removeAccountFromCookie = (req, res, sessionId) => {
|
|
209
|
+
const current = readAccountListFromCookie(req);
|
|
210
|
+
const next = current.filter(item => item.sessionId !== sessionId);
|
|
211
|
+
writeAccountList(res, next, req);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const clearAccountListCookie = (res) => {
|
|
215
|
+
res.clearCookie(ACCOUNT_LIST_COOKIE, cachedClearCookieOptions);
|
|
216
|
+
};
|
package/lib/main.js
CHANGED
|
@@ -69,10 +69,5 @@ router.get('/icon.svg', (req, res) => {
|
|
|
69
69
|
res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
router.get(['/favicon.ico', '/icon.ico'], (req, res) => {
|
|
73
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
74
|
-
res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
|
|
75
|
-
});
|
|
76
|
-
|
|
77
72
|
export { getLatestVersion } from "./routes/misc.js";
|
|
78
73
|
export default router;
|
package/lib/middleware/auth.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { dblogin } from "../database/pool.js";
|
|
2
2
|
import { mbkautheVar } from "../config/index.js";
|
|
3
3
|
import { renderError } from "../utils/response.js";
|
|
4
|
-
import { clearSessionCookies, cachedCookieOptions } from "../config/cookies.js";
|
|
4
|
+
import { clearSessionCookies, cachedCookieOptions, readAccountListFromCookie } from "../config/cookies.js";
|
|
5
5
|
|
|
6
6
|
async function validateSession(req, res, next) {
|
|
7
7
|
if (!req.session.user) {
|
|
8
8
|
console.log("[mbkauthe] User not authenticated");
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const remembered = readAccountListFromCookie(req) || [];
|
|
10
|
+
const hasRemembered = remembered.some(acct => acct && typeof acct.sessionId === 'string' && acct.sessionId.length > 0);
|
|
11
|
+
const pageTarget = hasRemembered ? '/mbkauthe/accounts' : `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`;
|
|
12
|
+
const message = hasRemembered
|
|
13
|
+
? "Another saved account is available. Open the switch page to continue."
|
|
14
|
+
: "You Are Not Logged In. Please Log In To Continue.";
|
|
15
|
+
return renderError(res, req, {
|
|
11
16
|
code: 401,
|
|
12
17
|
error: "Not Logged In",
|
|
13
|
-
message
|
|
14
|
-
pagename: "Login",
|
|
15
|
-
page:
|
|
18
|
+
message,
|
|
19
|
+
pagename: hasRemembered ? "Switch Account" : "Login",
|
|
20
|
+
page: pageTarget,
|
|
16
21
|
});
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -24,7 +29,7 @@ async function validateSession(req, res, next) {
|
|
|
24
29
|
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
25
30
|
req.session.destroy();
|
|
26
31
|
clearSessionCookies(res);
|
|
27
|
-
return renderError(res, {
|
|
32
|
+
return renderError(res, req, {
|
|
28
33
|
code: 401,
|
|
29
34
|
error: "Session Expired",
|
|
30
35
|
message: "Your Session Has Expired. Please Log In Again.",
|
|
@@ -47,7 +52,7 @@ async function validateSession(req, res, next) {
|
|
|
47
52
|
console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
|
|
48
53
|
req.session.destroy();
|
|
49
54
|
clearSessionCookies(res);
|
|
50
|
-
return renderError(res, {
|
|
55
|
+
return renderError(res, req, {
|
|
51
56
|
code: 401,
|
|
52
57
|
error: "Session Expired",
|
|
53
58
|
message: "Your Session Has Expired. Please Log In Again.",
|
|
@@ -64,7 +69,7 @@ async function validateSession(req, res, next) {
|
|
|
64
69
|
// destroy and clear cookies
|
|
65
70
|
req.session.destroy();
|
|
66
71
|
clearSessionCookies(res);
|
|
67
|
-
return renderError(res, {
|
|
72
|
+
return renderError(res, req, {
|
|
68
73
|
code: 401,
|
|
69
74
|
error: "Session Expired",
|
|
70
75
|
message: "Your Session Has Expired. Please Log In Again.",
|
|
@@ -78,7 +83,7 @@ async function validateSession(req, res, next) {
|
|
|
78
83
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
79
84
|
req.session.destroy();
|
|
80
85
|
clearSessionCookies(res);
|
|
81
|
-
return renderError(res, {
|
|
86
|
+
return renderError(res, req, {
|
|
82
87
|
code: 401,
|
|
83
88
|
error: "Account Inactive",
|
|
84
89
|
message: "Your Account Is Inactive. Please Contact Support.",
|
|
@@ -94,7 +99,7 @@ async function validateSession(req, res, next) {
|
|
|
94
99
|
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
95
100
|
req.session.destroy();
|
|
96
101
|
clearSessionCookies(res);
|
|
97
|
-
return renderError(res, {
|
|
102
|
+
return renderError(res, req, {
|
|
98
103
|
code: 401,
|
|
99
104
|
error: "Unauthorized",
|
|
100
105
|
message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"`,
|
|
@@ -192,7 +197,7 @@ async function validateApiSession(req, res, next) {
|
|
|
192
197
|
* Reload session user values from the database and refresh cookies.
|
|
193
198
|
* - Validates sessionId and active status
|
|
194
199
|
* - Updates `req.session.user` fields (username, role, allowedApps, fullname)
|
|
195
|
-
* - Uses cached `fullName` cookie when available, otherwise queries `
|
|
200
|
+
* - Uses cached `fullName` cookie when available, otherwise queries `Users`
|
|
196
201
|
* - Syncs `username`, `fullName` and `sessionId` cookies
|
|
197
202
|
* Returns: true if session refreshed and valid, false if session invalidated
|
|
198
203
|
*/
|
|
@@ -258,7 +263,7 @@ export async function reloadSessionUser(req, res) {
|
|
|
258
263
|
req.session.user.fullname = req.cookies.fullName;
|
|
259
264
|
} else {
|
|
260
265
|
try {
|
|
261
|
-
const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "
|
|
266
|
+
const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "USers" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
|
|
262
267
|
if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
|
|
263
268
|
} catch (profileErr) {
|
|
264
269
|
console.error('[mbkauthe] Error fetching fullname during reload:', profileErr);
|
|
@@ -290,7 +295,7 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
290
295
|
try {
|
|
291
296
|
if (!req.session || !req.session.user || !req.session.user.id) {
|
|
292
297
|
console.log("[mbkauthe] User not authenticated");
|
|
293
|
-
return renderError(res, {
|
|
298
|
+
return renderError(res, req, {
|
|
294
299
|
code: 401,
|
|
295
300
|
error: "Not Logged In",
|
|
296
301
|
message: "You Are Not Logged In. Please Log In To Continue.",
|
|
@@ -304,7 +309,7 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
304
309
|
|
|
305
310
|
// Check notAllowed role
|
|
306
311
|
if (notAllowed && userRole === notAllowed) {
|
|
307
|
-
return renderError(res, {
|
|
312
|
+
return renderError(res, req, {
|
|
308
313
|
code: 403,
|
|
309
314
|
error: "Access Denied",
|
|
310
315
|
message: "You are not allowed to access this resource",
|
|
@@ -323,7 +328,7 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
323
328
|
|
|
324
329
|
// Check if user role is in allowed roles
|
|
325
330
|
if (!rolesArray.includes(userRole)) {
|
|
326
|
-
return renderError(res, {
|
|
331
|
+
return renderError(res, req, {
|
|
327
332
|
code: 403,
|
|
328
333
|
error: "Access Denied",
|
|
329
334
|
message: "You do not have permission to access this resource",
|
package/lib/middleware/index.js
CHANGED
|
@@ -98,11 +98,11 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
98
98
|
if (req.cookies.fullName && typeof req.cookies.fullName === 'string') {
|
|
99
99
|
req.session.user.fullname = req.cookies.fullName;
|
|
100
100
|
} else {
|
|
101
|
-
// Fallback: attempt to fetch FullName from
|
|
101
|
+
// Fallback: attempt to fetch FullName from Users to populate session
|
|
102
102
|
try {
|
|
103
103
|
const profileRes = await dblogin.query({
|
|
104
104
|
name: 'restore-get-fullname',
|
|
105
|
-
text: 'SELECT "FullName" FROM "
|
|
105
|
+
text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
|
|
106
106
|
values: [row.UserName]
|
|
107
107
|
});
|
|
108
108
|
if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {
|