mbkauthe 4.1.2 → 4.1.4
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/docs/api.md +68 -2
- package/docs/db.md +19 -0
- package/docs/db.sql +17 -0
- package/docs/error-messages.md +6 -0
- package/index.js +1 -0
- package/lib/config/cookies.js +21 -1
- package/lib/config/index.js +15 -6
- package/lib/config/security.js +7 -0
- package/lib/middleware/auth.js +97 -2
- package/lib/middleware/index.js +13 -6
- package/lib/routes/auth.js +24 -11
- package/lib/routes/oauth.js +10 -6
- package/lib/utils/errors.js +12 -0
- package/package.json +1 -1
- package/views/showmessage.handlebars +3 -1
package/docs/api.md
CHANGED
|
@@ -25,7 +25,73 @@ This document provides comprehensive API documentation for MBKAuthe authenticati
|
|
|
25
25
|
MBKAuthe supports two authentication methods:
|
|
26
26
|
|
|
27
27
|
1. **Session-based Authentication** - Cookie-based sessions for web applications
|
|
28
|
-
2. **Token-based Authentication** - API
|
|
28
|
+
2. **Token-based Authentication** - Persistent API keys for server-to-server communication
|
|
29
|
+
|
|
30
|
+
### Token-based Authentication
|
|
31
|
+
|
|
32
|
+
For API clients and external services, use a Bearer token in the `Authorization` header.
|
|
33
|
+
|
|
34
|
+
**Header Format:**
|
|
35
|
+
```
|
|
36
|
+
Authorization: Bearer <your_api_token>
|
|
37
|
+
```
|
|
38
|
+
*Token format: `mbk_` followed by 64 hexadecimal characters.*
|
|
39
|
+
|
|
40
|
+
**Behavior:**
|
|
41
|
+
- **Stateless:** Validates against the `ApiTokens` table on every request.
|
|
42
|
+
- **Expiration:** Tokens can have an optional expiration date.
|
|
43
|
+
- **Permissions:** API tokens inherit the permissions of the user who created them.
|
|
44
|
+
- **Usage Tracking:** The system updates the `LastUsed` timestamp on every successful request.
|
|
45
|
+
|
|
46
|
+
**Errors:**
|
|
47
|
+
- `401 Unauthorized` (Code 1005: `INVALID_AUTH_TOKEN`): Token is malformed or not found.
|
|
48
|
+
- `401 Unauthorized` (Code 1006: `API_TOKEN_EXPIRED`): Token exists but has passed its expiration date.
|
|
49
|
+
|
|
50
|
+
**Example Usage:**
|
|
51
|
+
|
|
52
|
+
**1. Backend Implementation (Express):**
|
|
53
|
+
|
|
54
|
+
Even when using API tokens, the `validateSession` middleware hydrates `req.session.user` for consistency, allowing you to use the same route logic for both browser and API clients.
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
import { validateSession } from 'mbkauthe';
|
|
58
|
+
|
|
59
|
+
app.get('/api/protected-resource', validateSession, (req, res) => {
|
|
60
|
+
// Access user info populated from the token
|
|
61
|
+
const user = req.session.user; // { id, username, role, ... }
|
|
62
|
+
|
|
63
|
+
res.json({
|
|
64
|
+
message: `Hello ${user.username}`,
|
|
65
|
+
role: user.role
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**2. Client Request Examples:**
|
|
71
|
+
|
|
72
|
+
*cURL:*
|
|
73
|
+
```bash
|
|
74
|
+
curl -X GET https://api.yourdomain.com/api/protected-resource \
|
|
75
|
+
-H "Authorization: Bearer mbk_7f83a92b1dc..."
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
*JavaScript (Fetch):*
|
|
79
|
+
```javascript
|
|
80
|
+
const response = await fetch('https://api.yourdomain.com/api/protected-resource', {
|
|
81
|
+
headers: {
|
|
82
|
+
'Authorization': 'Bearer mbk_7f83a92b1dc...'
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Output:**
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"message": "Hello john.doe",
|
|
92
|
+
"role": "NormalUser"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
29
95
|
|
|
30
96
|
---
|
|
31
97
|
|
|
@@ -38,7 +104,7 @@ When a user logs in, MBKAuthe creates a session and sets the following cookies:
|
|
|
38
104
|
| Cookie Name | Description | HttpOnly | Secure | SameSite |
|
|
39
105
|
|------------|-------------|----------|--------|----------|
|
|
40
106
|
| `mbkauthe.sid` | Session identifier | ✓ | Auto* | lax |
|
|
41
|
-
| `sessionId` |
|
|
107
|
+
| `sessionId` | Encrypted session token (AES-256-GCM) | ✓ | Auto* | lax |
|
|
42
108
|
| `username` | Username | ✗ | Auto* | lax |
|
|
43
109
|
|
|
44
110
|
\* `secure` flag is automatically set to `true` in production when `IS_DEPLOYED=true`
|
package/docs/db.md
CHANGED
|
@@ -68,6 +68,25 @@ CREATE INDEX IF NOT EXISTS idx_user_google_google_id ON user_google (google_id);
|
|
|
68
68
|
CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
### 5. API Tokens Table
|
|
72
|
+
Used for storing persistent API keys.
|
|
73
|
+
|
|
74
|
+
```sql
|
|
75
|
+
CREATE TABLE "ApiTokens" (
|
|
76
|
+
"id" SERIAL PRIMARY KEY,
|
|
77
|
+
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
78
|
+
"Name" VARCHAR(255) NOT NULL, -- User-provided friendly name
|
|
79
|
+
"TokenHash" VARCHAR(128) NOT NULL UNIQUE, -- Hashed access token (SHA-256)
|
|
80
|
+
"Prefix" VARCHAR(32) NOT NULL, -- First few chars of token for ID
|
|
81
|
+
"LastUsed" TIMESTAMP WITH TIME ZONE,
|
|
82
|
+
"CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
83
|
+
"ExpiresAt" TIMESTAMP WITH TIME ZONE -- Optional expiration
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_apitokens_tokenhash ON "ApiTokens" ("TokenHash");
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_apitokens_username ON "ApiTokens" ("UserName");
|
|
88
|
+
```
|
|
89
|
+
|
|
71
90
|
## How It Works
|
|
72
91
|
|
|
73
92
|
### Login Flow (GitHub/Google)
|
package/docs/db.sql
CHANGED
|
@@ -130,3 +130,20 @@ CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices"("Expi
|
|
|
130
130
|
-- No Encrypted password for 'support' user
|
|
131
131
|
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "GuestRole")
|
|
132
132
|
VALUES ('support', '12345678', 'SuperAdmin', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
-- API Tokens for persistent programmatic access
|
|
137
|
+
CREATE TABLE "ApiTokens" (
|
|
138
|
+
"id" SERIAL PRIMARY KEY,
|
|
139
|
+
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
140
|
+
"Name" VARCHAR(255) NOT NULL, -- User-provided friendly name
|
|
141
|
+
"TokenHash" VARCHAR(128) NOT NULL UNIQUE, -- Hashed access token
|
|
142
|
+
"Prefix" VARCHAR(32) NOT NULL, -- First few chars of token for ID
|
|
143
|
+
"LastUsed" TIMESTAMP WITH TIME ZONE,
|
|
144
|
+
"CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
145
|
+
"ExpiresAt" TIMESTAMP WITH TIME ZONE -- Optional expiration
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_apitokens_tokenhash ON "ApiTokens" ("TokenHash");
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_apitokens_username ON "ApiTokens" ("UserName");
|
package/docs/error-messages.md
CHANGED
|
@@ -168,6 +168,12 @@ Password doesn't meet length requirements.
|
|
|
168
168
|
#### `1004 - INVALID_TOKEN_FORMAT`
|
|
169
169
|
Token format is incorrect.
|
|
170
170
|
|
|
171
|
+
#### `1005 - INVALID_AUTH_TOKEN`
|
|
172
|
+
The provided API token is invalid.
|
|
173
|
+
|
|
174
|
+
#### `1006 - API_TOKEN_EXPIRED`
|
|
175
|
+
The provided API token has expired.
|
|
176
|
+
|
|
171
177
|
### Rate Limiting (1100-1199)
|
|
172
178
|
|
|
173
179
|
#### `1101 - RATE_LIMIT_EXCEEDED`
|
package/index.js
CHANGED
package/lib/config/cookies.js
CHANGED
|
@@ -17,7 +17,7 @@ const getEncryptionKey = () => {
|
|
|
17
17
|
// Encrypt and sign cookie payload
|
|
18
18
|
const encryptCookiePayload = (data) => {
|
|
19
19
|
try {
|
|
20
|
-
const iv = crypto.randomBytes(
|
|
20
|
+
const iv = crypto.randomBytes(12);
|
|
21
21
|
const key = getEncryptionKey();
|
|
22
22
|
const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
|
|
23
23
|
|
|
@@ -78,6 +78,26 @@ const generateFingerprint = (req) => {
|
|
|
78
78
|
.substring(0, 32);
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// Encrypt sessionId for cookie storage
|
|
82
|
+
export const encryptSessionId = (sessionId) => {
|
|
83
|
+
if (!sessionId) return null;
|
|
84
|
+
const encrypted = encryptCookiePayload({ sessionId });
|
|
85
|
+
return encrypted ? JSON.stringify(encrypted) : null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Decrypt sessionId from cookie
|
|
89
|
+
export const decryptSessionId = (encryptedSessionId) => {
|
|
90
|
+
if (!encryptedSessionId) return null;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(encryptedSessionId);
|
|
93
|
+
const decrypted = decryptCookiePayload(parsed);
|
|
94
|
+
return decrypted?.sessionId || null;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('[mbkauthe] SessionId decryption error:', error);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
81
101
|
// Shared cookie options functions
|
|
82
102
|
const getCookieOptions = () => ({
|
|
83
103
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
package/lib/config/index.js
CHANGED
|
@@ -36,8 +36,6 @@ function validateConfiguration() {
|
|
|
36
36
|
if (mbkauthShared && typeof mbkauthShared !== 'object') {
|
|
37
37
|
console.warn('[mbkauthe] mbkauthShared is not a valid object, ignoring it');
|
|
38
38
|
mbkauthShared = null;
|
|
39
|
-
} else {
|
|
40
|
-
console.log('[mbkauthe] mbkauthShared detected and parsed successfully');
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
41
|
} catch (error) {
|
|
@@ -46,6 +44,8 @@ function validateConfiguration() {
|
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
// Merge fallback settings: for any key missing or empty in mbkautheVar, check mbkauthShared
|
|
47
|
+
const usedFromShared = [];
|
|
48
|
+
const usedDefaults = [];
|
|
49
49
|
const applyFallback = (source, sourceName) => {
|
|
50
50
|
if (!source) return;
|
|
51
51
|
Object.keys(source).forEach(key => {
|
|
@@ -53,7 +53,7 @@ function validateConfiguration() {
|
|
|
53
53
|
if ((mbkautheVar[key] === undefined || (typeof mbkautheVar[key] === 'string' && mbkautheVar[key].trim() === '')) &&
|
|
54
54
|
val !== undefined && !(typeof val === 'string' && val.trim() === '')) {
|
|
55
55
|
mbkautheVar[key] = val;
|
|
56
|
-
|
|
56
|
+
if (sourceName === 'mbkauthShared') usedFromShared.push(key);
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
};
|
|
@@ -86,10 +86,10 @@ function validateConfiguration() {
|
|
|
86
86
|
if (isEmpty) {
|
|
87
87
|
if (mbkauthShared && mbkauthShared[key] !== undefined && !(typeof mbkauthShared[key] === 'string' && mbkauthShared[key].trim() === '')) {
|
|
88
88
|
mbkautheVar[key] = mbkauthShared[key];
|
|
89
|
-
|
|
89
|
+
if (!usedFromShared.includes(key)) usedFromShared.push(key);
|
|
90
90
|
} else if (defaults[key] !== undefined) {
|
|
91
91
|
mbkautheVar[key] = defaults[key];
|
|
92
|
-
|
|
92
|
+
usedDefaults.push(key);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
});
|
|
@@ -209,7 +209,16 @@ function validateConfiguration() {
|
|
|
209
209
|
throw new Error(`[mbkauthe] Configuration Validation Failed:\n - ${errors.join('\n - ')}`);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
// Print consolidated configuration summary
|
|
213
|
+
const configParts = [];
|
|
214
|
+
if (mbkauthShared) {
|
|
215
|
+
configParts.push(`mbkauthShared: ${usedFromShared.length} keys`);
|
|
216
|
+
}
|
|
217
|
+
if (usedDefaults.length > 0) {
|
|
218
|
+
configParts.push(`defaults: ${usedDefaults.length} keys`);
|
|
219
|
+
}
|
|
220
|
+
const configSummary = configParts.length > 0 ? ` (${configParts.join(', ')})` : '';
|
|
221
|
+
console.log(`[mbkauthe] Configuration loaded${configSummary}`);
|
|
213
222
|
return mbkautheVar;
|
|
214
223
|
}
|
|
215
224
|
|
package/lib/config/security.js
CHANGED
|
@@ -5,4 +5,11 @@ export const hashPassword = (password, username) => {
|
|
|
5
5
|
const salt = username;
|
|
6
6
|
// 128 characters returned
|
|
7
7
|
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Hash an API token for storage
|
|
11
|
+
// Uses SHA-256 for fast, secure non-reversible hashing
|
|
12
|
+
export const hashApiToken = (token) => {
|
|
13
|
+
if (!token) return null;
|
|
14
|
+
return crypto.createHash('sha256').update(token).digest('hex');
|
|
8
15
|
};
|
package/lib/middleware/auth.js
CHANGED
|
@@ -2,8 +2,103 @@ import { dblogin } from "../database/pool.js";
|
|
|
2
2
|
import { mbkautheVar } from "../config/index.js";
|
|
3
3
|
import { renderError } from "../utils/response.js";
|
|
4
4
|
import { clearSessionCookies, cachedCookieOptions, readAccountListFromCookie } from "../config/cookies.js";
|
|
5
|
+
import { ErrorCodes, createErrorResponse } from "../utils/errors.js";
|
|
6
|
+
import { hashApiToken } from "../config/security.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates a Bearer token (API Token or Session UUID)
|
|
10
|
+
* Returns a user object if valid, or null/error object
|
|
11
|
+
*/
|
|
12
|
+
async function validateTokenAuthentication(req) {
|
|
13
|
+
const authHeader = req.headers.authorization;
|
|
14
|
+
if (!authHeader) return null;
|
|
15
|
+
|
|
16
|
+
const parts = authHeader.split(' ');
|
|
17
|
+
if (parts.length !== 2 || parts[0] !== 'Bearer') return null;
|
|
18
|
+
const token = parts[1];
|
|
19
|
+
|
|
20
|
+
// 1. Check for API Token (mbk_)
|
|
21
|
+
if (token.startsWith('mbk_')) {
|
|
22
|
+
const tokenHash = hashApiToken(token);
|
|
23
|
+
const tokenQuery = `
|
|
24
|
+
SELECT t.id, t."UserName", t."ExpiresAt", u.id as uid, u."Active", u."Role", u."AllowedApps", u."FullName"
|
|
25
|
+
FROM "ApiTokens" t
|
|
26
|
+
JOIN "Users" u ON t."UserName" = u."UserName"
|
|
27
|
+
WHERE t."TokenHash" = $1 LIMIT 1
|
|
28
|
+
`;
|
|
29
|
+
const tokenResult = await dblogin.query({ name: 'validate-api-token', text: tokenQuery, values: [tokenHash] });
|
|
30
|
+
|
|
31
|
+
if (tokenResult.rows.length === 0) return { error: 'INVALID_TOKEN' };
|
|
32
|
+
|
|
33
|
+
const row = tokenResult.rows[0];
|
|
34
|
+
if (row.ExpiresAt && new Date(row.ExpiresAt) <= new Date()) return { error: 'TOKEN_EXPIRED' };
|
|
35
|
+
|
|
36
|
+
// Update usage
|
|
37
|
+
dblogin.query({
|
|
38
|
+
text: 'UPDATE "ApiTokens" SET "LastUsed" = NOW() WHERE id = $1',
|
|
39
|
+
values: [row.id]
|
|
40
|
+
}).catch(e => console.error('[mbkauthe] Failed to update token usage:', e));
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: row.uid,
|
|
44
|
+
username: row.UserName,
|
|
45
|
+
fullname: row.FullName,
|
|
46
|
+
role: row.Role,
|
|
47
|
+
sessionId: 'api-token-session',
|
|
48
|
+
allowedApps: row.AllowedApps,
|
|
49
|
+
active: row.Active
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
5
55
|
|
|
6
56
|
async function validateSession(req, res, next) {
|
|
57
|
+
// --- Check for API Token Header first ---
|
|
58
|
+
if (req.headers.authorization) {
|
|
59
|
+
try {
|
|
60
|
+
const tokenUser = await validateTokenAuthentication(req);
|
|
61
|
+
|
|
62
|
+
if (tokenUser && !tokenUser.error) {
|
|
63
|
+
if (!tokenUser.active) {
|
|
64
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (tokenUser.role !== "SuperAdmin") {
|
|
68
|
+
const allowedApps = tokenUser.allowedApps;
|
|
69
|
+
const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
|
|
70
|
+
if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
71
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Populate session for downstream
|
|
76
|
+
req.session.user = {
|
|
77
|
+
id: tokenUser.id,
|
|
78
|
+
username: tokenUser.username,
|
|
79
|
+
fullname: tokenUser.fullname,
|
|
80
|
+
role: tokenUser.role,
|
|
81
|
+
sessionId: tokenUser.sessionId,
|
|
82
|
+
allowedApps: tokenUser.allowedApps,
|
|
83
|
+
};
|
|
84
|
+
req.userRole = tokenUser.role;
|
|
85
|
+
return next();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Token provided but invalid (or null if format incorrect)
|
|
89
|
+
let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
|
|
90
|
+
if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
|
|
91
|
+
errorCode = ErrorCodes.API_TOKEN_EXPIRED;
|
|
92
|
+
}
|
|
93
|
+
return res.status(401).json(createErrorResponse(401, errorCode));
|
|
94
|
+
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error("[mbkauthe] Token validation error:", err);
|
|
97
|
+
return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Fallback to Cookie Session ---
|
|
7
102
|
if (!req.session.user) {
|
|
8
103
|
console.log("[mbkauthe] User not authenticated");
|
|
9
104
|
console.log("[mbkauthe]: ", req.session.user);
|
|
@@ -258,7 +353,7 @@ export async function reloadSessionUser(req, res) {
|
|
|
258
353
|
req.session.user.fullname = req.cookies.fullName;
|
|
259
354
|
} else {
|
|
260
355
|
try {
|
|
261
|
-
const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "
|
|
356
|
+
const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
|
|
262
357
|
if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
|
|
263
358
|
} catch (profileErr) {
|
|
264
359
|
console.error('[mbkauthe] Error fetching fullname during reload:', profileErr);
|
|
@@ -270,7 +365,7 @@ export async function reloadSessionUser(req, res) {
|
|
|
270
365
|
|
|
271
366
|
// Sync cookies for client UI (non-httpOnly)
|
|
272
367
|
try {
|
|
273
|
-
|
|
368
|
+
res.cookie('username', req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
274
369
|
res.cookie('fullName', req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
275
370
|
res.cookie('sessionId', req.session.user.sessionId, cachedCookieOptions);
|
|
276
371
|
} catch (cookieErr) {
|
package/lib/middleware/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import pgSession from "connect-pg-simple";
|
|
|
3
3
|
const PgSession = pgSession(session);
|
|
4
4
|
import { dblogin } from "../database/pool.js";
|
|
5
5
|
import { mbkautheVar } from "../config/index.js";
|
|
6
|
-
import { cachedCookieOptions } from "../config/cookies.js";
|
|
6
|
+
import { cachedCookieOptions, decryptSessionId, encryptSessionId } from "../config/cookies.js";
|
|
7
7
|
|
|
8
8
|
// Session configuration
|
|
9
9
|
export const sessionConfig = {
|
|
@@ -55,10 +55,11 @@ export function corsMiddleware(req, res, next) {
|
|
|
55
55
|
export async function sessionRestorationMiddleware(req, res, next) {
|
|
56
56
|
// Only restore session if not already present and sessionId cookie exists
|
|
57
57
|
if (!req.session.user && req.cookies.sessionId) {
|
|
58
|
-
|
|
58
|
+
// Decrypt the sessionId from cookie
|
|
59
|
+
const sessionId = decryptSessionId(req.cookies.sessionId);
|
|
59
60
|
|
|
60
61
|
// Early validation to avoid unnecessary processing (expect DB UUID id)
|
|
61
|
-
if (typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
62
|
+
if (!sessionId || typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
62
63
|
// Clear invalid cookie to prevent repeated attempts
|
|
63
64
|
res.clearCookie('sessionId', {
|
|
64
65
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
@@ -124,12 +125,18 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
124
125
|
// Session cookie sync middleware
|
|
125
126
|
export function sessionCookieSyncMiddleware(req, res, next) {
|
|
126
127
|
if (req.session && req.session.user) {
|
|
128
|
+
// Decrypt existing cookie to compare with session
|
|
129
|
+
const currentDecryptedId = decryptSessionId(req.cookies.sessionId);
|
|
130
|
+
|
|
127
131
|
// Only set cookies if they're missing or different
|
|
128
|
-
if (
|
|
132
|
+
if (currentDecryptedId !== req.session.user.sessionId) {
|
|
129
133
|
res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
130
|
-
// Also expose FullName (fallback to username) for display in client-side UI
|
|
131
134
|
res.cookie("fullName", req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
132
|
-
|
|
135
|
+
|
|
136
|
+
const encryptedSessionId = encryptSessionId(req.session.user.sessionId);
|
|
137
|
+
if (encryptedSessionId) {
|
|
138
|
+
res.cookie("sessionId", encryptedSessionId, cachedCookieOptions);
|
|
139
|
+
}
|
|
133
140
|
}
|
|
134
141
|
}
|
|
135
142
|
next();
|
package/lib/routes/auth.js
CHANGED
|
@@ -8,10 +8,11 @@ import { mbkautheVar } from "../config/index.js";
|
|
|
8
8
|
import {
|
|
9
9
|
cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies,
|
|
10
10
|
generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS, hashDeviceToken,
|
|
11
|
-
upsertAccountListCookie, readAccountListFromCookie, removeAccountFromCookie, clearAccountListCookie
|
|
11
|
+
upsertAccountListCookie, readAccountListFromCookie, removeAccountFromCookie, clearAccountListCookie,
|
|
12
|
+
encryptSessionId
|
|
12
13
|
} from "../config/cookies.js";
|
|
13
14
|
import { packageJson } from "../config/index.js";
|
|
14
|
-
import { hashPassword } from "../config/security.js";
|
|
15
|
+
import { hashPassword, hashApiToken } from "../config/security.js";
|
|
15
16
|
import { ErrorCodes, createErrorResponse, logError } from "../utils/errors.js";
|
|
16
17
|
|
|
17
18
|
const router = express.Router();
|
|
@@ -218,13 +219,20 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
218
219
|
|
|
219
220
|
const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
|
|
220
221
|
|
|
221
|
-
// Insert new session record for the user (
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
// Insert new session record for the user (generate id explicitly to avoid relying on DB default)
|
|
223
|
+
const newSessionId = crypto.randomUUID();
|
|
224
|
+
let dbSessionId;
|
|
225
|
+
try {
|
|
226
|
+
const insertRes = await dblogin.query({
|
|
227
|
+
name: 'insert-app-session',
|
|
228
|
+
text: `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`,
|
|
229
|
+
values: [username, expiresAt, JSON.stringify({ ip: req.ip, ua: req.headers['user-agent'] || null })]
|
|
230
|
+
});
|
|
231
|
+
dbSessionId = insertRes.rows[0].id;
|
|
232
|
+
} catch (insertErr) {
|
|
233
|
+
console.error('[mbkauthe] Error inserting app session:', insertErr);
|
|
234
|
+
throw insertErr;
|
|
235
|
+
}
|
|
228
236
|
|
|
229
237
|
// Update last_login timestamp for the user
|
|
230
238
|
await dblogin.query({
|
|
@@ -269,7 +277,11 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
// Expose DB session id and display name to client for UI (fullName falls back to username)
|
|
272
|
-
|
|
280
|
+
const encryptedSessionId = encryptSessionId(dbSessionId);
|
|
281
|
+
if (encryptedSessionId) {
|
|
282
|
+
res.cookie("sessionId", encryptedSessionId, cachedCookieOptions);
|
|
283
|
+
}
|
|
284
|
+
res.cookie("username", username, { ...cachedCookieOptions, httpOnly: false });
|
|
273
285
|
res.cookie("fullName", req.session.user.fullname || username, { ...cachedCookieOptions, httpOnly: false });
|
|
274
286
|
|
|
275
287
|
// Remember this account on the device for quick switching (server-trusted list)
|
|
@@ -693,7 +705,8 @@ router.post("/api/switch-session", LoginLimit, async (req, res) => {
|
|
|
693
705
|
}
|
|
694
706
|
|
|
695
707
|
const storedAccounts = readAccountListFromCookie(req);
|
|
696
|
-
|
|
708
|
+
const acct = storedAccounts.find(a => a.sessionId === sessionId);
|
|
709
|
+
if (!acct) {
|
|
697
710
|
return res.status(403).json(createErrorResponse(403, ErrorCodes.SESSION_NOT_FOUND, { message: 'Account not available on this device' }));
|
|
698
711
|
}
|
|
699
712
|
|
package/lib/routes/oauth.js
CHANGED
|
@@ -108,6 +108,9 @@ const createOAuthStrategy = async (provider, profile, done) => {
|
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
// Configure OAuth strategies and track enabled providers
|
|
112
|
+
const enabledProviders = [];
|
|
113
|
+
|
|
111
114
|
// Configure GitHub Strategy for login (only if enabled and configured)
|
|
112
115
|
if ((mbkautheVar.GITHUB_LOGIN_ENABLED || "").toLowerCase() === "true") {
|
|
113
116
|
if (mbkautheVar.GITHUB_CLIENT_ID && mbkautheVar.GITHUB_CLIENT_SECRET) {
|
|
@@ -119,12 +122,10 @@ if ((mbkautheVar.GITHUB_LOGIN_ENABLED || "").toLowerCase() === "true") {
|
|
|
119
122
|
}, (accessToken, refreshToken, profile, done) =>
|
|
120
123
|
createOAuthStrategy('GitHub', profile, done)
|
|
121
124
|
));
|
|
122
|
-
|
|
125
|
+
enabledProviders.push('GitHub');
|
|
123
126
|
} else {
|
|
124
127
|
console.warn('[mbkauthe] GITHUB_LOGIN_ENABLED is true but GITHUB_CLIENT_ID/SECRET missing; skipping GitHub strategy registration');
|
|
125
128
|
}
|
|
126
|
-
} else {
|
|
127
|
-
console.log('[mbkauthe] GitHub OAuth not enabled; skipping GitHub strategy registration');
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
// Configure Google Strategy for login (only if enabled and configured)
|
|
@@ -138,12 +139,15 @@ if ((mbkautheVar.GOOGLE_LOGIN_ENABLED || "").toLowerCase() === "true") {
|
|
|
138
139
|
}, (accessToken, refreshToken, profile, done) =>
|
|
139
140
|
createOAuthStrategy('Google', profile, done)
|
|
140
141
|
));
|
|
141
|
-
|
|
142
|
+
enabledProviders.push('Google');
|
|
142
143
|
} else {
|
|
143
144
|
console.warn('[mbkauthe] GOOGLE_LOGIN_ENABLED is true but GOOGLE_CLIENT_ID/SECRET missing; skipping Google strategy registration');
|
|
144
145
|
}
|
|
145
|
-
}
|
|
146
|
-
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Print consolidated OAuth summary
|
|
149
|
+
if (enabledProviders.length > 0) {
|
|
150
|
+
console.log(`[mbkauthe] OAuth providers: ${enabledProviders.join(', ')}`);
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
// Serialize/Deserialize user for OAuth login
|
package/lib/utils/errors.js
CHANGED
|
@@ -32,6 +32,8 @@ export const ErrorCodes = {
|
|
|
32
32
|
INVALID_USERNAME_FORMAT: 1002,
|
|
33
33
|
INVALID_PASSWORD_LENGTH: 1003,
|
|
34
34
|
INVALID_TOKEN_FORMAT: 1004,
|
|
35
|
+
INVALID_AUTH_TOKEN: 1005,
|
|
36
|
+
API_TOKEN_EXPIRED: 1006,
|
|
35
37
|
|
|
36
38
|
// Rate limiting (1100-1199)
|
|
37
39
|
RATE_LIMIT_EXCEEDED: 1101,
|
|
@@ -148,6 +150,16 @@ export const ErrorMessages = {
|
|
|
148
150
|
userMessage: "Please enter a valid 6-digit code.",
|
|
149
151
|
hint: "The code should be 6 numbers from your authenticator app"
|
|
150
152
|
},
|
|
153
|
+
[ErrorCodes.INVALID_AUTH_TOKEN]: {
|
|
154
|
+
message: "Invalid API token",
|
|
155
|
+
userMessage: "The provided API token is invalid.",
|
|
156
|
+
hint: "Please check your token and try again"
|
|
157
|
+
},
|
|
158
|
+
[ErrorCodes.API_TOKEN_EXPIRED]: {
|
|
159
|
+
message: "API token expired",
|
|
160
|
+
userMessage: "The provided API token has expired.",
|
|
161
|
+
hint: "Please generate a new API token"
|
|
162
|
+
},
|
|
151
163
|
|
|
152
164
|
// Rate Limiting
|
|
153
165
|
[ErrorCodes.RATE_LIMIT_EXCEEDED]: {
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="showMessageOverlay" id="messageOverlay" aria-hidden="true">
|
|
1
|
+
<div class="showMessageOverlay" id="messageOverlay" aria-hidden="true" hidden>
|
|
2
2
|
<div class="messageModal" role="dialog" aria-modal="true" aria-labelledby="messageTitle"
|
|
3
3
|
aria-describedby="messageContent">
|
|
4
4
|
<div class="messageHeader">
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// Show Modal
|
|
118
|
+
overlay.removeAttribute("hidden");
|
|
118
119
|
overlay.classList.add("active");
|
|
119
120
|
document.body.classList.add("blur-active");
|
|
120
121
|
overlay.setAttribute("aria-hidden", "false");
|
|
@@ -131,6 +132,7 @@
|
|
|
131
132
|
overlay.classList.remove("active", "fade-out");
|
|
132
133
|
document.body.classList.remove("blur-active");
|
|
133
134
|
overlay.setAttribute("aria-hidden", "true");
|
|
135
|
+
overlay.setAttribute("hidden", "");
|
|
134
136
|
}, 300);
|
|
135
137
|
}
|
|
136
138
|
|