mbkauthe 4.9.0 → 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/docs/api.md +29 -178
- package/docs/db.md +1 -1
- package/docs/db.sql +305 -253
- package/index.js +5 -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 +57 -16
- package/lib/db/BaseRepository.js +9 -1
- package/lib/db/dialects/postgres.js +1 -1
- package/lib/main.js +5 -5
- package/lib/middleware/auth.js +201 -218
- package/lib/middleware/index.js +13 -14
- package/lib/middleware/scopeValidator.js +8 -3
- package/lib/pool.js +5 -6
- package/lib/routes/auth.js +42 -47
- package/lib/routes/dbLogs.js +247 -29
- package/lib/routes/misc.js +6 -4
- package/lib/routes/oauth.js +19 -23
- 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 +1 -1
- package/test.spec.js +515 -48
- package/views/pages/dbLogs.handlebars +618 -420
package/lib/middleware/auth.js
CHANGED
|
@@ -1,17 +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
9
|
import { AuthRepository } from "../db/AuthRepository.js";
|
|
10
|
+
import { createLogger } from "../utils/logger.js";
|
|
10
11
|
|
|
11
12
|
const IS_DEV = process.env.env === 'dev' || process.env.test === 'dev' || process.env.NODE_ENV === 'development';
|
|
12
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;
|
|
13
14
|
const isUuid = (val) => typeof val === 'string' && UUID_RE.test(val);
|
|
15
|
+
const MAX_API_TOKEN_LENGTH = 4096;
|
|
16
|
+
const API_TOKEN_SESSION_RESTORE = Symbol('mbkauthe.apiTokenSessionRestore');
|
|
14
17
|
const authRepo = new AuthRepository({ db: dblogin });
|
|
18
|
+
const logAuth = createLogger("auth");
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Decide if the incoming request should return JSON errors instead of HTML.
|
|
@@ -57,6 +61,8 @@ async function validateTokenAuthentication(req) {
|
|
|
57
61
|
|
|
58
62
|
// 1. Check for API Token (mbk_)
|
|
59
63
|
if (token.startsWith('mbk_')) {
|
|
64
|
+
if (token.length > MAX_API_TOKEN_LENGTH) return { error: 'INVALID_TOKEN' };
|
|
65
|
+
|
|
60
66
|
const tokenHash = hashApiToken(token);
|
|
61
67
|
const row = await authRepo.getApiTokenByHash(tokenHash);
|
|
62
68
|
|
|
@@ -74,7 +80,7 @@ async function validateTokenAuthentication(req) {
|
|
|
74
80
|
allowedApps = tokenAllowedApps;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
// Update usage
|
|
83
|
+
// Update usage opportunistically, but not on every request.
|
|
78
84
|
authRepo.updateApiTokenLastUsed(row.id).catch(e => console.error(`[mbkauthe] Failed to update token usage:`, e));
|
|
79
85
|
|
|
80
86
|
return {
|
|
@@ -93,104 +99,98 @@ async function validateTokenAuthentication(req) {
|
|
|
93
99
|
return null;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const tokenUser = await validateTokenAuthentication(req);
|
|
109
|
-
|
|
110
|
-
if (tokenUser && !tokenUser.error) {
|
|
111
|
-
if (!tokenUser.active) {
|
|
112
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// SuperAdmin bypasses app permission checks
|
|
116
|
-
if (tokenUser.role !== "SuperAdmin") {
|
|
117
|
-
// API tokens must respect their app restrictions for non-SuperAdmin users
|
|
118
|
-
const allowedApps = tokenUser.allowedApps;
|
|
119
|
-
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
|
+
};
|
|
120
112
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
req.auth = {
|
|
114
|
+
type: 'api-token',
|
|
115
|
+
user,
|
|
116
|
+
tokenScope: user.tokenScope,
|
|
117
|
+
allowedApps: user.allowedApps,
|
|
118
|
+
};
|
|
126
119
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
});
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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;
|
|
140
145
|
}
|
|
146
|
+
delete req.session[API_TOKEN_SESSION_RESTORE];
|
|
141
147
|
}
|
|
148
|
+
return originalEnd.apply(this, args);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
fullname: tokenUser.fullname,
|
|
148
|
-
role: tokenUser.role,
|
|
149
|
-
sessionId: tokenUser.sessionId,
|
|
150
|
-
allowedApps: tokenUser.allowedApps,
|
|
151
|
-
tokenScope: tokenUser.tokenScope || null, // Add scope for token-based auth
|
|
152
|
-
};
|
|
153
|
-
req.userRole = tokenUser.role;
|
|
154
|
-
|
|
155
|
-
// Validate token scope for API token requests
|
|
156
|
-
if (tokenUser.tokenScope) {
|
|
157
|
-
const requestMethod = req.method;
|
|
158
|
-
if (!canAccessMethod(tokenUser.tokenScope, requestMethod)) {
|
|
159
|
-
return res.status(403).json(createErrorResponse(403, ErrorCodes.TOKEN_SCOPE_INSUFFICIENT, {
|
|
160
|
-
message: `Token scope '${tokenUser.tokenScope}' does not allow ${requestMethod} requests`,
|
|
161
|
-
tokenScope: tokenUser.tokenScope,
|
|
162
|
-
requestedMethod: requestMethod,
|
|
163
|
-
hint: 'Use a token with write scope for write operations'
|
|
164
|
-
}));
|
|
165
|
-
}
|
|
166
|
-
}
|
|
153
|
+
req.user = user;
|
|
154
|
+
req.userRole = tokenUser.role;
|
|
155
|
+
return user;
|
|
156
|
+
}
|
|
167
157
|
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|
|
170
164
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
return res.status(401).json(createErrorResponse(401, errorCode));
|
|
165
|
+
function destroySessionCookies(req, res) {
|
|
166
|
+
req.session?.destroy?.(() => { });
|
|
167
|
+
clearSessionCookies(res);
|
|
168
|
+
}
|
|
177
169
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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));
|
|
182
174
|
}
|
|
175
|
+
return renderError(res, req, {
|
|
176
|
+
code,
|
|
177
|
+
error,
|
|
178
|
+
message,
|
|
179
|
+
pagename,
|
|
180
|
+
page,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
async function validateCookieSession(req, res, next, { prefersJson }) {
|
|
185
185
|
if (!req.session.user) {
|
|
186
186
|
if (IS_DEV) {
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
logAuth(`User not authenticated`);
|
|
188
|
+
logAuth(`req.session.user: %O`, req.session.user);
|
|
189
189
|
}
|
|
190
|
-
if (
|
|
190
|
+
if (prefersJson) {
|
|
191
191
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
192
192
|
}
|
|
193
|
-
|
|
193
|
+
|
|
194
194
|
const redirectParams = new URLSearchParams({
|
|
195
195
|
redirect: req.originalUrl,
|
|
196
196
|
reason: 'logged_out'
|
|
@@ -199,77 +199,61 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
try {
|
|
202
|
-
const { sessionId
|
|
202
|
+
const { sessionId } = req.session.user;
|
|
203
203
|
|
|
204
|
-
// Defensive checks for sessionId and allowedApps
|
|
205
204
|
if (!sessionId || !isUuid(sessionId)) {
|
|
206
205
|
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
207
|
-
req
|
|
208
|
-
|
|
209
|
-
if (isJsonRequest(req)) {
|
|
210
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
211
|
-
}
|
|
212
|
-
return renderError(res, req, {
|
|
206
|
+
return respondSessionFailure(req, res, {
|
|
207
|
+
prefersJson,
|
|
213
208
|
code: 401,
|
|
209
|
+
errorCode: ErrorCodes.SESSION_EXPIRED,
|
|
214
210
|
error: "Session Expired",
|
|
215
211
|
message: "Your Session Has Expired. Please Log In Again.",
|
|
216
|
-
pagename: "Login",
|
|
217
212
|
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
218
213
|
});
|
|
219
214
|
}
|
|
220
215
|
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
const sessionRow = await authRepo.getSessionAuthData(
|
|
217
|
+
sessionId,
|
|
218
|
+
prefersJson ? 'validate-app-session-for-api' : 'validate-app-session'
|
|
219
|
+
);
|
|
223
220
|
|
|
224
221
|
if (!sessionRow) {
|
|
225
|
-
|
|
226
|
-
req
|
|
227
|
-
|
|
228
|
-
if (isJsonRequest(req)) {
|
|
229
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
230
|
-
}
|
|
231
|
-
return renderError(res, req, {
|
|
222
|
+
logAuth(`Session not found for user "${req.session.user.username}"`);
|
|
223
|
+
return respondSessionFailure(req, res, {
|
|
224
|
+
prefersJson,
|
|
232
225
|
code: 401,
|
|
226
|
+
errorCode: prefersJson ? ErrorCodes.SESSION_INVALID : ErrorCodes.SESSION_EXPIRED,
|
|
233
227
|
error: "Session Expired",
|
|
234
228
|
message: "Your Session Has Expired. Please Log In Again.",
|
|
235
|
-
pagename: "Login",
|
|
236
229
|
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
237
230
|
});
|
|
238
231
|
}
|
|
239
232
|
|
|
240
|
-
// Check expired
|
|
241
233
|
if (sessionRow.expires_at) {
|
|
242
234
|
const expiresMs = sessionRow.expires_at instanceof Date
|
|
243
235
|
? sessionRow.expires_at.getTime()
|
|
244
236
|
: Date.parse(sessionRow.expires_at);
|
|
237
|
+
|
|
245
238
|
if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
});
|
|
252
248
|
}
|
|
253
|
-
return renderError(res, req, {
|
|
254
|
-
code: 401,
|
|
255
|
-
error: "Session Expired",
|
|
256
|
-
message: "Your Session Has Expired. Please Log In Again.",
|
|
257
|
-
pagename: "Login",
|
|
258
|
-
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
259
|
-
});
|
|
260
249
|
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
250
|
|
|
264
251
|
if (!sessionRow.Active) {
|
|
265
|
-
|
|
266
|
-
req
|
|
267
|
-
|
|
268
|
-
if (isJsonRequest(req)) {
|
|
269
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
270
|
-
}
|
|
271
|
-
return renderError(res, req, {
|
|
252
|
+
logAuth(`Account is inactive for user "${sessionRow.UserName || req.session.user.username}"`);
|
|
253
|
+
return respondSessionFailure(req, res, {
|
|
254
|
+
prefersJson,
|
|
272
255
|
code: 401,
|
|
256
|
+
errorCode: ErrorCodes.ACCOUNT_INACTIVE,
|
|
273
257
|
error: "Account Inactive",
|
|
274
258
|
message: "Your Account Is Inactive. Please Contact Support.",
|
|
275
259
|
pagename: "Support",
|
|
@@ -277,104 +261,106 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
277
261
|
});
|
|
278
262
|
}
|
|
279
263
|
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
code: 401,
|
|
292
|
-
error: "Unauthorized",
|
|
293
|
-
message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"`,
|
|
294
|
-
pagename: "Home",
|
|
295
|
-
page: `/${mbkautheVar.loginRedirectURL}`
|
|
296
|
-
});
|
|
297
|
-
}
|
|
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
|
+
});
|
|
298
275
|
}
|
|
299
276
|
|
|
300
|
-
// Store user role in request for checkRolePermission to use
|
|
301
277
|
req.userRole = sessionRow.Role;
|
|
302
|
-
|
|
303
|
-
next();
|
|
278
|
+
return next();
|
|
304
279
|
} catch (err) {
|
|
305
280
|
console.error(`[mbkauthe] Session validation error:`, err);
|
|
306
|
-
res.status(500).json(
|
|
281
|
+
return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
|
|
307
282
|
}
|
|
308
283
|
}
|
|
309
284
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}
|
|
318
293
|
|
|
319
|
-
|
|
320
|
-
|
|
294
|
+
try {
|
|
295
|
+
const tokenUser = await validateTokenAuthentication(req);
|
|
321
296
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
clearSessionCookies(res);
|
|
327
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
328
|
-
}
|
|
297
|
+
if (tokenUser && !tokenUser.error) {
|
|
298
|
+
if (!tokenUser.active) {
|
|
299
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
300
|
+
}
|
|
329
301
|
|
|
330
|
-
|
|
331
|
-
|
|
302
|
+
if (tokenUser.role !== "SuperAdmin") {
|
|
303
|
+
const allowedApps = tokenUser.allowedApps;
|
|
304
|
+
const userAllowedApps = tokenUser.userAllowedApps;
|
|
332
305
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
|
|
337
|
-
}
|
|
306
|
+
if (!Array.isArray(allowedApps) || allowedApps.length === 0) {
|
|
307
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
308
|
+
}
|
|
338
309
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const expiresMs = sessionRow.expires_at instanceof Date
|
|
342
|
-
? sessionRow.expires_at.getTime()
|
|
343
|
-
: Date.parse(sessionRow.expires_at);
|
|
344
|
-
if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) {
|
|
345
|
-
req.session.destroy();
|
|
346
|
-
clearSessionCookies(res);
|
|
347
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
348
|
-
}
|
|
349
|
-
}
|
|
310
|
+
const hasWildcard = allowedApps.includes('*');
|
|
311
|
+
const hasSpecificApp = allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase());
|
|
350
312
|
|
|
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
|
+
}
|
|
351
323
|
|
|
352
|
-
|
|
353
|
-
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
354
|
-
req.session.destroy();
|
|
355
|
-
clearSessionCookies(res);
|
|
356
|
-
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
357
|
-
}
|
|
324
|
+
attachApiTokenUser(req, res, tokenUser);
|
|
358
325
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
}
|
|
337
|
+
|
|
338
|
+
return next();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
|
|
342
|
+
if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
|
|
343
|
+
errorCode = ErrorCodes.API_TOKEN_EXPIRED;
|
|
367
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));
|
|
368
349
|
}
|
|
350
|
+
}
|
|
369
351
|
|
|
370
|
-
|
|
371
|
-
|
|
352
|
+
return validateCookieSession(req, res, next, { prefersJson: isJsonRequest(req) });
|
|
353
|
+
}
|
|
372
354
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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);
|
|
377
362
|
}
|
|
363
|
+
return validateCookieSession(req, res, next, { prefersJson: true });
|
|
378
364
|
}
|
|
379
365
|
|
|
380
366
|
/**
|
|
@@ -388,7 +374,7 @@ async function validateApiSession(req, res, next) {
|
|
|
388
374
|
async function reloadSessionUser(req, res) {
|
|
389
375
|
if (!req.session || !req.session.user || !req.session.user.id) return false;
|
|
390
376
|
try {
|
|
391
|
-
const {
|
|
377
|
+
const { sessionId: currentSessionId } = req.session.user;
|
|
392
378
|
|
|
393
379
|
if (!currentSessionId) {
|
|
394
380
|
req.session.destroy(() => { });
|
|
@@ -437,15 +423,10 @@ async function reloadSessionUser(req, res) {
|
|
|
437
423
|
req.session.user.allowedApps = row.AllowedApps;
|
|
438
424
|
|
|
439
425
|
// Obtain fullname from client cookie cache when present else DB
|
|
440
|
-
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') {
|
|
441
429
|
req.session.user.fullname = req.cookies.fullName;
|
|
442
|
-
} else {
|
|
443
|
-
try {
|
|
444
|
-
const prof = await authRepo.getUserFullNameByUsername(row.UserName, 'reload-get-fullname');
|
|
445
|
-
if (prof && prof.FullName) req.session.user.fullname = prof.FullName;
|
|
446
|
-
} catch (profileErr) {
|
|
447
|
-
console.error(`[mbkauthe] Error fetching fullname during reload:`, profileErr);
|
|
448
|
-
}
|
|
449
430
|
}
|
|
450
431
|
|
|
451
432
|
// Persist session changes
|
|
@@ -472,8 +453,10 @@ async function reloadSessionUser(req, res) {
|
|
|
472
453
|
const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
473
454
|
return async (req, res, next) => {
|
|
474
455
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
456
|
+
const authUser = req.auth?.user || req.session?.user;
|
|
457
|
+
|
|
458
|
+
if (!authUser || !authUser.id) {
|
|
459
|
+
logAuth(`User not authenticated`);
|
|
477
460
|
if (isJsonRequest(req)) {
|
|
478
461
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
479
462
|
}
|
|
@@ -487,10 +470,10 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
487
470
|
}
|
|
488
471
|
|
|
489
472
|
// Use role from validateSession to avoid additional DB query
|
|
490
|
-
const userRole = req.userRole;
|
|
473
|
+
const userRole = req.userRole || authUser.role;
|
|
491
474
|
|
|
492
475
|
// SuperAdmin bypasses all role checks
|
|
493
|
-
if(
|
|
476
|
+
if(authUser.role === "SuperAdmin" || userRole === "SuperAdmin") {
|
|
494
477
|
return next();
|
|
495
478
|
}
|
|
496
479
|
|
|
@@ -550,10 +533,10 @@ const authenticate = (authentication) => {
|
|
|
550
533
|
return (req, res, next) => {
|
|
551
534
|
const token = extractAuthorizationToken(req.headers?.authorization ?? req.headers?.["authorization"]);
|
|
552
535
|
if (timingSafeTokenMatch(token, authentication)) {
|
|
553
|
-
|
|
536
|
+
logAuth(`Authentication successful`);
|
|
554
537
|
next();
|
|
555
538
|
} else {
|
|
556
|
-
|
|
539
|
+
logAuth(`Authentication failed`);
|
|
557
540
|
res.status(401).send("Unauthorized");
|
|
558
541
|
}
|
|
559
542
|
};
|
package/lib/middleware/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export const sessionConfig = {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const authRepo = new AuthRepository({ db: dblogin });
|
|
38
|
+
const hasAuthorizationHeader = (req) => typeof req.headers?.authorization === 'string' && req.headers.authorization.trim().length > 0;
|
|
38
39
|
|
|
39
40
|
// CORS middleware
|
|
40
41
|
export function corsMiddleware(req, res, next) {
|
|
@@ -60,6 +61,10 @@ export function corsMiddleware(req, res, next) {
|
|
|
60
61
|
|
|
61
62
|
// Session restoration middleware
|
|
62
63
|
export async function sessionRestorationMiddleware(req, res, next) {
|
|
64
|
+
if (hasAuthorizationHeader(req)) {
|
|
65
|
+
return next();
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
// Only restore session if not already present and sessionId cookie exists
|
|
64
69
|
if (!req.session.user && req.cookies.sessionId) {
|
|
65
70
|
// Decrypt the sessionId from cookie
|
|
@@ -83,33 +88,23 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
83
88
|
const row = await authRepo.getSessionWithUserById(sessionId, 'restore-user-session');
|
|
84
89
|
|
|
85
90
|
if (row) {
|
|
86
|
-
|
|
87
91
|
// Reject expired sessions or inactive users
|
|
88
92
|
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
89
93
|
// leave cookies cleared and don't restore session
|
|
90
94
|
} else {
|
|
91
95
|
const normalizedSessionId = String(sessionId);
|
|
92
96
|
req.session.user = {
|
|
93
|
-
id: row.
|
|
97
|
+
id: row.uid,
|
|
94
98
|
username: row.UserName,
|
|
95
99
|
role: row.Role,
|
|
96
100
|
sessionId: normalizedSessionId,
|
|
97
101
|
allowedApps: row.AllowedApps,
|
|
98
102
|
};
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
if (typeof row.FullName === 'string' && row.FullName.trim() !== '') {
|
|
105
|
+
req.session.user.fullname = row.FullName;
|
|
106
|
+
} else if (req.cookies && typeof req.cookies.fullName === 'string') {
|
|
102
107
|
req.session.user.fullname = req.cookies.fullName;
|
|
103
|
-
} else {
|
|
104
|
-
// Fallback: attempt to fetch FullName from Users to populate session
|
|
105
|
-
try {
|
|
106
|
-
const profileRes = await authRepo.getUserFullNameByUsername(row.UserName, 'restore-get-fullname');
|
|
107
|
-
if (profileRes && profileRes.FullName) {
|
|
108
|
-
req.session.user.fullname = profileRes.FullName;
|
|
109
|
-
}
|
|
110
|
-
} catch (profileErr) {
|
|
111
|
-
console.error(`[mbkauthe] Error fetching FullName during session restore:`, profileErr);
|
|
112
|
-
}
|
|
113
108
|
}
|
|
114
109
|
}
|
|
115
110
|
}
|
|
@@ -122,6 +117,10 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
122
117
|
|
|
123
118
|
// Session cookie sync middleware
|
|
124
119
|
export function sessionCookieSyncMiddleware(req, res, next) {
|
|
120
|
+
if (hasAuthorizationHeader(req) || req.auth?.type === 'api-token') {
|
|
121
|
+
return next();
|
|
122
|
+
}
|
|
123
|
+
|
|
125
124
|
if (req.session && req.session.user) {
|
|
126
125
|
// Decrypt existing cookie to compare with session
|
|
127
126
|
const currentDecryptedId = decryptSessionId(req.cookies.sessionId);
|