mbkauthe 5.0.2 → 5.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 +9 -4
- package/docs/guides/configuration.md +1 -6
- package/docs/guides/database.md +1 -1
- package/docs/schema/db.sql +22 -1
- package/index.d.ts +19 -6
- package/lib/config/cookies.js +35 -10
- package/lib/config/index.js +162 -153
- package/lib/config/security.js +48 -8
- package/lib/db/AuthRepository.js +8 -8
- package/lib/main.js +2 -0
- package/lib/middleware/auth.js +36 -9
- package/lib/middleware/index.js +20 -20
- package/lib/routes/auth.js +21 -26
- package/lib/routes/misc.js +87 -24
- package/lib/routes/oauth.js +5 -5
- package/lib/utils/appAccess.js +8 -0
- package/lib/utils/dbQueryLogger.js +1 -1
- package/lib/utils/redirect.js +14 -0
- package/lib/utils/urlSafety.js +67 -0
- package/package.json +10 -7
- package/public/main.js +158 -99
- package/test.spec.js +73 -1
- package/views/head.handlebars +1 -0
- package/views/pages/accountSwitch.handlebars +11 -11
- package/views/pages/loginmbkauthe.handlebars +32 -24
- package/views/pages/test.handlebars +2 -3
- package/views/profilemenu.handlebars +1 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkauthe",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
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,174 @@
|
|
|
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
|
+
}
|
|
6
30
|
|
|
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"
|
|
16
|
-
});
|
|
31
|
+
const domains = new Set();
|
|
32
|
+
const configuredDomain = window.mbkautheConfig?.cookieDomain;
|
|
17
33
|
|
|
18
|
-
|
|
34
|
+
if (configuredDomain) {
|
|
35
|
+
domains.add(configuredDomain);
|
|
36
|
+
}
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// selectiveCacheClear already redirects, so no need for additional redirect
|
|
24
|
-
} else {
|
|
25
|
-
alert(result.message);
|
|
38
|
+
domains.add(hostname);
|
|
39
|
+
if (hostname.includes('.')) {
|
|
40
|
+
domains.add(`.${hostname}`);
|
|
26
41
|
}
|
|
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
42
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
43
|
+
return [...domains];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const clearCookie = (name) => {
|
|
47
|
+
document.cookie = `${name}=; ${EXPIRED_COOKIE}; path=/`;
|
|
48
|
+
|
|
49
|
+
getCookieDomains().forEach((domain) => {
|
|
50
|
+
document.cookie = `${name}=; ${EXPIRED_COOKIE}; path=/; domain=${domain}`;
|
|
64
51
|
});
|
|
52
|
+
};
|
|
65
53
|
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
const parseJson = async (response) => {
|
|
55
|
+
try {
|
|
56
|
+
return await response.json();
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
68
61
|
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
62
|
+
async function logout({ confirmLogout = true } = {}) {
|
|
63
|
+
if (confirmLogout && !confirm('Are you sure you want to logout?')) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch('/mbkauthe/api/logout', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'Cache-Control': 'no-cache'
|
|
73
|
+
},
|
|
74
|
+
credentials: 'include',
|
|
75
|
+
cache: 'no-store'
|
|
76
|
+
});
|
|
77
|
+
const result = await parseJson(response);
|
|
78
|
+
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
selectiveCacheClear();
|
|
81
|
+
return true;
|
|
87
82
|
}
|
|
83
|
+
|
|
84
|
+
alert(result.message || 'Logout failed. Please try again.');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(`${LOG_PREFIX} Error during logout:`, error);
|
|
87
|
+
alert(`Logout failed: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function selectiveCacheClear() {
|
|
94
|
+
try {
|
|
95
|
+
SESSION_KEYS.forEach((key) => localStorage.removeItem(key));
|
|
96
|
+
SESSION_KEYS.forEach(clearCookie);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`${LOG_PREFIX} selective cache clear failed:`, error);
|
|
99
|
+
} finally {
|
|
100
|
+
reloadPage();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function logoutuser() {
|
|
105
|
+
return logout();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkSession() {
|
|
109
|
+
return fetch('/mbkauthe/api/checkSession', {
|
|
110
|
+
credentials: 'include',
|
|
111
|
+
cache: 'no-store'
|
|
88
112
|
})
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
.then(async (response) => {
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
reloadPage();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const session = await parseJson(response);
|
|
120
|
+
if (session.sessionValid === false) {
|
|
121
|
+
reloadPage();
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.catch((error) => console.error(`${LOG_PREFIX} Error checking session:`, error));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getCookieValue(cookieName) {
|
|
128
|
+
if (!cookieName) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const prefix = `${cookieName}=`;
|
|
133
|
+
const cookie = document.cookie
|
|
134
|
+
.split('; ')
|
|
135
|
+
.find((entry) => entry.startsWith(prefix));
|
|
136
|
+
|
|
137
|
+
if (!cookie) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const value = cookie.slice(prefix.length);
|
|
142
|
+
|
|
143
|
+
try {
|
|
99
144
|
return decodeURIComponent(value);
|
|
145
|
+
} catch {
|
|
146
|
+
return value;
|
|
100
147
|
}
|
|
101
148
|
}
|
|
102
|
-
return null; // Return null if the cookie is not found
|
|
103
|
-
}
|
|
104
149
|
|
|
105
|
-
function loadpage(url) {
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
function loadpage(url) {
|
|
151
|
+
if (url) {
|
|
152
|
+
window.location.href = url;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
108
155
|
|
|
109
|
-
function formatDate(date) {
|
|
110
|
-
|
|
111
|
-
|
|
156
|
+
function formatDate(date) {
|
|
157
|
+
const parsedDate = new Date(date);
|
|
158
|
+
return Number.isNaN(parsedDate.getTime()) ? 'Invalid Date' : dateFormatter.format(parsedDate);
|
|
159
|
+
}
|
|
112
160
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
161
|
+
const api = {
|
|
162
|
+
checkSession,
|
|
163
|
+
formatDate,
|
|
164
|
+
getCookieValue,
|
|
165
|
+
loadpage,
|
|
166
|
+
logout,
|
|
167
|
+
logoutuser,
|
|
168
|
+
reloadPage,
|
|
169
|
+
selectiveCacheClear
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
window.mbkauthe = Object.assign(window.mbkauthe || {}, api);
|
|
173
|
+
Object.assign(window, api);
|
|
174
|
+
})();
|
package/test.spec.js
CHANGED
|
@@ -14,7 +14,13 @@ process.env.dbLogs = 'true';
|
|
|
14
14
|
process.env.dbLogsCallsite = 'false';
|
|
15
15
|
|
|
16
16
|
const { default: router } = await import('./lib/main.js');
|
|
17
|
-
const { packageJson } = await import('./lib/config/index.js');
|
|
17
|
+
const { packageJson, mbkautheVar } = await import('./lib/config/index.js');
|
|
18
|
+
const {
|
|
19
|
+
resolveCookieDomain,
|
|
20
|
+
isAllowedOriginHostname,
|
|
21
|
+
getCookieDomain,
|
|
22
|
+
cachedCookieOptions
|
|
23
|
+
} = await import('./lib/config/cookies.js');
|
|
18
24
|
const { dblogin } = await import('./lib/pool.js');
|
|
19
25
|
const {
|
|
20
26
|
attachDevQueryLogger,
|
|
@@ -23,6 +29,11 @@ const {
|
|
|
23
29
|
runWithRequestContext
|
|
24
30
|
} = await import('./lib/utils/dbQueryLogger.js');
|
|
25
31
|
|
|
32
|
+
const { isSafeRelativeRedirect, sanitizeRelativeRedirect } = await import('./lib/utils/redirect.js');
|
|
33
|
+
const { isSafeFetchUrl } = await import('./lib/utils/urlSafety.js');
|
|
34
|
+
const { hashPassword, verifyPassword } = await import('./lib/config/index.js');
|
|
35
|
+
const { hashDeviceToken } = await import('./lib/config/cookies.js');
|
|
36
|
+
|
|
26
37
|
const viewsPath = path.join(__dirname, 'views');
|
|
27
38
|
|
|
28
39
|
const handlebarsHelpers = {
|
|
@@ -252,6 +263,7 @@ describe('mbkauthe Routes', () => {
|
|
|
252
263
|
|
|
253
264
|
expect(response.status).toBe(200);
|
|
254
265
|
expect(response.headers['content-type']).toContain('javascript');
|
|
266
|
+
expect(response.text).toMatch(/^window\.mbkautheConfig=/);
|
|
255
267
|
});
|
|
256
268
|
|
|
257
269
|
test('GET /icon.svg returns SVG content', async () => {
|
|
@@ -680,4 +692,64 @@ describe('mbkauthe Routes', () => {
|
|
|
680
692
|
}
|
|
681
693
|
});
|
|
682
694
|
});
|
|
695
|
+
|
|
696
|
+
describe('Security hardening', () => {
|
|
697
|
+
test('sanitizeRelativeRedirect accepts safe paths and rejects open redirects', () => {
|
|
698
|
+
expect(sanitizeRelativeRedirect('/dashboard')).toBe('/dashboard');
|
|
699
|
+
expect(sanitizeRelativeRedirect('//evil.com')).toBeNull();
|
|
700
|
+
expect(sanitizeRelativeRedirect('https://evil.com')).toBeNull();
|
|
701
|
+
expect(isSafeRelativeRedirect('/app/settings')).toBe(true);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('isSafeFetchUrl blocks localhost and allows public https URLs', () => {
|
|
705
|
+
expect(isSafeFetchUrl('https://avatars.githubusercontent.com/u/1')).toBe(true);
|
|
706
|
+
expect(isSafeFetchUrl('http://127.0.0.1/secret')).toBe(false);
|
|
707
|
+
expect(isSafeFetchUrl('https://localhost/admin')).toBe(false);
|
|
708
|
+
expect(isSafeFetchUrl('ftp://example.com/icon.png')).toBe(false);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test('verifyPassword validates peppered password hashes', async () => {
|
|
712
|
+
const username = 'test.user';
|
|
713
|
+
const password = 'test-password-123';
|
|
714
|
+
const stored = hashPassword(password, username);
|
|
715
|
+
|
|
716
|
+
expect(stored).toMatch(/^[0-9a-f]{128}$/);
|
|
717
|
+
expect(await verifyPassword(password, username, stored)).toBe(true);
|
|
718
|
+
expect(await verifyPassword('wrong-password', username, stored)).toBe(false);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('hashDeviceToken returns a keyed hex digest', () => {
|
|
722
|
+
const token = 'a'.repeat(64);
|
|
723
|
+
expect(hashDeviceToken(token)).toMatch(/^[0-9a-f]{64}$/);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test('security headers are set on API responses', async () => {
|
|
727
|
+
const response = await request(app).get('/mbkauthe/main.js');
|
|
728
|
+
expect(response.headers['x-content-type-options']).toBe('nosniff');
|
|
729
|
+
expect(response.headers['x-frame-options']).toBe('SAMEORIGIN');
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe('Cross-subdomain cookie sharing', () => {
|
|
734
|
+
test('resolveCookieDomain returns parent domain only when deployed outside test dev', () => {
|
|
735
|
+
expect(resolveCookieDomain('true', 'mbktech.org', true)).toBeUndefined();
|
|
736
|
+
expect(resolveCookieDomain('false', 'mbktech.org', false)).toBeUndefined();
|
|
737
|
+
expect(resolveCookieDomain('true', 'mbktech.org', false)).toBe('.mbktech.org');
|
|
738
|
+
expect(resolveCookieDomain('true', '.mbktech.org', false)).toBe('.mbktech.org');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test('isAllowedOriginHostname accepts root and nested subdomains', () => {
|
|
742
|
+
expect(isAllowedOriginHostname('mbktech.org', 'mbktech.org')).toBe(true);
|
|
743
|
+
expect(isAllowedOriginHostname('auth.mbktech.org', 'mbktech.org')).toBe(true);
|
|
744
|
+
expect(isAllowedOriginHostname('app.auth.mbktech.org', 'mbktech.org')).toBe(true);
|
|
745
|
+
expect(isAllowedOriginHostname('notmbktech.org', 'mbktech.org')).toBe(false);
|
|
746
|
+
expect(isAllowedOriginHostname('mbktech.org.evil.com', 'mbktech.org')).toBe(false);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test('cached cookie options omit domain in test dev environment', () => {
|
|
750
|
+
expect(getCookieDomain()).toBeUndefined();
|
|
751
|
+
expect(cachedCookieOptions.domain).toBeUndefined();
|
|
752
|
+
expect(cachedCookieOptions.secure).toBe(false);
|
|
753
|
+
});
|
|
754
|
+
});
|
|
683
755
|
});
|
package/views/head.handlebars
CHANGED
|
@@ -340,7 +340,15 @@
|
|
|
340
340
|
</style>
|
|
341
341
|
|
|
342
342
|
<script>
|
|
343
|
-
|
|
343
|
+
function sanitizeRelativeRedirect(value) {
|
|
344
|
+
if (typeof value !== 'string') return null;
|
|
345
|
+
const trimmed = value.trim();
|
|
346
|
+
if (!trimmed.startsWith('/') || trimmed.startsWith('//')) return null;
|
|
347
|
+
if (trimmed.includes('://') || trimmed.includes('\\')) return null;
|
|
348
|
+
return trimmed;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const redirectTarget = sanitizeRelativeRedirect(new URLSearchParams(window.location.search).get('redirect')) || '{{customURL}}';
|
|
344
352
|
|
|
345
353
|
// Ensure login anchors include ?redirect using the redirect query param (not the current page URL)
|
|
346
354
|
(function setLoginHrefFromQuery() {
|
|
@@ -485,16 +493,8 @@
|
|
|
485
493
|
if (res.ok && data.success) {
|
|
486
494
|
// Prioritize explicit ?redirect if present and valid, else fall back to server-provided redirect or template
|
|
487
495
|
const params = new URLSearchParams(window.location.search);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
try {
|
|
491
|
-
const raw = params.get('redirect');
|
|
492
|
-
if (raw) queryRedirect = decodeURIComponent(raw);
|
|
493
|
-
} catch (e) {
|
|
494
|
-
queryRedirect = null;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
const finalRedirect = queryRedirect || data.redirect || redirectTarget;
|
|
496
|
+
const queryRedirect = sanitizeRelativeRedirect(params.get('redirect'));
|
|
497
|
+
const finalRedirect = data.redirect || queryRedirect || redirectTarget;
|
|
498
498
|
if (finalRedirect) window.location.href = finalRedirect;
|
|
499
499
|
} else {
|
|
500
500
|
showMessage(data.message || 'Could not switch account', 'Switch Account');
|
|
@@ -249,6 +249,14 @@
|
|
|
249
249
|
{{> versionInfo}}
|
|
250
250
|
|
|
251
251
|
<script>
|
|
252
|
+
function sanitizeRelativeRedirect(value) {
|
|
253
|
+
if (typeof value !== 'string') return null;
|
|
254
|
+
const trimmed = value.trim();
|
|
255
|
+
if (!trimmed.startsWith('/') || trimmed.startsWith('//')) return null;
|
|
256
|
+
if (trimmed.includes('://') || trimmed.includes('\\')) return null;
|
|
257
|
+
return trimmed;
|
|
258
|
+
}
|
|
259
|
+
|
|
252
260
|
// Toggle password visibility
|
|
253
261
|
const togglePassword = document.getElementById('togglePassword');
|
|
254
262
|
const passwordInput = document.getElementById('loginPassword');
|
|
@@ -301,7 +309,7 @@
|
|
|
301
309
|
|
|
302
310
|
// Pass redirect query param through to server so it can be used by 2FA flow
|
|
303
311
|
const urlParams = new URLSearchParams(window.location.search);
|
|
304
|
-
const pageRedirect = urlParams.get('redirect');
|
|
312
|
+
const pageRedirect = sanitizeRelativeRedirect(urlParams.get('redirect'));
|
|
305
313
|
fetch('/mbkauthe/api/login', {
|
|
306
314
|
method: 'POST',
|
|
307
315
|
credentials: 'include',
|
|
@@ -330,9 +338,8 @@
|
|
|
330
338
|
deleteCookie('rememberedUsername');
|
|
331
339
|
}
|
|
332
340
|
|
|
333
|
-
// Redirect to the
|
|
334
|
-
|
|
335
|
-
window.location.href = redirectUrl ? decodeURIComponent(redirectUrl) : '{{customURL}}';
|
|
341
|
+
// Redirect to the validated target from the server or safe query param
|
|
342
|
+
window.location.href = data.redirectUrl || pageRedirect || '{{customURL}}';
|
|
336
343
|
}
|
|
337
344
|
} else {
|
|
338
345
|
// Handle errors
|
|
@@ -349,7 +356,9 @@
|
|
|
349
356
|
});
|
|
350
357
|
});
|
|
351
358
|
|
|
352
|
-
// Cookie helper functions for cross-
|
|
359
|
+
// Cookie helper functions for cross-subdomain sharing
|
|
360
|
+
const cookieDomain = '{{cookieDomain}}';
|
|
361
|
+
|
|
353
362
|
function setCookie(name, value, days) {
|
|
354
363
|
let expires = "";
|
|
355
364
|
if (days) {
|
|
@@ -357,11 +366,11 @@
|
|
|
357
366
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
358
367
|
expires = "; expires=" + date.toUTCString();
|
|
359
368
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
document.cookie =
|
|
369
|
+
let cookieValue = name + "=" + (value || "") + expires + "; path=/; SameSite=Lax";
|
|
370
|
+
if (cookieDomain) {
|
|
371
|
+
cookieValue += "; domain=" + cookieDomain;
|
|
372
|
+
}
|
|
373
|
+
document.cookie = cookieValue;
|
|
365
374
|
}
|
|
366
375
|
|
|
367
376
|
function getCookie(name) {
|
|
@@ -376,19 +385,26 @@
|
|
|
376
385
|
}
|
|
377
386
|
|
|
378
387
|
function deleteCookie(name) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
388
|
+
let cookieValue = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";
|
|
389
|
+
if (cookieDomain) {
|
|
390
|
+
cookieValue += "; domain=" + cookieDomain;
|
|
391
|
+
}
|
|
392
|
+
document.cookie = cookieValue;
|
|
383
393
|
}
|
|
384
394
|
|
|
385
395
|
// Check for URL parameters
|
|
386
396
|
document.addEventListener('DOMContentLoaded', function () {
|
|
387
397
|
const urlParams = new URLSearchParams(window.location.search);
|
|
388
|
-
const usernameFromUrl = urlParams.get('username');
|
|
389
|
-
const passwordFromUrl = urlParams.get('password');
|
|
390
398
|
const usernameInput = document.getElementById('loginUsername');
|
|
391
399
|
|
|
400
|
+
if (urlParams.has('password')) {
|
|
401
|
+
urlParams.delete('password');
|
|
402
|
+
const sanitizedQuery = urlParams.toString();
|
|
403
|
+
const nextUrl = `${window.location.pathname}${sanitizedQuery ? `?${sanitizedQuery}` : ''}${window.location.hash}`;
|
|
404
|
+
window.history.replaceState({}, document.title, nextUrl);
|
|
405
|
+
showMessage('Passwords must not be passed in the URL. Enter your password in the form instead.', 'Security Notice');
|
|
406
|
+
}
|
|
407
|
+
|
|
392
408
|
// Check for remembered username in cookies
|
|
393
409
|
const rememberedUsername = getCookie('rememberedUsername');
|
|
394
410
|
if (rememberedUsername) {
|
|
@@ -396,14 +412,6 @@
|
|
|
396
412
|
document.getElementById('rememberMe').checked = true;
|
|
397
413
|
}
|
|
398
414
|
|
|
399
|
-
if (usernameFromUrl) {
|
|
400
|
-
usernameInput.value = usernameFromUrl;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (passwordFromUrl) {
|
|
404
|
-
document.getElementById('loginPassword').value = passwordFromUrl;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
415
|
// Automatically focus the first empty field
|
|
408
416
|
if (!usernameInput.value) {
|
|
409
417
|
usernameInput.focus();
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
<div>
|
|
195
195
|
<div class="session-status">✅ Authentication successful</div>
|
|
196
196
|
<h3 class="session-title">{{username}} <small class="session-role">· {{role}}</small></h3>
|
|
197
|
-
<p class="session-sub">
|
|
197
|
+
<p class="session-sub">UserId: {{userId}} · Session: {{sessionIdShort}}…</p>
|
|
198
198
|
</div>
|
|
199
199
|
|
|
200
200
|
<div class="session-details" aria-live="polite">
|
|
@@ -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.');
|