mbkauthe 5.0.1 → 5.0.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/LICENSE +161 -335
- package/README.md +9 -4
- package/docs/diagrams/auth-processes.mmd +37 -77
- package/docs/images/auth-processes.svg +1 -1
- package/lib/config/index.js +157 -152
- package/lib/middleware/auth.js +32 -6
- package/lib/middleware/index.js +1 -5
- package/lib/routes/auth.js +7 -7
- package/lib/routes/misc.js +55 -17
- package/lib/routes/oauth.js +1 -1
- package/package.json +10 -7
- package/public/main.js +146 -99
- package/views/head.handlebars +1 -0
- package/views/pages/test.handlebars +1 -2
- package/views/profilemenu.handlebars +1 -15
- package/docs/diagrams/c.md +0 -122
- package/docs/images/auth-process.svg +0 -102
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkauthe",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"description": "MBKTech's reusable authentication system for Node.js applications.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
"create-tables": "node lib/createTable.js",
|
|
11
11
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
12
12
|
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
13
|
+
"tets": "npm run test",
|
|
13
14
|
"render:mermaid": "npx @mermaid-js/mermaid-cli -i docs/diagrams/auth-flows.mmd -o docs/images/auth-flows.svg",
|
|
14
|
-
"render:mermaid:png": "npx @mermaid-js/mermaid-cli -i docs/diagrams/auth-flows.mmd -o docs/images/auth-flows.png -s 20"
|
|
15
|
+
"render:mermaid:png": "npx @mermaid-js/mermaid-cli -i docs/diagrams/auth-flows.mmd -o docs/images/auth-flows.png -s 20",
|
|
16
|
+
"install-mermaid": "npm i @mermaid-js/mermaid-cli@11.15.0 --save-dev"
|
|
15
17
|
},
|
|
16
18
|
"imports": {
|
|
17
19
|
"#pool.js": "./lib/pool.js",
|
|
@@ -29,10 +31,13 @@
|
|
|
29
31
|
"nodejs",
|
|
30
32
|
"express",
|
|
31
33
|
"session",
|
|
32
|
-
"security"
|
|
34
|
+
"security",
|
|
35
|
+
"oauth",
|
|
36
|
+
"2fa",
|
|
37
|
+
"csrf"
|
|
33
38
|
],
|
|
34
|
-
"author": "Muhammad Bin Khalid <support@mbktech.org>",
|
|
35
|
-
"license": "
|
|
39
|
+
"author": "Muhammad Bin Khalid <support@mbktech.org> <chmuhammadbinkhalid28@gmail.com>",
|
|
40
|
+
"license": "LGPL-3.0-only",
|
|
36
41
|
"bugs": {
|
|
37
42
|
"url": "https://github.com/MIbnEKhalid/mbkauthe/issues"
|
|
38
43
|
},
|
|
@@ -53,7 +58,6 @@
|
|
|
53
58
|
"registry": "https://registry.npmjs.org/"
|
|
54
59
|
},
|
|
55
60
|
"dependencies": {
|
|
56
|
-
"@mermaid-js/mermaid-cli": "^11.15.0",
|
|
57
61
|
"bytes": "^3.1.2",
|
|
58
62
|
"connect-pg-simple": "^10.0.0",
|
|
59
63
|
"content-type": "^1.0.5",
|
|
@@ -99,7 +103,6 @@
|
|
|
99
103
|
}
|
|
100
104
|
},
|
|
101
105
|
"devDependencies": {
|
|
102
|
-
"@types/express": "^5.0.6",
|
|
103
106
|
"cross-env": "^7.0.3",
|
|
104
107
|
"jest": "^30.2.0",
|
|
105
108
|
"nodemon": "^3.1.11",
|
package/public/main.js
CHANGED
|
@@ -1,115 +1,162 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
(() => {
|
|
2
|
+
const SESSION_KEYS = [
|
|
3
|
+
'sessionId',
|
|
4
|
+
'mbkauthe.sid',
|
|
5
|
+
'fullName',
|
|
6
|
+
'_csrf',
|
|
7
|
+
'profileImageUser',
|
|
8
|
+
'profileImageUrl'
|
|
9
|
+
];
|
|
10
|
+
const LOG_PREFIX = '[mbkauthe]';
|
|
11
|
+
const EXPIRED_COOKIE = 'expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
12
|
+
const dateFormatter = new Intl.DateTimeFormat('en-GB', {
|
|
13
|
+
day: '2-digit',
|
|
14
|
+
month: '2-digit',
|
|
15
|
+
year: 'numeric',
|
|
16
|
+
hour: 'numeric',
|
|
17
|
+
minute: 'numeric',
|
|
18
|
+
second: 'numeric',
|
|
19
|
+
hour12: true
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const reloadPage = () => window.location.reload();
|
|
23
|
+
|
|
24
|
+
const getCookieDomains = () => {
|
|
25
|
+
const hostname = window.location.hostname;
|
|
26
|
+
|
|
27
|
+
if (!hostname) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [...new Set([hostname, hostname.includes('.') ? `.${hostname}` : null].filter(Boolean))];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const clearCookie = (name) => {
|
|
35
|
+
document.cookie = `${name}=; ${EXPIRED_COOKIE}; path=/`;
|
|
6
36
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const response = await fetch("/mbkauthe/api/logout", {
|
|
10
|
-
method: "POST",
|
|
11
|
-
headers: {
|
|
12
|
-
"Content-Type": "application/json",
|
|
13
|
-
"Cache-Control": "no-cache"
|
|
14
|
-
},
|
|
15
|
-
credentials: "include"
|
|
37
|
+
getCookieDomains().forEach((domain) => {
|
|
38
|
+
document.cookie = `${name}=; ${EXPIRED_COOKIE}; path=/; domain=${domain}`;
|
|
16
39
|
});
|
|
40
|
+
};
|
|
17
41
|
|
|
18
|
-
|
|
42
|
+
const parseJson = async (response) => {
|
|
43
|
+
try {
|
|
44
|
+
return await response.json();
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
19
49
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// selectiveCacheClear already redirects, so no need for additional redirect
|
|
24
|
-
} else {
|
|
25
|
-
alert(result.message);
|
|
50
|
+
async function logout({ confirmLogout = true } = {}) {
|
|
51
|
+
if (confirmLogout && !confirm('Are you sure you want to logout?')) {
|
|
52
|
+
return false;
|
|
26
53
|
}
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error(`[mbkauthe] Error during logout:`, error);
|
|
29
|
-
alert(`Logout failed: ${error.message}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function selectiveCacheClear() {
|
|
34
|
-
try {
|
|
35
|
-
|
|
36
|
-
const cookiesToClear = [
|
|
37
|
-
'sessionId',
|
|
38
|
-
'mbkauthe.sid',
|
|
39
|
-
'fullName',
|
|
40
|
-
'_csrf',
|
|
41
|
-
'profileImageUser',
|
|
42
|
-
'profileImageUrl'
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const localStorageToClear = [
|
|
46
|
-
'sessionId',
|
|
47
|
-
'mbkauthe.sid',
|
|
48
|
-
'fullName',
|
|
49
|
-
'_csrf',
|
|
50
|
-
'profileImageUser',
|
|
51
|
-
'profileImageUrl'
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
// 1. Clear selected localStorage keys
|
|
55
|
-
localStorageToClear.forEach(key => {
|
|
56
|
-
localStorage.removeItem(key);
|
|
57
|
-
});
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch('/mbkauthe/api/logout', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'Cache-Control': 'no-cache'
|
|
61
|
+
},
|
|
62
|
+
credentials: 'include',
|
|
63
|
+
cache: 'no-store'
|
|
64
|
+
});
|
|
65
|
+
const result = await parseJson(response);
|
|
66
|
+
|
|
67
|
+
if (response.ok) {
|
|
68
|
+
selectiveCacheClear();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
alert(result.message || 'Logout failed. Please try again.');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`${LOG_PREFIX} Error during logout:`, error);
|
|
75
|
+
alert(`Logout failed: ${error.message}`);
|
|
76
|
+
}
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
console.error(`[mbkauthe] selective cache clear failed:`, error);
|
|
71
|
-
window.location.reload();
|
|
78
|
+
return false;
|
|
72
79
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
|
|
81
|
+
function selectiveCacheClear() {
|
|
82
|
+
try {
|
|
83
|
+
SESSION_KEYS.forEach((key) => localStorage.removeItem(key));
|
|
84
|
+
SESSION_KEYS.forEach(clearCookie);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(`${LOG_PREFIX} selective cache clear failed:`, error);
|
|
87
|
+
} finally {
|
|
88
|
+
reloadPage();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function logoutuser() {
|
|
93
|
+
return logout();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function checkSession() {
|
|
97
|
+
return fetch('/mbkauthe/api/checkSession', {
|
|
98
|
+
credentials: 'include',
|
|
99
|
+
cache: 'no-store'
|
|
88
100
|
})
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
.then(async (response) => {
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
reloadPage();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const session = await parseJson(response);
|
|
108
|
+
if (session.sessionValid === false) {
|
|
109
|
+
reloadPage();
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.catch((error) => console.error(`${LOG_PREFIX} Error checking session:`, error));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getCookieValue(cookieName) {
|
|
116
|
+
if (!cookieName) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const prefix = `${cookieName}=`;
|
|
121
|
+
const cookie = document.cookie
|
|
122
|
+
.split('; ')
|
|
123
|
+
.find((entry) => entry.startsWith(prefix));
|
|
124
|
+
|
|
125
|
+
if (!cookie) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const value = cookie.slice(prefix.length);
|
|
130
|
+
|
|
131
|
+
try {
|
|
99
132
|
return decodeURIComponent(value);
|
|
133
|
+
} catch {
|
|
134
|
+
return value;
|
|
100
135
|
}
|
|
101
136
|
}
|
|
102
|
-
return null; // Return null if the cookie is not found
|
|
103
|
-
}
|
|
104
137
|
|
|
105
|
-
function loadpage(url) {
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
function loadpage(url) {
|
|
139
|
+
if (url) {
|
|
140
|
+
window.location.href = url;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
108
143
|
|
|
109
|
-
function formatDate(date) {
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
function formatDate(date) {
|
|
145
|
+
const parsedDate = new Date(date);
|
|
146
|
+
return Number.isNaN(parsedDate.getTime()) ? 'Invalid Date' : dateFormatter.format(parsedDate);
|
|
147
|
+
}
|
|
112
148
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
149
|
+
const api = {
|
|
150
|
+
checkSession,
|
|
151
|
+
formatDate,
|
|
152
|
+
getCookieValue,
|
|
153
|
+
loadpage,
|
|
154
|
+
logout,
|
|
155
|
+
logoutuser,
|
|
156
|
+
reloadPage,
|
|
157
|
+
selectiveCacheClear
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
window.mbkauthe = Object.assign(window.mbkauthe || {}, api);
|
|
161
|
+
Object.assign(window, api);
|
|
162
|
+
})();
|
package/views/head.handlebars
CHANGED
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
</div>
|
|
206
206
|
|
|
207
207
|
<div class="session-actions">
|
|
208
|
-
<button class="btn btn-danger" onclick="logout()" aria-label="Log out">
|
|
208
|
+
<button class="btn btn-danger" onclick="mbkauthe.logout()" aria-label="Log out">
|
|
209
209
|
<i class="fas fa-sign-out-alt"></i> Logout
|
|
210
210
|
</button>
|
|
211
211
|
<a class="btn btn-outline" href="https://portal.mbktech.org/">
|
|
@@ -233,7 +233,6 @@
|
|
|
233
233
|
</div>
|
|
234
234
|
</section>
|
|
235
235
|
|
|
236
|
-
<script src="/mbkauthe/main.js{{cacheBuster}}"></script>
|
|
237
236
|
{{#if sessionExpiry}}
|
|
238
237
|
<script>
|
|
239
238
|
(function () {
|
|
@@ -261,21 +261,7 @@
|
|
|
261
261
|
logoutButton.textContent = 'Logging out...';
|
|
262
262
|
|
|
263
263
|
try {
|
|
264
|
-
|
|
265
|
-
method: 'POST',
|
|
266
|
-
headers: {
|
|
267
|
-
'Content-Type': 'application/json'
|
|
268
|
-
},
|
|
269
|
-
credentials: 'include'
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const result = await response.json().catch(() => ({}));
|
|
273
|
-
|
|
274
|
-
if (response.ok) {
|
|
275
|
-
window.location.reload();
|
|
276
|
-
} else {
|
|
277
|
-
alert(result.message || 'Logout failed. Please try again.');
|
|
278
|
-
}
|
|
264
|
+
await mbkauthe.logout();
|
|
279
265
|
} catch (error) {
|
|
280
266
|
console.error(`[mbkauthe] Error during logout:`, error);
|
|
281
267
|
alert('Logout failed. Please try again.');
|
package/docs/diagrams/c.md
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
```mermaid
|
|
2
|
-
sequenceDiagram
|
|
3
|
-
autonumber
|
|
4
|
-
|
|
5
|
-
participant R as Protected Route
|
|
6
|
-
participant VS as validateSession
|
|
7
|
-
participant JT as isJsonRequest
|
|
8
|
-
participant VT as validateTokenAuthentication
|
|
9
|
-
participant CS as validateCookieSession
|
|
10
|
-
participant DB as AuthRepository / DB
|
|
11
|
-
participant RES as Response
|
|
12
|
-
|
|
13
|
-
R->>VS: validateSession(req, res, next, strictTokenValidation?)
|
|
14
|
-
|
|
15
|
-
alt Authorization header exists
|
|
16
|
-
alt strictTokenValidation is true
|
|
17
|
-
VS-->>RES: 401 INVALID_AUTH_TOKEN
|
|
18
|
-
Note over VS,RES: Token auth is rejected for strict cookie-only middleware.
|
|
19
|
-
else token auth allowed
|
|
20
|
-
VS->>VT: validateTokenAuthentication(req)
|
|
21
|
-
|
|
22
|
-
alt Header is not "Bearer <token>"
|
|
23
|
-
VT-->>VS: null
|
|
24
|
-
VS-->>RES: 401 INVALID_AUTH_TOKEN
|
|
25
|
-
else Token does not start with mbk_ or is too long
|
|
26
|
-
VT-->>VS: { error: "INVALID_TOKEN" }
|
|
27
|
-
VS-->>RES: 401 INVALID_AUTH_TOKEN
|
|
28
|
-
else Token format is accepted
|
|
29
|
-
VT->>VT: hashApiToken(token)
|
|
30
|
-
VT->>DB: getApiTokenByHash(tokenHash)
|
|
31
|
-
|
|
32
|
-
alt API token row not found
|
|
33
|
-
DB-->>VT: null
|
|
34
|
-
VT-->>VS: { error: "INVALID_TOKEN" }
|
|
35
|
-
VS-->>RES: 401 INVALID_AUTH_TOKEN
|
|
36
|
-
else API token expired
|
|
37
|
-
DB-->>VT: row with ExpiresAt <= now
|
|
38
|
-
VT-->>VS: { error: "TOKEN_EXPIRED" }
|
|
39
|
-
VS-->>RES: 401 API_TOKEN_EXPIRED
|
|
40
|
-
else API token found
|
|
41
|
-
DB-->>VT: token row joined to user
|
|
42
|
-
VT->>VT: Resolve token scope and allowedApps
|
|
43
|
-
VT->>DB: updateApiTokenLastUsed(row.id) async
|
|
44
|
-
VT-->>VS: tokenUser
|
|
45
|
-
|
|
46
|
-
alt tokenUser.active is false
|
|
47
|
-
VS-->>RES: 401 ACCOUNT_INACTIVE
|
|
48
|
-
else Non-SuperAdmin has no allowed apps
|
|
49
|
-
VS-->>RES: 401 APP_NOT_AUTHORIZED
|
|
50
|
-
else Token allowedApps has wildcard but user apps do not include APP_NAME
|
|
51
|
-
VS-->>RES: 401 APP_NOT_AUTHORIZED
|
|
52
|
-
else Token allowedApps does not include APP_NAME
|
|
53
|
-
VS-->>RES: 401 APP_NOT_AUTHORIZED
|
|
54
|
-
else App access allowed
|
|
55
|
-
VS->>VS: attachApiTokenUser(req, res, tokenUser)
|
|
56
|
-
Note over VS: Sets req.auth, req.user, req.userRole,<br/>and request-local req.session.user.
|
|
57
|
-
|
|
58
|
-
alt tokenScope does not allow req.method
|
|
59
|
-
VS-->>RES: 403 TOKEN_SCOPE_INSUFFICIENT
|
|
60
|
-
else token scope allows method
|
|
61
|
-
VS-->>R: next()
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
else No Authorization header
|
|
68
|
-
VS->>JT: isJsonRequest(req)
|
|
69
|
-
JT-->>VS: prefersJson
|
|
70
|
-
VS->>CS: validateCookieSession(req, res, next, prefersJson)
|
|
71
|
-
|
|
72
|
-
alt req.session.user is missing
|
|
73
|
-
alt prefersJson
|
|
74
|
-
CS-->>RES: 401 SESSION_NOT_FOUND
|
|
75
|
-
else browser/page request
|
|
76
|
-
CS-->>RES: 302 /mbkauthe/login?redirect=...&reason=logged_out
|
|
77
|
-
end
|
|
78
|
-
else req.session.user exists
|
|
79
|
-
CS->>CS: Read req.session.user.sessionId
|
|
80
|
-
|
|
81
|
-
alt sessionId missing or not UUID
|
|
82
|
-
CS->>CS: destroy session and clear cookies
|
|
83
|
-
alt prefersJson
|
|
84
|
-
CS-->>RES: 401 SESSION_EXPIRED
|
|
85
|
-
else browser/page request
|
|
86
|
-
CS-->>RES: Render "Session Expired" error page
|
|
87
|
-
end
|
|
88
|
-
else sessionId is UUID
|
|
89
|
-
CS->>DB: getSessionAuthData(sessionId)
|
|
90
|
-
|
|
91
|
-
alt DB session row not found
|
|
92
|
-
CS->>CS: destroy session and clear cookies
|
|
93
|
-
alt prefersJson
|
|
94
|
-
CS-->>RES: 401 SESSION_INVALID
|
|
95
|
-
else browser/page request
|
|
96
|
-
CS-->>RES: Render "Session Expired" error page
|
|
97
|
-
end
|
|
98
|
-
else DB session row found
|
|
99
|
-
DB-->>CS: session row joined to user
|
|
100
|
-
|
|
101
|
-
alt session expired
|
|
102
|
-
CS->>CS: destroy session and clear cookies
|
|
103
|
-
CS-->>RES: 401 SESSION_EXPIRED or rendered error page
|
|
104
|
-
else user account inactive
|
|
105
|
-
CS->>CS: destroy session and clear cookies
|
|
106
|
-
CS-->>RES: 401 ACCOUNT_INACTIVE or rendered support page
|
|
107
|
-
else user not allowed for APP_NAME
|
|
108
|
-
CS->>CS: destroy session and clear cookies
|
|
109
|
-
CS-->>RES: 401 APP_NOT_AUTHORIZED or rendered unauthorized page
|
|
110
|
-
else session valid and app access allowed
|
|
111
|
-
CS->>CS: req.userRole = sessionRow.Role
|
|
112
|
-
CS-->>R: next()
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
opt Token or session validation throws
|
|
120
|
-
VS-->>RES: 500 INTERNAL_SERVER_ERROR
|
|
121
|
-
end
|
|
122
|
-
```
|