mbkauthe 3.4.0 → 4.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 +54 -304
- package/docs/api.md +71 -3
- package/docs/db.md +26 -20
- package/docs/db.sql +116 -0
- package/docs/env.md +10 -0
- package/index.d.ts +10 -3
- package/index.js +4 -1
- package/lib/config/cookies.js +7 -0
- package/lib/config/index.js +20 -4
- package/lib/config/security.js +1 -1
- package/lib/middleware/auth.js +202 -15
- package/lib/middleware/index.js +45 -15
- package/lib/routes/auth.js +74 -35
- package/lib/routes/misc.js +54 -6
- package/package.json +1 -1
- package/test.spec.js +15 -0
- package/views/loginmbkauthe.handlebars +0 -2
package/docs/db.sql
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
|
|
2
|
+
-- GitHub users table
|
|
3
|
+
CREATE TABLE user_github (
|
|
4
|
+
id SERIAL PRIMARY KEY,
|
|
5
|
+
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
6
|
+
github_id VARCHAR(255) UNIQUE,
|
|
7
|
+
github_username VARCHAR(255),
|
|
8
|
+
access_token TEXT,
|
|
9
|
+
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
10
|
+
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
-- Add indexes for performance optimization
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_user_github_github_id ON user_github (github_id);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_user_github_user_name ON user_github (user_name);
|
|
16
|
+
|
|
17
|
+
-- Google users table
|
|
18
|
+
CREATE TABLE user_google (
|
|
19
|
+
id SERIAL PRIMARY KEY,
|
|
20
|
+
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
21
|
+
google_id VARCHAR(255) UNIQUE,
|
|
22
|
+
google_email VARCHAR(255),
|
|
23
|
+
access_token TEXT,
|
|
24
|
+
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
25
|
+
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- Add indexes for performance optimization
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_user_google_google_id ON user_google (google_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
|
|
36
|
+
|
|
37
|
+
CREATE TABLE "Users" (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
"UserName" VARCHAR(50) NOT NULL UNIQUE,
|
|
40
|
+
"Password" VARCHAR(61), -- For raw passwords (when EncPass=false)
|
|
41
|
+
"PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
|
|
42
|
+
"Role" role DEFAULT 'NormalUser' NOT NULL,
|
|
43
|
+
"Active" BOOLEAN DEFAULT FALSE,
|
|
44
|
+
"HaveMailAccount" BOOLEAN DEFAULT FALSE,
|
|
45
|
+
"AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
|
|
46
|
+
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
48
|
+
"last_login" TIMESTAMP WITH TIME ZONE
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- Add indexes for performance optimization
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" ("UserName");
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" ("Active");
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" ("Role");
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" (last_login);
|
|
56
|
+
|
|
57
|
+
-- Application Sessions table (stores multiple concurrent sessions per user)
|
|
58
|
+
-- Note: this is separate from the express-session store table named "session"
|
|
59
|
+
CREATE TABLE "Sessions" (
|
|
60
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- requires pgcrypto or uuid-ossp
|
|
61
|
+
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
62
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
63
|
+
expires_at TIMESTAMP WITH TIME ZONE,
|
|
64
|
+
meta JSONB
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
-- Indexes optimized by username instead of numeric user id
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_username ON "Sessions" ("UserName");
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_created ON "Sessions" ("UserName", created_at);
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
CREATE TABLE "session" (
|
|
73
|
+
sid VARCHAR(33) PRIMARY KEY NOT NULL,
|
|
74
|
+
sess JSONB NOT NULL,
|
|
75
|
+
expire TimeStamp WITH TIME ZONE Not Null,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- Add indexes for performance optimization
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_session_expire ON "session" ("expire");
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_session_user_id ON "session" ((sess->'user'->>'id'));
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
CREATE TABLE "TwoFA" (
|
|
85
|
+
"UserName" VARCHAR(50) primary key REFERENCES "Users"("UserName"),
|
|
86
|
+
"TwoFAStatus" boolean NOT NULL,
|
|
87
|
+
"TwoFASecret" TEXT
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- Add indexes for performance optimization
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_twofa_username ON "TwoFA" ("UserName");
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_twofa_username_status ON "TwoFA" ("UserName", "TwoFAStatus");
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
CREATE TABLE "TrustedDevices" (
|
|
97
|
+
"id" SERIAL PRIMARY KEY,
|
|
98
|
+
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
99
|
+
"DeviceToken" VARCHAR(64) UNIQUE NOT NULL,
|
|
100
|
+
"DeviceName" VARCHAR(255),
|
|
101
|
+
"UserAgent" TEXT,
|
|
102
|
+
"IpAddress" VARCHAR(45),
|
|
103
|
+
"CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
104
|
+
"ExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
105
|
+
"LastUsed" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
-- Add indexes for performance optimization
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_trusted_devices_token ON "TrustedDevices"("DeviceToken");
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_trusted_devices_username ON "TrustedDevices"("UserName");
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices"("ExpiresAt");
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
-- No Encrypted password for 'support' user
|
|
115
|
+
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "GuestRole")
|
|
116
|
+
VALUES ('support', '12345678', 'SuperAdmin', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
package/docs/env.md
CHANGED
|
@@ -42,6 +42,7 @@ This document describes the environment variables MBKAuth expects and keeps brie
|
|
|
42
42
|
- Description: PostgreSQL connection string for auth (must start with `postgresql://` or `postgres://`).
|
|
43
43
|
- Example: `"LOGIN_DB":"postgresql://user:pass@localhost:5432/mbkauth"`
|
|
44
44
|
- Required: Yes
|
|
45
|
+
- Create free postgres db: https://neon.com/
|
|
45
46
|
|
|
46
47
|
- MBKAUTH_TWO_FA_ENABLE
|
|
47
48
|
- Description: Enable Two-Factor Authentication.
|
|
@@ -67,6 +68,13 @@ This document describes the environment variables MBKAuth expects and keeps brie
|
|
|
67
68
|
- Example: `"DEVICE_TRUST_DURATION_DAYS":30`
|
|
68
69
|
- Required: No
|
|
69
70
|
|
|
71
|
+
- MAX_SESSIONS_PER_USER
|
|
72
|
+
- Description: Maximum number of concurrent application sessions allowed per user. When creating a new session that would exceed this number, the oldest session(s) for that user are pruned to make room for the new session.
|
|
73
|
+
- Default: `5`
|
|
74
|
+
- Example: `"MAX_SESSIONS_PER_USER": 10`
|
|
75
|
+
- Notes: Must be a positive integer. Validation is performed at startup by `lib/config/index.js`.
|
|
76
|
+
- Required: No
|
|
77
|
+
|
|
70
78
|
- loginRedirectURL
|
|
71
79
|
- Description: Post-login redirect path.
|
|
72
80
|
- Default: `/dashboard`
|
|
@@ -81,6 +89,8 @@ This document describes the environment variables MBKAuth expects and keeps brie
|
|
|
81
89
|
- GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
|
|
82
90
|
- Description: OAuth credentials (put in `mbkautheVar` preferred, or `mbkauthShared`).
|
|
83
91
|
- Required when provider enabled.
|
|
92
|
+
- Create Github OAuth App: https://github.com/settings/developers
|
|
93
|
+
- Create Google OAuth: https://console.cloud.google.com/
|
|
84
94
|
|
|
85
95
|
---
|
|
86
96
|
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ declare global {
|
|
|
12
12
|
user?: {
|
|
13
13
|
username: string;
|
|
14
14
|
role: 'SuperAdmin' | 'NormalUser' | 'Guest';
|
|
15
|
+
fullname?: string;
|
|
15
16
|
};
|
|
16
17
|
userRole?: 'SuperAdmin' | 'NormalUser' | 'Guest';
|
|
17
18
|
}
|
|
@@ -20,8 +21,9 @@ declare global {
|
|
|
20
21
|
user?: {
|
|
21
22
|
id: number;
|
|
22
23
|
username: string;
|
|
24
|
+
fullname?: string;
|
|
23
25
|
role: 'SuperAdmin' | 'NormalUser' | 'Guest';
|
|
24
|
-
sessionId
|
|
26
|
+
sessionId?: string;
|
|
25
27
|
allowedApps?: string[];
|
|
26
28
|
};
|
|
27
29
|
preAuthUser?: {
|
|
@@ -75,8 +77,9 @@ declare module 'mbkauthe' {
|
|
|
75
77
|
export interface SessionUser {
|
|
76
78
|
id: number;
|
|
77
79
|
username: string;
|
|
80
|
+
fullname?: string;
|
|
78
81
|
role: UserRole;
|
|
79
|
-
sessionId
|
|
82
|
+
sessionId?: string;
|
|
80
83
|
allowedApps?: string[];
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -98,7 +101,7 @@ declare module 'mbkauthe' {
|
|
|
98
101
|
Role: UserRole;
|
|
99
102
|
Active: boolean;
|
|
100
103
|
AllowedApps: string[];
|
|
101
|
-
|
|
104
|
+
|
|
102
105
|
created_at?: Date;
|
|
103
106
|
updated_at?: Date;
|
|
104
107
|
last_login?: Date;
|
|
@@ -209,6 +212,10 @@ declare module 'mbkauthe' {
|
|
|
209
212
|
|
|
210
213
|
export function authenticate(token: string): AuthMiddleware;
|
|
211
214
|
|
|
215
|
+
// Reload session user values from DB and refresh cookies.
|
|
216
|
+
// Returns true when session is refreshed and valid, false if session invalidated.
|
|
217
|
+
export function reloadSessionUser(req: Request, res: Response): Promise<boolean>;
|
|
218
|
+
|
|
212
219
|
// Utility Functions
|
|
213
220
|
export function renderError(
|
|
214
221
|
res: Response,
|
package/index.js
CHANGED
|
@@ -89,7 +89,10 @@ if (process.env.test !== "dev") {
|
|
|
89
89
|
await checkVersion();
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export {
|
|
92
|
+
export {
|
|
93
|
+
validateSession, validateApiSession, checkRolePermission,
|
|
94
|
+
validateSessionAndRole, authenticate, reloadSessionUser
|
|
95
|
+
} from "./lib/middleware/auth.js";
|
|
93
96
|
export { renderError } from "./lib/utils/response.js";
|
|
94
97
|
export { dblogin } from "./lib/database/pool.js";
|
|
95
98
|
export { ErrorCodes, ErrorMessages, getErrorByCode, createErrorResponse, logError } from "./lib/utils/errors.js";
|
package/lib/config/cookies.js
CHANGED
|
@@ -32,6 +32,12 @@ export const generateDeviceToken = () => {
|
|
|
32
32
|
return crypto.randomBytes(32).toString('hex');
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Hash a device token for safe storage in the database
|
|
36
|
+
export const hashDeviceToken = (token) => {
|
|
37
|
+
if (!token || typeof token !== 'string') return null;
|
|
38
|
+
return crypto.createHmac('sha256').update(token).digest('hex');
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
export const getDeviceTokenCookieOptions = () => ({
|
|
36
42
|
maxAge: DEVICE_TRUST_DURATION_MS,
|
|
37
43
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
@@ -46,6 +52,7 @@ export const clearSessionCookies = (res) => {
|
|
|
46
52
|
res.clearCookie("mbkauthe.sid", cachedClearCookieOptions);
|
|
47
53
|
res.clearCookie("sessionId", cachedClearCookieOptions);
|
|
48
54
|
res.clearCookie("username", cachedClearCookieOptions);
|
|
55
|
+
res.clearCookie("fullName", cachedClearCookieOptions);
|
|
49
56
|
res.clearCookie("device_token", cachedClearCookieOptions);
|
|
50
57
|
};
|
|
51
58
|
|
package/lib/config/index.js
CHANGED
|
@@ -62,9 +62,10 @@ function validateConfiguration() {
|
|
|
62
62
|
|
|
63
63
|
// Ensure specific keys are checked in mbkautheVar first, then mbkauthShared, then apply config defaults
|
|
64
64
|
const keysToCheck = [
|
|
65
|
-
"APP_NAME","DEVICE_TRUST_DURATION_DAYS","EncPass","Main_SECRET_TOKEN","SESSION_SECRET_KEY",
|
|
66
|
-
"LOGIN_DB","MBKAUTH_TWO_FA_ENABLE","COOKIE_EXPIRE_TIME","DOMAIN","loginRedirectURL",
|
|
67
|
-
"GITHUB_LOGIN_ENABLED","GITHUB_CLIENT_ID","GITHUB_CLIENT_SECRET","GOOGLE_LOGIN_ENABLED","GOOGLE_CLIENT_ID",
|
|
65
|
+
"APP_NAME","DEVICE_TRUST_DURATION_DAYS","EncPass","Main_SECRET_TOKEN","SESSION_SECRET_KEY",
|
|
66
|
+
"IS_DEPLOYED","LOGIN_DB","MBKAUTH_TWO_FA_ENABLE","COOKIE_EXPIRE_TIME","DOMAIN","loginRedirectURL",
|
|
67
|
+
"GITHUB_LOGIN_ENABLED","GITHUB_CLIENT_ID","GITHUB_CLIENT_SECRET","GOOGLE_LOGIN_ENABLED","GOOGLE_CLIENT_ID",
|
|
68
|
+
"GOOGLE_CLIENT_SECRET","MAX_SESSIONS_PER_USER"
|
|
68
69
|
];
|
|
69
70
|
|
|
70
71
|
const defaults = {
|
|
@@ -75,7 +76,8 @@ function validateConfiguration() {
|
|
|
75
76
|
COOKIE_EXPIRE_TIME: 2,
|
|
76
77
|
loginRedirectURL: '/dashboard',
|
|
77
78
|
GITHUB_LOGIN_ENABLED: 'false',
|
|
78
|
-
GOOGLE_LOGIN_ENABLED: 'false'
|
|
79
|
+
GOOGLE_LOGIN_ENABLED: 'false',
|
|
80
|
+
MAX_SESSIONS_PER_USER: 5
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
keysToCheck.forEach(key => {
|
|
@@ -183,6 +185,20 @@ function validateConfiguration() {
|
|
|
183
185
|
mbkautheVar.DEVICE_TRUST_DURATION_DAYS = 7;
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
// Validate MAX_SESSIONS_PER_USER if provided (must be positive integer)
|
|
189
|
+
if (mbkautheVar.MAX_SESSIONS_PER_USER !== undefined) {
|
|
190
|
+
const maxSessions = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
|
|
191
|
+
if (isNaN(maxSessions) || maxSessions <= 0) {
|
|
192
|
+
errors.push("mbkautheVar.MAX_SESSIONS_PER_USER must be a valid positive integer");
|
|
193
|
+
} else {
|
|
194
|
+
// Normalize to integer
|
|
195
|
+
mbkautheVar.MAX_SESSIONS_PER_USER = maxSessions;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Ensure default value is set
|
|
199
|
+
mbkautheVar.MAX_SESSIONS_PER_USER = 5;
|
|
200
|
+
}
|
|
201
|
+
|
|
186
202
|
// Validate LOGIN_DB connection string format
|
|
187
203
|
if (mbkautheVar.LOGIN_DB && !mbkautheVar.LOGIN_DB.startsWith('postgresql://') && !mbkautheVar.LOGIN_DB.startsWith('postgres://')) {
|
|
188
204
|
errors.push("mbkautheVar.LOGIN_DB must be a valid PostgreSQL connection string");
|
package/lib/config/security.js
CHANGED
package/lib/middleware/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dblogin } from "../database/pool.js";
|
|
2
2
|
import { mbkautheVar } from "../config/index.js";
|
|
3
3
|
import { renderError } from "../utils/response.js";
|
|
4
|
-
import { clearSessionCookies } from "../config/cookies.js";
|
|
4
|
+
import { clearSessionCookies, cachedCookieOptions } from "../config/cookies.js";
|
|
5
5
|
|
|
6
6
|
async function validateSession(req, res, next) {
|
|
7
7
|
if (!req.session.user) {
|
|
@@ -33,19 +33,18 @@ async function validateSession(req, res, next) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// Normalize sessionId
|
|
37
|
-
const normalizedSessionId = sessionId
|
|
36
|
+
// Normalize sessionId (DB id) for consistent comparison
|
|
37
|
+
const normalizedSessionId = sessionId;
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
const query = `SELECT
|
|
41
|
-
|
|
39
|
+
// Validate session by DB primary key id and join to user
|
|
40
|
+
const query = `SELECT s.id as sid, s.expires_at, u."Active", u."Role"
|
|
41
|
+
FROM "Sessions" s
|
|
42
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
43
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
44
|
+
const result = await dblogin.query({ name: 'validate-app-session', text: query, values: [normalizedSessionId] });
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (result.rows.length > 0 && !result.rows[0].SessionId) {
|
|
46
|
-
console.warn(`[mbkauthe] DB sessionId is null for user "${req.session.user.username}"`);
|
|
47
|
-
}
|
|
48
|
-
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
46
|
+
if (result.rows.length === 0) {
|
|
47
|
+
console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
|
|
49
48
|
req.session.destroy();
|
|
50
49
|
clearSessionCookies(res);
|
|
51
50
|
return renderError(res, {
|
|
@@ -57,7 +56,25 @@ async function validateSession(req, res, next) {
|
|
|
57
56
|
});
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
const sessionRow = result.rows[0];
|
|
60
|
+
|
|
61
|
+
// Check expired
|
|
62
|
+
if (sessionRow.expires_at && new Date(sessionRow.expires_at) <= new Date()) {
|
|
63
|
+
console.log(`[mbkauthe] Session invalidated (expired) for user "${req.session.user.username}"`);
|
|
64
|
+
// destroy and clear cookies
|
|
65
|
+
req.session.destroy();
|
|
66
|
+
clearSessionCookies(res);
|
|
67
|
+
return renderError(res, {
|
|
68
|
+
code: 401,
|
|
69
|
+
error: "Session Expired",
|
|
70
|
+
message: "Your Session Has Expired. Please Log In Again.",
|
|
71
|
+
pagename: "Login",
|
|
72
|
+
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if (!sessionRow.Active) {
|
|
61
78
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
62
79
|
req.session.destroy();
|
|
63
80
|
clearSessionCookies(res);
|
|
@@ -97,6 +114,177 @@ async function validateSession(req, res, next) {
|
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
/**
|
|
118
|
+
* API-friendly session validation middleware
|
|
119
|
+
* Returns JSON error responses instead of rendering pages
|
|
120
|
+
*/
|
|
121
|
+
async function validateApiSession(req, res, next) {
|
|
122
|
+
if (!req.session.user) {
|
|
123
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const { id, sessionId, role, allowedApps } = req.session.user;
|
|
128
|
+
|
|
129
|
+
// Defensive checks for sessionId and allowedApps
|
|
130
|
+
if (!sessionId) {
|
|
131
|
+
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
132
|
+
req.session.destroy();
|
|
133
|
+
clearSessionCookies(res);
|
|
134
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Normalize sessionId (DB id) for consistent comparison
|
|
138
|
+
const normalizedSessionId = sessionId;
|
|
139
|
+
|
|
140
|
+
// Validate session by DB primary key id and join to user
|
|
141
|
+
const query = `SELECT s.id as sid, s.expires_at, u."Active", u."Role"
|
|
142
|
+
FROM "Sessions" s
|
|
143
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
144
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
145
|
+
const result = await dblogin.query({ name: 'validate-app-session-for-api', text: query, values: [normalizedSessionId] });
|
|
146
|
+
|
|
147
|
+
if (result.rows.length === 0) {
|
|
148
|
+
req.session.destroy();
|
|
149
|
+
clearSessionCookies(res);
|
|
150
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sessionRow = result.rows[0];
|
|
154
|
+
|
|
155
|
+
// Check expired
|
|
156
|
+
if (sessionRow.expires_at && new Date(sessionRow.expires_at) <= new Date()) {
|
|
157
|
+
req.session.destroy();
|
|
158
|
+
clearSessionCookies(res);
|
|
159
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if (!result.rows[0].Active) {
|
|
164
|
+
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
165
|
+
req.session.destroy();
|
|
166
|
+
clearSessionCookies(res);
|
|
167
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (role !== "SuperAdmin") {
|
|
171
|
+
// If allowedApps is not provided or not an array, treat as no access
|
|
172
|
+
const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
|
|
173
|
+
if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
174
|
+
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
175
|
+
req.session.destroy();
|
|
176
|
+
clearSessionCookies(res);
|
|
177
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Store user role in request for checkRolePermission to use
|
|
182
|
+
req.userRole = result.rows[0].Role;
|
|
183
|
+
|
|
184
|
+
next();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error("[mbkauthe] API session validation error:", err);
|
|
187
|
+
return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Reload session user values from the database and refresh cookies.
|
|
193
|
+
* - Validates sessionId and active status
|
|
194
|
+
* - Updates `req.session.user` fields (username, role, allowedApps, fullname)
|
|
195
|
+
* - Uses cached `fullName` cookie when available, otherwise queries `profiledata`
|
|
196
|
+
* - Syncs `username`, `fullName` and `sessionId` cookies
|
|
197
|
+
* Returns: true if session refreshed and valid, false if session invalidated
|
|
198
|
+
*/
|
|
199
|
+
export async function reloadSessionUser(req, res) {
|
|
200
|
+
if (!req.session || !req.session.user || !req.session.user.id) return false;
|
|
201
|
+
try {
|
|
202
|
+
const { id, sessionId: currentSessionId } = req.session.user;
|
|
203
|
+
|
|
204
|
+
if (!currentSessionId) {
|
|
205
|
+
req.session.destroy(() => {});
|
|
206
|
+
clearSessionCookies(res);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const normalizedSessionId = String(currentSessionId);
|
|
211
|
+
const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
|
|
212
|
+
FROM "Sessions" s
|
|
213
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
214
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
215
|
+
const result = await dblogin.query({ name: 'reload-session-user', text: query, values: [normalizedSessionId] });
|
|
216
|
+
|
|
217
|
+
if (result.rows.length === 0) {
|
|
218
|
+
// Session not found — invalidate session
|
|
219
|
+
req.session.destroy(() => {});
|
|
220
|
+
clearSessionCookies(res);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const row = result.rows[0];
|
|
225
|
+
|
|
226
|
+
// Check expired
|
|
227
|
+
if (row.expires_at && new Date(row.expires_at) <= new Date()) {
|
|
228
|
+
req.session.destroy(() => {});
|
|
229
|
+
clearSessionCookies(res);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!row.Active) {
|
|
234
|
+
// Account is inactive
|
|
235
|
+
req.session.destroy(() => {});
|
|
236
|
+
clearSessionCookies(res);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Authorization: ensure allowed for current app unless SuperAdmin
|
|
241
|
+
if (row.Role !== 'SuperAdmin') {
|
|
242
|
+
const allowedApps = row.AllowedApps;
|
|
243
|
+
const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
|
|
244
|
+
if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
245
|
+
req.session.destroy(() => {});
|
|
246
|
+
clearSessionCookies(res);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Update session fields
|
|
252
|
+
req.session.user.username = row.UserName;
|
|
253
|
+
req.session.user.role = row.Role;
|
|
254
|
+
req.session.user.allowedApps = row.AllowedApps;
|
|
255
|
+
|
|
256
|
+
// Obtain fullname from client cookie cache when present else DB
|
|
257
|
+
if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
|
|
258
|
+
req.session.user.fullname = req.cookies.fullName;
|
|
259
|
+
} else {
|
|
260
|
+
try {
|
|
261
|
+
const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
|
|
262
|
+
if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
|
|
263
|
+
} catch (profileErr) {
|
|
264
|
+
console.error('[mbkauthe] Error fetching fullname during reload:', profileErr);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Persist session changes
|
|
269
|
+
await new Promise((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
|
|
270
|
+
|
|
271
|
+
// Sync cookies for client UI (non-httpOnly)
|
|
272
|
+
try {
|
|
273
|
+
res.cookie('username', req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
274
|
+
res.cookie('fullName', req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
275
|
+
res.cookie('sessionId', req.session.user.sessionId, cachedCookieOptions);
|
|
276
|
+
} catch (cookieErr) {
|
|
277
|
+
// Ignore cookie setting errors, session is still refreshed
|
|
278
|
+
console.error('[mbkauthe] Error syncing cookies during reload:', cookieErr);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return true;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error('[mbkauthe] reloadSessionUser error:', err);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
100
288
|
const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
101
289
|
return async (req, res, next) => {
|
|
102
290
|
try {
|
|
@@ -173,5 +361,4 @@ const authenticate = (authentication) => {
|
|
|
173
361
|
};
|
|
174
362
|
};
|
|
175
363
|
|
|
176
|
-
|
|
177
|
-
export { validateSession, checkRolePermission, validateSessionAndRole, authenticate };
|
|
364
|
+
export { validateSession, validateApiSession, checkRolePermission, validateSessionAndRole, authenticate };
|
package/lib/middleware/index.js
CHANGED
|
@@ -57,8 +57,8 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
57
57
|
if (!req.session.user && req.cookies.sessionId) {
|
|
58
58
|
const sessionId = req.cookies.sessionId;
|
|
59
59
|
|
|
60
|
-
// Early validation to avoid unnecessary processing
|
|
61
|
-
if (typeof sessionId !== 'string' || !/^[
|
|
60
|
+
// Early validation to avoid unnecessary processing (expect DB UUID id)
|
|
61
|
+
if (typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
62
62
|
// Clear invalid cookie to prevent repeated attempts
|
|
63
63
|
res.clearCookie('sessionId', {
|
|
64
64
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
@@ -71,20 +71,48 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
// Validate session by DB primary key id and join to user
|
|
75
|
+
const query = `SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps", s.expires_at
|
|
76
|
+
FROM "Sessions" s
|
|
77
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
78
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
79
|
+
const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [sessionId] });
|
|
78
80
|
|
|
79
81
|
if (result.rows.length > 0) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
const row = result.rows[0];
|
|
83
|
+
|
|
84
|
+
// Reject expired sessions or inactive users
|
|
85
|
+
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
86
|
+
// leave cookies cleared and don't restore session
|
|
87
|
+
} else {
|
|
88
|
+
const normalizedSessionId = String(sessionId);
|
|
89
|
+
req.session.user = {
|
|
90
|
+
id: row.id,
|
|
91
|
+
username: row.UserName,
|
|
92
|
+
role: row.Role,
|
|
93
|
+
sessionId: normalizedSessionId,
|
|
94
|
+
allowedApps: row.AllowedApps,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Use cached FullName from client cookie when available to avoid extra DB queries
|
|
98
|
+
if (req.cookies.fullName && typeof req.cookies.fullName === 'string') {
|
|
99
|
+
req.session.user.fullname = req.cookies.fullName;
|
|
100
|
+
} else {
|
|
101
|
+
// Fallback: attempt to fetch FullName from profiledata to populate session
|
|
102
|
+
try {
|
|
103
|
+
const profileRes = await dblogin.query({
|
|
104
|
+
name: 'restore-get-fullname',
|
|
105
|
+
text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1',
|
|
106
|
+
values: [row.UserName]
|
|
107
|
+
});
|
|
108
|
+
if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {
|
|
109
|
+
req.session.user.fullname = profileRes.rows[0].FullName;
|
|
110
|
+
}
|
|
111
|
+
} catch (profileErr) {
|
|
112
|
+
console.error("[mbkauthe] Error fetching FullName during session restore:", profileErr);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
88
116
|
}
|
|
89
117
|
} catch (err) {
|
|
90
118
|
console.error("[mbkauthe] Session restoration error:", err);
|
|
@@ -99,8 +127,10 @@ export function sessionCookieSyncMiddleware(req, res, next) {
|
|
|
99
127
|
// Only set cookies if they're missing or different
|
|
100
128
|
if (req.cookies.sessionId !== req.session.user.sessionId) {
|
|
101
129
|
res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
130
|
+
// Also expose FullName (fallback to username) for display in client-side UI
|
|
131
|
+
res.cookie("fullName", req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
102
132
|
res.cookie("sessionId", req.session.user.sessionId, cachedCookieOptions);
|
|
103
133
|
}
|
|
104
134
|
}
|
|
105
135
|
next();
|
|
106
|
-
}
|
|
136
|
+
}
|