mbkauthe 4.8.4 → 5.0.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 +1 -0
- package/docs/api.md +29 -178
- package/docs/db.md +1 -1
- package/docs/db.sql +305 -253
- package/index.js +6 -3
- package/lib/config/cookies.js +84 -18
- package/lib/config/index.js +3 -1
- package/lib/config/tokenScopes.js +1 -1
- package/lib/createTable.js +95 -8
- package/lib/db/AuthRepository.js +336 -0
- package/lib/db/BaseRepository.js +193 -0
- package/lib/db/dialects/postgres.js +18 -0
- package/lib/main.js +5 -5
- package/lib/middleware/auth.js +213 -259
- package/lib/middleware/index.js +18 -25
- package/lib/middleware/scopeValidator.js +8 -3
- package/lib/pool.js +5 -6
- package/lib/routes/auth.js +95 -169
- package/lib/routes/dbLogs.js +247 -29
- package/lib/routes/misc.js +17 -50
- package/lib/routes/oauth.js +23 -48
- package/lib/utils/dbQueryLogger.js +485 -80
- package/lib/utils/errors.js +1 -1
- package/lib/utils/logger.js +12 -0
- package/lib/utils/timingSafeToken.js +1 -1
- package/package.json +1 -1
- package/public/main.css +36 -3
- package/test.spec.js +515 -48
- package/views/header.handlebars +1 -1
- package/views/pages/2fa.handlebars +9 -5
- package/views/pages/dbLogs.handlebars +618 -420
- package/views/pages/loginmbkauthe.handlebars +42 -25
- package/views/showmessage.handlebars +2 -2
package/lib/middleware/auth.js
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import { dblogin } from "#pool.js";
|
|
2
2
|
import { mbkautheVar } from "#config.js";
|
|
3
3
|
import { renderError } from "#response.js";
|
|
4
|
-
import { clearSessionCookies, cachedCookieOptions,
|
|
4
|
+
import { clearSessionCookies, cachedCookieOptions, encryptSessionId } from "#cookies.js";
|
|
5
5
|
import { ErrorCodes, createErrorResponse } from "../utils/errors.js";
|
|
6
6
|
import { hashApiToken } from "#config.js";
|
|
7
7
|
import { canAccessMethod } from "#config.js";
|
|
8
8
|
import { extractAuthorizationToken, timingSafeTokenMatch } from "../utils/timingSafeToken.js";
|
|
9
|
+
import { AuthRepository } from "../db/AuthRepository.js";
|
|
10
|
+
import { createLogger } from "../utils/logger.js";
|
|
9
11
|
|
|
10
12
|
const IS_DEV = process.env.env === 'dev' || process.env.test === 'dev' || process.env.NODE_ENV === 'development';
|
|
11
13
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
14
|
const isUuid = (val) => typeof val === 'string' && UUID_RE.test(val);
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
18
|
-
WHERE s.id = $1
|
|
19
|
-
LIMIT 1
|
|
20
|
-
`;
|
|
15
|
+
const MAX_API_TOKEN_LENGTH = 4096;
|
|
16
|
+
const API_TOKEN_SESSION_RESTORE = Symbol('mbkauthe.apiTokenSessionRestore');
|
|
17
|
+
const authRepo = new AuthRepository({ db: dblogin });
|
|
18
|
+
const logAuth = createLogger("auth");
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
21
|
* Decide if the incoming request should return JSON errors instead of HTML.
|
|
@@ -63,19 +61,12 @@ async function validateTokenAuthentication(req) {
|
|
|
63
61
|
|
|
64
62
|
// 1. Check for API Token (mbk_)
|
|
65
63
|
if (token.startsWith('mbk_')) {
|
|
64
|
+
if (token.length > MAX_API_TOKEN_LENGTH) return { error: 'INVALID_TOKEN' };
|
|
65
|
+
|
|
66
66
|
const tokenHash = hashApiToken(token);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
FROM "ApiTokens" t
|
|
71
|
-
JOIN "Users" u ON t."UserName" = u."UserName"
|
|
72
|
-
WHERE t."TokenHash" = $1 LIMIT 1
|
|
73
|
-
`;
|
|
74
|
-
const tokenResult = await dblogin.query({ name: 'validate-api-token', text: tokenQuery, values: [tokenHash] });
|
|
75
|
-
|
|
76
|
-
if (tokenResult.rows.length === 0) return { error: 'INVALID_TOKEN' };
|
|
77
|
-
|
|
78
|
-
const row = tokenResult.rows[0];
|
|
67
|
+
const row = await authRepo.getApiTokenByHash(tokenHash);
|
|
68
|
+
|
|
69
|
+
if (!row) return { error: 'INVALID_TOKEN' };
|
|
79
70
|
if (row.ExpiresAt && new Date(row.ExpiresAt) <= new Date()) return { error: 'TOKEN_EXPIRED' };
|
|
80
71
|
|
|
81
72
|
// Parse permissions from JSONB
|
|
@@ -89,11 +80,8 @@ async function validateTokenAuthentication(req) {
|
|
|
89
80
|
allowedApps = tokenAllowedApps;
|
|
90
81
|
}
|
|
91
82
|
|
|
92
|
-
// Update usage
|
|
93
|
-
|
|
94
|
-
text: 'UPDATE "ApiTokens" SET "LastUsed" = NOW() WHERE id = $1',
|
|
95
|
-
values: [row.id]
|
|
96
|
-
}).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
|
|
83
|
+
// Update usage opportunistically, but not on every request.
|
|
84
|
+
authRepo.updateApiTokenLastUsed(row.id).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
|
|
97
85
|
|
|
98
86
|
return {
|
|
99
87
|
id: row.uid,
|
|
@@ -111,186 +99,161 @@ async function validateTokenAuthentication(req) {
|
|
|
111
99
|
return null;
|
|
112
100
|
}
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const tokenUser = await validateTokenAuthentication(req);
|
|
127
|
-
|
|
128
|
-
if (tokenUser && !tokenUser.error) {
|
|
129
|
-
if (!tokenUser.active) {
|
|
130
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// SuperAdmin bypasses app permission checks
|
|
134
|
-
if (tokenUser.role !== "SuperAdmin") {
|
|
135
|
-
// API tokens must respect their app restrictions for non-SuperAdmin users
|
|
136
|
-
const allowedApps = tokenUser.allowedApps;
|
|
137
|
-
const userAllowedApps = tokenUser.userAllowedApps;
|
|
102
|
+
function attachApiTokenUser(req, res, tokenUser) {
|
|
103
|
+
const user = {
|
|
104
|
+
id: tokenUser.id,
|
|
105
|
+
username: tokenUser.username,
|
|
106
|
+
fullname: tokenUser.fullname,
|
|
107
|
+
role: tokenUser.role,
|
|
108
|
+
sessionId: tokenUser.sessionId,
|
|
109
|
+
allowedApps: tokenUser.allowedApps,
|
|
110
|
+
tokenScope: tokenUser.tokenScope || null,
|
|
111
|
+
};
|
|
138
112
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
113
|
+
req.auth = {
|
|
114
|
+
type: 'api-token',
|
|
115
|
+
user,
|
|
116
|
+
tokenScope: user.tokenScope,
|
|
117
|
+
allowedApps: user.allowedApps,
|
|
118
|
+
};
|
|
144
119
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
120
|
+
// Backwards compatibility: existing protected routes commonly read
|
|
121
|
+
// req.session.user. For API tokens this must be request-local so the
|
|
122
|
+
// Postgres session store is not dirtied/saved on every token request.
|
|
123
|
+
if (req.session) {
|
|
124
|
+
const originalDescriptor = Object.getOwnPropertyDescriptor(req.session, 'user');
|
|
125
|
+
|
|
126
|
+
Object.defineProperty(req.session, 'user', {
|
|
127
|
+
value: user,
|
|
128
|
+
enumerable: false,
|
|
129
|
+
configurable: true,
|
|
130
|
+
writable: true,
|
|
131
|
+
});
|
|
148
132
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
133
|
+
if (res && !req.session[API_TOKEN_SESSION_RESTORE]) {
|
|
134
|
+
req.session[API_TOKEN_SESSION_RESTORE] = true;
|
|
135
|
+
const originalEnd = res.end;
|
|
136
|
+
let restored = false;
|
|
137
|
+
|
|
138
|
+
res.end = function apiTokenSessionEnd(...args) {
|
|
139
|
+
if (!restored) {
|
|
140
|
+
restored = true;
|
|
141
|
+
if (originalDescriptor) {
|
|
142
|
+
Object.defineProperty(req.session, 'user', originalDescriptor);
|
|
143
|
+
} else {
|
|
144
|
+
delete req.session.user;
|
|
158
145
|
}
|
|
146
|
+
delete req.session[API_TOKEN_SESSION_RESTORE];
|
|
159
147
|
}
|
|
148
|
+
return originalEnd.apply(this, args);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
160
152
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
fullname: tokenUser.fullname,
|
|
166
|
-
role: tokenUser.role,
|
|
167
|
-
sessionId: tokenUser.sessionId,
|
|
168
|
-
allowedApps: tokenUser.allowedApps,
|
|
169
|
-
tokenScope: tokenUser.tokenScope || null, // Add scope for token-based auth
|
|
170
|
-
};
|
|
171
|
-
req.userRole = tokenUser.role;
|
|
172
|
-
|
|
173
|
-
// Validate token scope for API token requests
|
|
174
|
-
if (tokenUser.tokenScope) {
|
|
175
|
-
const requestMethod = req.method;
|
|
176
|
-
if (!canAccessMethod(tokenUser.tokenScope, requestMethod)) {
|
|
177
|
-
return res.status(403).json(createErrorResponse(403, ErrorCodes.TOKEN_SCOPE_INSUFFICIENT, {
|
|
178
|
-
message: `Token scope '${tokenUser.tokenScope}' does not allow ${requestMethod} requests`,
|
|
179
|
-
tokenScope: tokenUser.tokenScope,
|
|
180
|
-
requestedMethod: requestMethod,
|
|
181
|
-
hint: 'Use a token with write scope for write operations'
|
|
182
|
-
}));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
153
|
+
req.user = user;
|
|
154
|
+
req.userRole = tokenUser.role;
|
|
155
|
+
return user;
|
|
156
|
+
}
|
|
185
157
|
|
|
186
|
-
|
|
187
|
-
|
|
158
|
+
function hasAppAccess(role, allowedApps) {
|
|
159
|
+
if (role === "SuperAdmin") return true;
|
|
160
|
+
return Array.isArray(allowedApps)
|
|
161
|
+
&& allowedApps.length > 0
|
|
162
|
+
&& allowedApps.some((app) => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
|
|
163
|
+
}
|
|
188
164
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
return res.status(401).json(createErrorResponse(401, errorCode));
|
|
165
|
+
function destroySessionCookies(req, res) {
|
|
166
|
+
req.session?.destroy?.(() => { });
|
|
167
|
+
clearSessionCookies(res);
|
|
168
|
+
}
|
|
195
169
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
170
|
+
function respondSessionFailure(req, res, { prefersJson, code, errorCode, error, message, page, pagename = "Login" }) {
|
|
171
|
+
destroySessionCookies(req, res);
|
|
172
|
+
if (prefersJson) {
|
|
173
|
+
return res.status(code).json(createErrorResponse(code, errorCode));
|
|
200
174
|
}
|
|
175
|
+
return renderError(res, req, {
|
|
176
|
+
code,
|
|
177
|
+
error,
|
|
178
|
+
message,
|
|
179
|
+
pagename,
|
|
180
|
+
page,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
201
183
|
|
|
202
|
-
|
|
184
|
+
async function validateCookieSession(req, res, next, { prefersJson }) {
|
|
203
185
|
if (!req.session.user) {
|
|
204
186
|
if (IS_DEV) {
|
|
205
|
-
|
|
206
|
-
|
|
187
|
+
logAuth(`User not authenticated`);
|
|
188
|
+
logAuth(`req.session.user: %O`, req.session.user);
|
|
207
189
|
}
|
|
208
|
-
if (
|
|
190
|
+
if (prefersJson) {
|
|
209
191
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
210
192
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
pagename: "Login",
|
|
216
|
-
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
193
|
+
|
|
194
|
+
const redirectParams = new URLSearchParams({
|
|
195
|
+
redirect: req.originalUrl,
|
|
196
|
+
reason: 'logged_out'
|
|
217
197
|
});
|
|
198
|
+
return res.redirect(302, `/mbkauthe/login?${redirectParams.toString()}`);
|
|
218
199
|
}
|
|
219
200
|
|
|
220
201
|
try {
|
|
221
|
-
const { sessionId
|
|
202
|
+
const { sessionId } = req.session.user;
|
|
222
203
|
|
|
223
|
-
// Defensive checks for sessionId and allowedApps
|
|
224
204
|
if (!sessionId || !isUuid(sessionId)) {
|
|
225
205
|
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
226
|
-
req
|
|
227
|
-
|
|
228
|
-
if (isJsonRequest(req)) {
|
|
229
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
230
|
-
}
|
|
231
|
-
return renderError(res, req, {
|
|
206
|
+
return respondSessionFailure(req, res, {
|
|
207
|
+
prefersJson,
|
|
232
208
|
code: 401,
|
|
209
|
+
errorCode: ErrorCodes.SESSION_EXPIRED,
|
|
233
210
|
error: "Session Expired",
|
|
234
211
|
message: "Your Session Has Expired. Please Log In Again.",
|
|
235
|
-
pagename: "Login",
|
|
236
212
|
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
237
213
|
});
|
|
238
214
|
}
|
|
239
215
|
|
|
240
|
-
|
|
241
|
-
|
|
216
|
+
const sessionRow = await authRepo.getSessionAuthData(
|
|
217
|
+
sessionId,
|
|
218
|
+
prefersJson ? 'validate-app-session-for-api' : 'validate-app-session'
|
|
219
|
+
);
|
|
242
220
|
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
req
|
|
246
|
-
|
|
247
|
-
if (isJsonRequest(req)) {
|
|
248
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
249
|
-
}
|
|
250
|
-
return renderError(res, req, {
|
|
221
|
+
if (!sessionRow) {
|
|
222
|
+
logAuth(`Session not found for user "${req.session.user.username}"`);
|
|
223
|
+
return respondSessionFailure(req, res, {
|
|
224
|
+
prefersJson,
|
|
251
225
|
code: 401,
|
|
226
|
+
errorCode: prefersJson ? ErrorCodes.SESSION_INVALID : ErrorCodes.SESSION_EXPIRED,
|
|
252
227
|
error: "Session Expired",
|
|
253
228
|
message: "Your Session Has Expired. Please Log In Again.",
|
|
254
|
-
pagename: "Login",
|
|
255
229
|
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
256
230
|
});
|
|
257
231
|
}
|
|
258
232
|
|
|
259
|
-
const sessionRow = result.rows[0];
|
|
260
|
-
|
|
261
|
-
// Check expired
|
|
262
233
|
if (sessionRow.expires_at) {
|
|
263
234
|
const expiresMs = sessionRow.expires_at instanceof Date
|
|
264
235
|
? sessionRow.expires_at.getTime()
|
|
265
236
|
: Date.parse(sessionRow.expires_at);
|
|
237
|
+
|
|
266
238
|
if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
239
|
+
logAuth(`Session invalidated (expired) for user "${sessionRow.UserName || req.session.user.username}"`);
|
|
240
|
+
return respondSessionFailure(req, res, {
|
|
241
|
+
prefersJson,
|
|
242
|
+
code: 401,
|
|
243
|
+
errorCode: ErrorCodes.SESSION_EXPIRED,
|
|
244
|
+
error: "Session Expired",
|
|
245
|
+
message: "Your Session Has Expired. Please Log In Again.",
|
|
246
|
+
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
247
|
+
});
|
|
273
248
|
}
|
|
274
|
-
return renderError(res, req, {
|
|
275
|
-
code: 401,
|
|
276
|
-
error: "Session Expired",
|
|
277
|
-
message: "Your Session Has Expired. Please Log In Again.",
|
|
278
|
-
pagename: "Login",
|
|
279
|
-
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
249
|
}
|
|
283
250
|
|
|
284
|
-
|
|
285
251
|
if (!sessionRow.Active) {
|
|
286
|
-
|
|
287
|
-
req
|
|
288
|
-
|
|
289
|
-
if (isJsonRequest(req)) {
|
|
290
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
291
|
-
}
|
|
292
|
-
return renderError(res, req, {
|
|
252
|
+
logAuth(`Account is inactive for user "${sessionRow.UserName || req.session.user.username}"`);
|
|
253
|
+
return respondSessionFailure(req, res, {
|
|
254
|
+
prefersJson,
|
|
293
255
|
code: 401,
|
|
256
|
+
errorCode: ErrorCodes.ACCOUNT_INACTIVE,
|
|
294
257
|
error: "Account Inactive",
|
|
295
258
|
message: "Your Account Is Inactive. Please Contact Support.",
|
|
296
259
|
pagename: "Support",
|
|
@@ -298,106 +261,106 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
298
261
|
});
|
|
299
262
|
}
|
|
300
263
|
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
code: 401,
|
|
313
|
-
error: "Unauthorized",
|
|
314
|
-
message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"`,
|
|
315
|
-
pagename: "Home",
|
|
316
|
-
page: `/${mbkautheVar.loginRedirectURL}`
|
|
317
|
-
});
|
|
318
|
-
}
|
|
264
|
+
if (!hasAppAccess(sessionRow.Role, sessionRow.AllowedApps)) {
|
|
265
|
+
console.warn(`[mbkauthe] User "${sessionRow.UserName || req.session.user.username}" is not authorized to use the application "${mbkautheVar.APP_NAME}"`);
|
|
266
|
+
return respondSessionFailure(req, res, {
|
|
267
|
+
prefersJson,
|
|
268
|
+
code: 401,
|
|
269
|
+
errorCode: ErrorCodes.APP_NOT_AUTHORIZED,
|
|
270
|
+
error: "Unauthorized",
|
|
271
|
+
message: `You Are Not Authorized To Use The Application "${mbkautheVar.APP_NAME}"`,
|
|
272
|
+
pagename: "Home",
|
|
273
|
+
page: `/${mbkautheVar.loginRedirectURL}`
|
|
274
|
+
});
|
|
319
275
|
}
|
|
320
276
|
|
|
321
|
-
// Store user role in request for checkRolePermission to use
|
|
322
277
|
req.userRole = sessionRow.Role;
|
|
323
|
-
|
|
324
|
-
next();
|
|
278
|
+
return next();
|
|
325
279
|
} catch (err) {
|
|
326
280
|
console.error(`[mbkauthe] Session validation error:`, err);
|
|
327
|
-
res.status(500).json(
|
|
281
|
+
return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
|
|
328
282
|
}
|
|
329
283
|
}
|
|
330
284
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
285
|
+
async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
286
|
+
if (req.headers.authorization) {
|
|
287
|
+
if (strictTokenValidation) {
|
|
288
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.INVALID_AUTH_TOKEN, {
|
|
289
|
+
message: 'Token-based authentication not allowed for this endpoint',
|
|
290
|
+
hint: 'Use session-based authentication (cookies) instead'
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
339
293
|
|
|
340
|
-
|
|
341
|
-
|
|
294
|
+
try {
|
|
295
|
+
const tokenUser = await validateTokenAuthentication(req);
|
|
342
296
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
clearSessionCookies(res);
|
|
348
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
349
|
-
}
|
|
297
|
+
if (tokenUser && !tokenUser.error) {
|
|
298
|
+
if (!tokenUser.active) {
|
|
299
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
300
|
+
}
|
|
350
301
|
|
|
351
|
-
|
|
352
|
-
|
|
302
|
+
if (tokenUser.role !== "SuperAdmin") {
|
|
303
|
+
const allowedApps = tokenUser.allowedApps;
|
|
304
|
+
const userAllowedApps = tokenUser.userAllowedApps;
|
|
353
305
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
|
|
358
|
-
}
|
|
306
|
+
if (!Array.isArray(allowedApps) || allowedApps.length === 0) {
|
|
307
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
308
|
+
}
|
|
359
309
|
|
|
360
|
-
|
|
310
|
+
const hasWildcard = allowedApps.includes('*');
|
|
311
|
+
const hasSpecificApp = allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
|
|
361
312
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
313
|
+
if (hasWildcard) {
|
|
314
|
+
const userHasApp = Array.isArray(userAllowedApps)
|
|
315
|
+
&& userAllowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
|
|
316
|
+
if (!userHasApp) {
|
|
317
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
318
|
+
}
|
|
319
|
+
} else if (!hasSpecificApp) {
|
|
320
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
373
323
|
|
|
324
|
+
attachApiTokenUser(req, res, tokenUser);
|
|
374
325
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
326
|
+
if (tokenUser.tokenScope) {
|
|
327
|
+
const requestMethod = req.method;
|
|
328
|
+
if (!canAccessMethod(tokenUser.tokenScope, requestMethod)) {
|
|
329
|
+
return res.status(403).json(createErrorResponse(403, ErrorCodes.TOKEN_SCOPE_INSUFFICIENT, {
|
|
330
|
+
message: `Token scope '${tokenUser.tokenScope}' does not allow ${requestMethod} requests`,
|
|
331
|
+
tokenScope: tokenUser.tokenScope,
|
|
332
|
+
requestedMethod: requestMethod,
|
|
333
|
+
hint: 'Use a token with write scope for write operations'
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
381
337
|
|
|
382
|
-
|
|
383
|
-
// If allowedApps is not provided or not an array, treat as no access
|
|
384
|
-
const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
|
|
385
|
-
if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
386
|
-
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
387
|
-
req.session.destroy();
|
|
388
|
-
clearSessionCookies(res);
|
|
389
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
338
|
+
return next();
|
|
390
339
|
}
|
|
340
|
+
|
|
341
|
+
let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
|
|
342
|
+
if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
|
|
343
|
+
errorCode = ErrorCodes.API_TOKEN_EXPIRED;
|
|
344
|
+
}
|
|
345
|
+
return res.status(401).json(createErrorResponse(401, errorCode));
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.error(`[mbkauthe] Token validation error:`, err);
|
|
348
|
+
return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
|
|
391
349
|
}
|
|
350
|
+
}
|
|
392
351
|
|
|
393
|
-
|
|
394
|
-
|
|
352
|
+
return validateCookieSession(req, res, next, { prefersJson: isJsonRequest(req) });
|
|
353
|
+
}
|
|
395
354
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
355
|
+
/**
|
|
356
|
+
* API-friendly session validation middleware
|
|
357
|
+
* Returns JSON error responses instead of rendering pages
|
|
358
|
+
*/
|
|
359
|
+
async function validateApiSession(req, res, next) {
|
|
360
|
+
if (req.headers.authorization) {
|
|
361
|
+
return validateSession(req, res, next);
|
|
400
362
|
}
|
|
363
|
+
return validateCookieSession(req, res, next, { prefersJson: true });
|
|
401
364
|
}
|
|
402
365
|
|
|
403
366
|
/**
|
|
@@ -411,7 +374,7 @@ async function validateApiSession(req, res, next) {
|
|
|
411
374
|
async function reloadSessionUser(req, res) {
|
|
412
375
|
if (!req.session || !req.session.user || !req.session.user.id) return false;
|
|
413
376
|
try {
|
|
414
|
-
const {
|
|
377
|
+
const { sessionId: currentSessionId } = req.session.user;
|
|
415
378
|
|
|
416
379
|
if (!currentSessionId) {
|
|
417
380
|
req.session.destroy(() => { });
|
|
@@ -420,21 +383,15 @@ async function reloadSessionUser(req, res) {
|
|
|
420
383
|
}
|
|
421
384
|
|
|
422
385
|
const normalizedSessionId = String(currentSessionId);
|
|
423
|
-
const
|
|
424
|
-
FROM "Sessions" s
|
|
425
|
-
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
426
|
-
WHERE s.id = $1 LIMIT 1`;
|
|
427
|
-
const result = await dblogin.query({ name: 'reload-session-user', text: query, values: [normalizedSessionId] });
|
|
386
|
+
const row = await authRepo.getSessionWithUserForReload(normalizedSessionId, 'reload-session-user');
|
|
428
387
|
|
|
429
|
-
if (
|
|
388
|
+
if (!row) {
|
|
430
389
|
// Session not found — invalidate session
|
|
431
390
|
req.session.destroy(() => { });
|
|
432
391
|
clearSessionCookies(res);
|
|
433
392
|
return false;
|
|
434
393
|
}
|
|
435
394
|
|
|
436
|
-
const row = result.rows[0];
|
|
437
|
-
|
|
438
395
|
// Check expired
|
|
439
396
|
if (row.expires_at && new Date(row.expires_at) <= new Date()) {
|
|
440
397
|
req.session.destroy(() => { });
|
|
@@ -466,15 +423,10 @@ async function reloadSessionUser(req, res) {
|
|
|
466
423
|
req.session.user.allowedApps = row.AllowedApps;
|
|
467
424
|
|
|
468
425
|
// Obtain fullname from client cookie cache when present else DB
|
|
469
|
-
if (
|
|
426
|
+
if (typeof row.FullName === 'string' && row.FullName.trim() !== '') {
|
|
427
|
+
req.session.user.fullname = row.FullName;
|
|
428
|
+
} else if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
|
|
470
429
|
req.session.user.fullname = req.cookies.fullName;
|
|
471
|
-
} else {
|
|
472
|
-
try {
|
|
473
|
-
const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
|
|
474
|
-
if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
|
|
475
|
-
} catch (profileErr) {
|
|
476
|
-
console.error(`[mbkauthe] Error fetching fullname during reload:`, profileErr);
|
|
477
|
-
}
|
|
478
430
|
}
|
|
479
431
|
|
|
480
432
|
// Persist session changes
|
|
@@ -501,8 +453,10 @@ async function reloadSessionUser(req, res) {
|
|
|
501
453
|
const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
502
454
|
return async (req, res, next) => {
|
|
503
455
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
456
|
+
const authUser = req.auth?.user || req.session?.user;
|
|
457
|
+
|
|
458
|
+
if (!authUser || !authUser.id) {
|
|
459
|
+
logAuth(`User not authenticated`);
|
|
506
460
|
if (isJsonRequest(req)) {
|
|
507
461
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
508
462
|
}
|
|
@@ -516,10 +470,10 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
516
470
|
}
|
|
517
471
|
|
|
518
472
|
// Use role from validateSession to avoid additional DB query
|
|
519
|
-
const userRole = req.userRole;
|
|
473
|
+
const userRole = req.userRole || authUser.role;
|
|
520
474
|
|
|
521
475
|
// SuperAdmin bypasses all role checks
|
|
522
|
-
if(
|
|
476
|
+
if(authUser.role === "SuperAdmin" || userRole === "SuperAdmin") {
|
|
523
477
|
return next();
|
|
524
478
|
}
|
|
525
479
|
|
|
@@ -579,10 +533,10 @@ const authenticate = (authentication) => {
|
|
|
579
533
|
return (req, res, next) => {
|
|
580
534
|
const token = extractAuthorizationToken(req.headers?.authorization ?? req.headers?.["authorization"]);
|
|
581
535
|
if (timingSafeTokenMatch(token, authentication)) {
|
|
582
|
-
|
|
536
|
+
logAuth(`Authentication successful`);
|
|
583
537
|
next();
|
|
584
538
|
} else {
|
|
585
|
-
|
|
539
|
+
logAuth(`Authentication failed`);
|
|
586
540
|
res.status(401).send("Unauthorized");
|
|
587
541
|
}
|
|
588
542
|
};
|