mbkauthe 1.3.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -187
- package/docs/api.md +841 -0
- package/docs/db.md +54 -31
- package/index.js +13 -2
- package/lib/main.js +106 -38
- package/lib/pool.js +14 -7
- package/lib/validateSessionAndRole.js +75 -83
- package/package.json +5 -5
- package/public/main.js +134 -0
- package/views/2fa.handlebars +35 -445
- package/views/Error/dError.handlebars +71 -551
- package/views/info.handlebars +180 -160
- package/views/loginmbkauthe.handlebars +37 -557
- package/views/sharedStyles.handlebars +498 -0
- package/views/showmessage.handlebars +97 -62
|
@@ -11,13 +11,27 @@ const getCookieOptions = () => ({
|
|
|
11
11
|
httpOnly: true
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
const getClearCookieOptions = () => ({
|
|
15
|
+
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
16
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false,
|
|
17
|
+
sameSite: 'lax',
|
|
18
|
+
path: '/',
|
|
19
|
+
httpOnly: true
|
|
20
|
+
});
|
|
21
|
+
|
|
14
22
|
async function validateSession(req, res, next) {
|
|
15
23
|
if (!req.session.user && req.cookies.sessionId) {
|
|
16
24
|
try {
|
|
17
25
|
const sessionId = req.cookies.sessionId;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
|
|
27
|
+
// Validate sessionId format (should be 64 hex characters)
|
|
28
|
+
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
29
|
+
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
30
|
+
return next();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE "SessionId" = $1 AND "Active" = true`;
|
|
34
|
+
const result = await dblogin.query({ name: 'get-user-by-sessionid', text: query, values: [sessionId] });
|
|
21
35
|
|
|
22
36
|
if (result.rows.length > 0) {
|
|
23
37
|
const user = result.rows[0];
|
|
@@ -25,7 +39,10 @@ async function validateSession(req, res, next) {
|
|
|
25
39
|
id: user.id,
|
|
26
40
|
username: user.UserName,
|
|
27
41
|
UserName: user.UserName,
|
|
42
|
+
role: user.Role,
|
|
43
|
+
Role: user.Role,
|
|
28
44
|
sessionId,
|
|
45
|
+
allowedApps: user.AllowedApps,
|
|
29
46
|
};
|
|
30
47
|
}
|
|
31
48
|
} catch (err) {
|
|
@@ -48,15 +65,16 @@ async function validateSession(req, res, next) {
|
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
try {
|
|
51
|
-
const { id, sessionId } = req.session.user;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
68
|
+
const { id, sessionId, role, allowedApps } = req.session.user;
|
|
69
|
+
|
|
70
|
+
// Fetch only SessionId and Active status for validation (reduced query)
|
|
71
|
+
const query = `SELECT "SessionId", "Active" FROM "Users" WHERE "id" = $1`;
|
|
72
|
+
const result = await dblogin.query({ name: 'get-user-by-id', text: query, values: [id] });
|
|
55
73
|
|
|
56
|
-
if (result.rows.length === 0 ||
|
|
74
|
+
if (result.rows.length === 0 || result.rows[0].SessionId !== sessionId) {
|
|
57
75
|
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
58
76
|
req.session.destroy();
|
|
59
|
-
const cookieOptions =
|
|
77
|
+
const cookieOptions = getClearCookieOptions();
|
|
60
78
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
61
79
|
res.clearCookie("sessionId", cookieOptions);
|
|
62
80
|
res.clearCookie("username", cookieOptions);
|
|
@@ -70,10 +88,10 @@ async function validateSession(req, res, next) {
|
|
|
70
88
|
});
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
if (!
|
|
91
|
+
if (!result.rows[0].Active) {
|
|
74
92
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
75
93
|
req.session.destroy();
|
|
76
|
-
const cookieOptions =
|
|
94
|
+
const cookieOptions = getClearCookieOptions();
|
|
77
95
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
78
96
|
res.clearCookie("sessionId", cookieOptions);
|
|
79
97
|
res.clearCookie("username", cookieOptions);
|
|
@@ -87,12 +105,11 @@ async function validateSession(req, res, next) {
|
|
|
87
105
|
});
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
if (
|
|
91
|
-
const allowedApps = userResult.AllowedApps;
|
|
108
|
+
if (role !== "SuperAdmin") {
|
|
92
109
|
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
93
110
|
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
94
111
|
req.session.destroy();
|
|
95
|
-
const cookieOptions =
|
|
112
|
+
const cookieOptions = getClearCookieOptions();
|
|
96
113
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
97
114
|
res.clearCookie("sessionId", cookieOptions);
|
|
98
115
|
res.clearCookie("username", cookieOptions);
|
|
@@ -133,7 +150,7 @@ const checkRolePermission = (requiredRole, notAllowed) => {
|
|
|
133
150
|
const userId = req.session.user.id;
|
|
134
151
|
|
|
135
152
|
const query = `SELECT "Role" FROM "Users" WHERE "id" = $1`;
|
|
136
|
-
const result = await dblogin.query(query, [userId]);
|
|
153
|
+
const result = await dblogin.query({ name: 'get-role-by-id', text: query, values: [userId] });
|
|
137
154
|
|
|
138
155
|
if (result.rows.length === 0) {
|
|
139
156
|
return res.status(401).json({ success: false, message: "User not found" });
|
|
@@ -202,98 +219,73 @@ const authapi = (requiredRole = []) => {
|
|
|
202
219
|
return (req, res, next) => {
|
|
203
220
|
const token = req.headers["authorization"];
|
|
204
221
|
|
|
205
|
-
|
|
206
|
-
console.log("[mbkauthe] [authapi] Received request with token:", token[0] + token[1] + token[2], ".....", token[63]);
|
|
207
|
-
} else {
|
|
208
|
-
console.log("[mbkauthe] [authapi] Token is not a valid string:", token);
|
|
209
|
-
}
|
|
210
|
-
|
|
222
|
+
// Validate token
|
|
211
223
|
if (!token) {
|
|
212
|
-
console.
|
|
224
|
+
console.warn("[mbkauthe] [authapi] No token provided in the request headers");
|
|
213
225
|
return res.status(401).json({
|
|
214
226
|
success: false,
|
|
215
227
|
message: "Authorization token is required"
|
|
216
228
|
});
|
|
217
229
|
}
|
|
218
230
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
message: "Internal Server Error"
|
|
227
|
-
});
|
|
228
|
-
}
|
|
231
|
+
if (typeof token !== 'string' || token.length === 0 || token.length > 512) {
|
|
232
|
+
console.warn("[mbkauthe] [authapi] Invalid token format");
|
|
233
|
+
return res.status(401).json({
|
|
234
|
+
success: false,
|
|
235
|
+
message: "Invalid authorization token format"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
229
238
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
236
|
-
}
|
|
239
|
+
if (typeof token === 'string' && token.length >= 64) {
|
|
240
|
+
console.log("[mbkauthe] [authapi] Received request with token:", token.substring(0, 3) + ".....", token.charAt(63));
|
|
241
|
+
} else {
|
|
242
|
+
console.log("[mbkauthe] [authapi] Received request with short token");
|
|
243
|
+
}
|
|
237
244
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
});
|
|
245
|
+
// Single query to validate API key and fetch user in one DB round trip.
|
|
246
|
+
(async () => {
|
|
247
|
+
try {
|
|
248
|
+
const jointQuery = `
|
|
249
|
+
SELECT u.id, u."UserName", u."Active", u."Role", k."key" as apikey
|
|
250
|
+
FROM "UserAuthApiKey" k
|
|
251
|
+
JOIN "Users" u ON u."UserName" = k.username
|
|
252
|
+
WHERE k."key" = $1 AND u."Active" = true
|
|
253
|
+
LIMIT 1
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
const result = await pool.query(jointQuery, [token]);
|
|
257
|
+
|
|
258
|
+
if (result.rows.length === 0) {
|
|
259
|
+
console.warn("[mbkauthe] [authapi] Invalid token or associated user inactive");
|
|
260
|
+
return res.status(401).json({ success: false, message: "The AuthApiToken Is Invalid or user inactive" });
|
|
254
261
|
}
|
|
255
262
|
|
|
256
|
-
|
|
257
|
-
console.log("[mbkauthe] [authapi] User does not exist or is not active. Username:", username);
|
|
258
|
-
return res.status(401).json({
|
|
259
|
-
success: false,
|
|
260
|
-
message: "User does not exist or is not active",
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
+
const user = result.rows[0];
|
|
263
264
|
|
|
264
|
-
if (
|
|
265
|
-
console.
|
|
266
|
-
return res.status(401).json({
|
|
267
|
-
success: false,
|
|
268
|
-
message: "Demo user is not allowed to access endpoints",
|
|
269
|
-
});
|
|
265
|
+
if (user.UserName === "demo") {
|
|
266
|
+
console.warn("[mbkauthe] [authapi] Demo user attempted to access an endpoint. Access denied.");
|
|
267
|
+
return res.status(401).json({ success: false, message: "Demo user is not allowed to access endpoints" });
|
|
270
268
|
}
|
|
271
269
|
|
|
272
|
-
|
|
273
|
-
console.log("[mbkauthe] [authapi] User is valid. User details:", user);
|
|
274
|
-
|
|
275
|
-
// Check if role is required and if user has it
|
|
270
|
+
// role check
|
|
276
271
|
if ((requiredRole && user.Role !== requiredRole) && user.Role !== "SuperAdmin") {
|
|
277
|
-
console.
|
|
278
|
-
return res.status(403).json({
|
|
279
|
-
success: false,
|
|
280
|
-
message: `Access denied. Required role: ${requiredRole}`,
|
|
281
|
-
});
|
|
272
|
+
console.warn(`[mbkauthe] [authapi] User does not have the required role. Required: ${requiredRole}, User's role: ${user.Role}`);
|
|
273
|
+
return res.status(403).json({ success: false, message: `Access denied. Required role: ${requiredRole}` });
|
|
282
274
|
}
|
|
283
275
|
|
|
284
|
-
console.log("[mbkauthe] [authapi] User has the required role or no specific role is required. Proceeding to next middleware.");
|
|
285
276
|
req.user = {
|
|
286
277
|
username: user.UserName,
|
|
287
278
|
UserName: user.UserName,
|
|
288
279
|
role: user.Role,
|
|
289
|
-
Role: user.
|
|
290
|
-
// Add other user properties you might need
|
|
280
|
+
Role: user.Role,
|
|
291
281
|
};
|
|
292
282
|
|
|
293
|
-
console.log("[mbkauthe] [authapi] Token and user validation successful. Passing control to next middleware.");
|
|
294
283
|
next();
|
|
295
|
-
})
|
|
296
|
-
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error("[mbkauthe] [authapi] Database error while validating token/user:", err);
|
|
286
|
+
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
297
289
|
};
|
|
298
290
|
};
|
|
299
291
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkauthe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MBKTechStudio's reusable authentication system for Node.js applications.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
8
|
+
"dev": "set test=dev&& nodemon index.js"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/MIbnEKhalid/mbkauthe#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"bcrypt": "^
|
|
29
|
+
"bcrypt": "^6.0.0",
|
|
30
30
|
"cheerio": "^1.0.0",
|
|
31
31
|
"connect-pg-simple": "^10.0.0",
|
|
32
32
|
"cookie-parser": "^1.4.7",
|
|
33
|
-
"csurf": "^1.
|
|
33
|
+
"csurf": "^1.2.2",
|
|
34
34
|
"dotenv": "^16.4.7",
|
|
35
35
|
"express": "^5.1.0",
|
|
36
36
|
"express-handlebars": "^8.0.1",
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"url": "^0.11.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"nodemon": "^
|
|
47
|
+
"nodemon": "^3.1.11"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/public/main.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
async function logout() {
|
|
2
|
+
const confirmation = confirm("Are you sure you want to logout?");
|
|
3
|
+
if (!confirmation) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// Clear all caches before logging out (except rememberedUsername)
|
|
9
|
+
await nuclearCacheClear();
|
|
10
|
+
|
|
11
|
+
const response = await fetch("/mbkauthe/api/logout", {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
"Cache-Control": "no-cache"
|
|
16
|
+
},
|
|
17
|
+
credentials: "include"
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const result = await response.json();
|
|
21
|
+
|
|
22
|
+
if (response.ok) {
|
|
23
|
+
alert(result.message);
|
|
24
|
+
// Force a full page reload with cache bypass
|
|
25
|
+
window.location.href = window.location.origin;
|
|
26
|
+
} else {
|
|
27
|
+
alert(result.message);
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("Error during logout:", error);
|
|
31
|
+
alert("Logout failed");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function nuclearCacheClear() {
|
|
36
|
+
try {
|
|
37
|
+
// Preserve rememberedUsername before clearing storage
|
|
38
|
+
const rememberedUsername = localStorage.getItem('rememberedUsername');
|
|
39
|
+
|
|
40
|
+
// 1. Clear all possible caches
|
|
41
|
+
if ('caches' in window) {
|
|
42
|
+
const cacheNames = await caches.keys();
|
|
43
|
+
await Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Clear service workers
|
|
47
|
+
if ('serviceWorker' in navigator) {
|
|
48
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
49
|
+
await Promise.all(registrations.map(registration => registration.unregister()));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Clear all storage except rememberedUsername
|
|
53
|
+
localStorage.clear();
|
|
54
|
+
sessionStorage.clear();
|
|
55
|
+
if (rememberedUsername) {
|
|
56
|
+
localStorage.setItem('rememberedUsername', rememberedUsername);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ('indexedDB' in window) {
|
|
60
|
+
try {
|
|
61
|
+
const dbs = await window.indexedDB.databases();
|
|
62
|
+
await Promise.all(dbs.map(db => {
|
|
63
|
+
if (db.name) {
|
|
64
|
+
return window.indexedDB.deleteDatabase(db.name);
|
|
65
|
+
}
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
}));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("Error clearing IndexedDB:", error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Clear cookies (except those you want to preserve)
|
|
74
|
+
document.cookie.split(';').forEach(cookie => {
|
|
75
|
+
const eqPos = cookie.indexOf('=');
|
|
76
|
+
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
|
|
77
|
+
|
|
78
|
+
// Skip cookies you want to preserve (add conditions as needed)
|
|
79
|
+
if (!name.startsWith('preserve_')) { // Example condition
|
|
80
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${window.location.hostname}`;
|
|
81
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname}`;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 5. Force hard reload with cache busting
|
|
86
|
+
window.location.reload();
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Nuclear cache clear failed:', error);
|
|
90
|
+
window.location.reload(true);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function logoutuser() {
|
|
95
|
+
await logout();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const validateSessionInterval = 60000;
|
|
99
|
+
// 1 minutes in milliseconds Function to check session validity by sending a request to the server
|
|
100
|
+
function checkSession() {
|
|
101
|
+
fetch("/api/validate-session")
|
|
102
|
+
.then((response) => {
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
// Redirect or handle errors (session expired, user inactive, etc.)
|
|
105
|
+
window.location.reload(); // Reload the page to update the session status
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
.catch((error) => console.error("Error checking session:", error));
|
|
109
|
+
}
|
|
110
|
+
// Call validateSession every 2 minutes (120000 milliseconds)
|
|
111
|
+
// setInterval(checkSession, validateSessionInterval);
|
|
112
|
+
|
|
113
|
+
function getCookieValue(cookieName) {
|
|
114
|
+
const cookies = document.cookie.split('; ');
|
|
115
|
+
for (let cookie of cookies) {
|
|
116
|
+
const [name, value] = cookie.split('=');
|
|
117
|
+
if (name === cookieName) {
|
|
118
|
+
return decodeURIComponent(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null; // Return null if the cookie is not found
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function loadpage(url) {
|
|
125
|
+
window.location.href = url;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatDate(date) {
|
|
129
|
+
return new Date(date).toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function reloadPage() {
|
|
133
|
+
window.location.reload();
|
|
134
|
+
}
|