mbkauthe 3.1.0 → 3.2.1
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 +75 -15
- package/docs/api.md +115 -10
- package/docs/db.md +72 -17
- package/docs/env.md +91 -8
- package/index.d.ts +25 -2
- package/lib/config/index.js +48 -1
- package/lib/main.js +12 -0
- package/lib/middleware/index.js +4 -2
- package/lib/routes/auth.js +25 -3
- package/lib/routes/misc.js +24 -3
- package/lib/routes/oauth.js +379 -264
- package/lib/utils/response.js +2 -2
- package/package.json +18 -5
- package/test.spec.js +20 -2
- package/views/2fa.handlebars +2 -2
- package/views/loginmbkauthe.handlebars +35 -5
- package/views/sharedStyles.handlebars +5 -0
- package/views/showmessage.handlebars +27 -1
- package/.env.example +0 -3
- package/.github/PACKAGE.md +0 -17
- package/.github/workflows/codeql.yml +0 -98
- package/.github/workflows/publish.yml +0 -87
package/index.d.ts
CHANGED
|
@@ -34,10 +34,11 @@ declare global {
|
|
|
34
34
|
UserName?: string;
|
|
35
35
|
role: 'SuperAdmin' | 'NormalUser' | 'Guest';
|
|
36
36
|
Role?: 'SuperAdmin' | 'NormalUser' | 'Guest';
|
|
37
|
-
loginMethod?: 'password' | 'github';
|
|
37
|
+
loginMethod?: 'password' | 'github' | 'google';
|
|
38
38
|
redirectUrl?: string | null;
|
|
39
39
|
};
|
|
40
40
|
oauthRedirect?: string;
|
|
41
|
+
oauthCsrfToken?: string;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -57,10 +58,22 @@ declare module 'mbkauthe' {
|
|
|
57
58
|
GITHUB_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
58
59
|
GITHUB_CLIENT_ID?: string;
|
|
59
60
|
GITHUB_CLIENT_SECRET?: string;
|
|
61
|
+
GOOGLE_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
62
|
+
GOOGLE_CLIENT_ID?: string;
|
|
63
|
+
GOOGLE_CLIENT_SECRET?: string;
|
|
60
64
|
loginRedirectURL?: string;
|
|
61
65
|
EncPass?: 'true' | 'false' | 'f';
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
export interface OAuthConfig {
|
|
69
|
+
GITHUB_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
70
|
+
GITHUB_CLIENT_ID?: string;
|
|
71
|
+
GITHUB_CLIENT_SECRET?: string;
|
|
72
|
+
GOOGLE_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
73
|
+
GOOGLE_CLIENT_ID?: string;
|
|
74
|
+
GOOGLE_CLIENT_SECRET?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
// User Types
|
|
65
78
|
export type UserRole = 'SuperAdmin' | 'NormalUser' | 'Guest';
|
|
66
79
|
|
|
@@ -80,7 +93,7 @@ declare module 'mbkauthe' {
|
|
|
80
93
|
UserName?: string;
|
|
81
94
|
role: UserRole;
|
|
82
95
|
Role?: UserRole;
|
|
83
|
-
loginMethod?: 'password' | 'github';
|
|
96
|
+
loginMethod?: 'password' | 'github' | 'google';
|
|
84
97
|
redirectUrl?: string | null;
|
|
85
98
|
}
|
|
86
99
|
|
|
@@ -127,6 +140,16 @@ declare module 'mbkauthe' {
|
|
|
127
140
|
updated_at: Date;
|
|
128
141
|
}
|
|
129
142
|
|
|
143
|
+
export interface GoogleUser {
|
|
144
|
+
id: number;
|
|
145
|
+
user_name: string;
|
|
146
|
+
google_id: string;
|
|
147
|
+
google_email: string;
|
|
148
|
+
access_token: string;
|
|
149
|
+
created_at: Date;
|
|
150
|
+
updated_at: Date;
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
// API Response Types
|
|
131
154
|
export interface LoginResponse {
|
|
132
155
|
success: boolean;
|
package/lib/config/index.js
CHANGED
|
@@ -28,6 +28,38 @@ function validateConfiguration() {
|
|
|
28
28
|
throw new Error(`[mbkauthe] Configuration Error:\n - ${errors.join('\n - ')}`);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Parse and validate oAuthVar (optional fallback for OAuth settings)
|
|
32
|
+
let oAuthVar = null;
|
|
33
|
+
try {
|
|
34
|
+
if (process.env.oAuthVar) {
|
|
35
|
+
oAuthVar = JSON.parse(process.env.oAuthVar);
|
|
36
|
+
if (oAuthVar && typeof oAuthVar !== 'object') {
|
|
37
|
+
console.warn('[mbkauthe] oAuthVar is not a valid object, ignoring it');
|
|
38
|
+
oAuthVar = null;
|
|
39
|
+
} else {
|
|
40
|
+
console.log('[mbkauthe] oAuthVar detected and parsed successfully');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn('[mbkauthe] Invalid JSON in process.env.oAuthVar, ignoring it');
|
|
45
|
+
oAuthVar = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Merge OAuth settings: use oAuthVar as fallback if values not in mbkautheVar
|
|
49
|
+
const oAuthKeys = [
|
|
50
|
+
'GITHUB_LOGIN_ENABLED', 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET',
|
|
51
|
+
'GOOGLE_LOGIN_ENABLED', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (oAuthVar) {
|
|
55
|
+
oAuthKeys.forEach(key => {
|
|
56
|
+
if ((!mbkautheVar[key] || (typeof mbkautheVar[key] === 'string' && mbkautheVar[key].trim() === '')) && oAuthVar[key]) {
|
|
57
|
+
mbkautheVar[key] = oAuthVar[key];
|
|
58
|
+
console.log(`[mbkauthe] Using ${key} from oAuthVar`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
// Validate required keys
|
|
32
64
|
// COOKIE_EXPIRE_TIME is not required but if provided must be valid, COOKIE_EXPIRE_TIME by default is 2 days
|
|
33
65
|
// loginRedirectURL is not required but if provided must be valid, loginRedirectURL by default is /dashboard
|
|
@@ -55,6 +87,11 @@ function validateConfiguration() {
|
|
|
55
87
|
errors.push("mbkautheVar.GITHUB_LOGIN_ENABLED must be either 'true' or 'false' or 'f'");
|
|
56
88
|
}
|
|
57
89
|
|
|
90
|
+
// Validate GOOGLE_LOGIN_ENABLED value
|
|
91
|
+
if (mbkautheVar.GOOGLE_LOGIN_ENABLED && !['true', 'false', 'f'].includes(mbkautheVar.GOOGLE_LOGIN_ENABLED.toLowerCase())) {
|
|
92
|
+
errors.push("mbkautheVar.GOOGLE_LOGIN_ENABLED must be either 'true' or 'false' or 'f'");
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
// Validate EncPass value if provided
|
|
59
96
|
if (mbkautheVar.EncPass && !['true', 'false', 'f'].includes(mbkautheVar.EncPass.toLowerCase())) {
|
|
60
97
|
errors.push("mbkautheVar.EncPass must be either 'true' or 'false' or 'f'");
|
|
@@ -70,6 +107,16 @@ function validateConfiguration() {
|
|
|
70
107
|
}
|
|
71
108
|
}
|
|
72
109
|
|
|
110
|
+
// Validate Google login configuration
|
|
111
|
+
if (mbkautheVar.GOOGLE_LOGIN_ENABLED === "true") {
|
|
112
|
+
if (!mbkautheVar.GOOGLE_CLIENT_ID || mbkautheVar.GOOGLE_CLIENT_ID.trim() === '') {
|
|
113
|
+
errors.push("mbkautheVar.GOOGLE_CLIENT_ID is required when GOOGLE_LOGIN_ENABLED is 'true'");
|
|
114
|
+
}
|
|
115
|
+
if (!mbkautheVar.GOOGLE_CLIENT_SECRET || mbkautheVar.GOOGLE_CLIENT_SECRET.trim() === '') {
|
|
116
|
+
errors.push("mbkautheVar.GOOGLE_CLIENT_SECRET is required when GOOGLE_LOGIN_ENABLED is 'true'");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
73
120
|
// Validate COOKIE_EXPIRE_TIME if provided
|
|
74
121
|
if (mbkautheVar.COOKIE_EXPIRE_TIME !== undefined) {
|
|
75
122
|
const expireTime = parseFloat(mbkautheVar.COOKIE_EXPIRE_TIME);
|
|
@@ -133,4 +180,4 @@ try {
|
|
|
133
180
|
}
|
|
134
181
|
}
|
|
135
182
|
|
|
136
|
-
export { packageJson, appVersion };
|
|
183
|
+
export { packageJson, appVersion };
|
package/lib/main.js
CHANGED
|
@@ -19,6 +19,18 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
19
19
|
|
|
20
20
|
const router = express.Router();
|
|
21
21
|
|
|
22
|
+
// Configure Express to trust proxy headers for rate limiting in dev mode only
|
|
23
|
+
// This prevents conflicts with parent project proxy settings in production
|
|
24
|
+
if (process.env.test === "dev") {
|
|
25
|
+
router.use((req, res, next) => {
|
|
26
|
+
// Set trust proxy to true for the app instance if not already set
|
|
27
|
+
if (!req.app.get('trust proxy')) {
|
|
28
|
+
req.app.set('trust proxy', true);
|
|
29
|
+
}
|
|
30
|
+
next();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
// Basic middleware
|
|
23
35
|
router.use(express.json());
|
|
24
36
|
router.use(express.urlencoded({ extended: true }));
|
package/lib/middleware/index.js
CHANGED
|
@@ -18,9 +18,11 @@ export const sessionConfig = {
|
|
|
18
18
|
proxy: true,
|
|
19
19
|
cookie: {
|
|
20
20
|
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
21
|
-
domain
|
|
21
|
+
// Don't set domain in development/localhost to avoid cookie issues
|
|
22
|
+
domain: (mbkautheVar.IS_DEPLOYED === 'true' && process.env.test !== 'dev') ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
22
23
|
httpOnly: true,
|
|
23
|
-
secure
|
|
24
|
+
// Only use secure cookies in production with HTTPS
|
|
25
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true' && process.env.test !== 'dev',
|
|
24
26
|
sameSite: 'lax',
|
|
25
27
|
path: '/'
|
|
26
28
|
},
|
package/lib/routes/auth.js
CHANGED
|
@@ -22,19 +22,31 @@ const LoginLimit = rateLimit({
|
|
|
22
22
|
message: { success: false, message: "Too many attempts, please try again later" },
|
|
23
23
|
skip: (req) => {
|
|
24
24
|
return !!req.session.user;
|
|
25
|
+
},
|
|
26
|
+
validate: {
|
|
27
|
+
trustProxy: false,
|
|
28
|
+
xForwardedForHeader: false
|
|
25
29
|
}
|
|
26
30
|
});
|
|
27
31
|
|
|
28
32
|
const LogoutLimit = rateLimit({
|
|
29
33
|
windowMs: 1 * 60 * 1000,
|
|
30
34
|
max: 10,
|
|
31
|
-
message: { success: false, message: "Too many logout attempts, please try again later" }
|
|
35
|
+
message: { success: false, message: "Too many logout attempts, please try again later" },
|
|
36
|
+
validate: {
|
|
37
|
+
trustProxy: false,
|
|
38
|
+
xForwardedForHeader: false
|
|
39
|
+
}
|
|
32
40
|
});
|
|
33
41
|
|
|
34
42
|
const TwoFALimit = rateLimit({
|
|
35
43
|
windowMs: 1 * 60 * 1000,
|
|
36
44
|
max: 5,
|
|
37
|
-
message: { success: false, message: "Too many 2FA attempts, please try again later" }
|
|
45
|
+
message: { success: false, message: "Too many 2FA attempts, please try again later" },
|
|
46
|
+
validate: {
|
|
47
|
+
trustProxy: false,
|
|
48
|
+
xForwardedForHeader: false
|
|
49
|
+
}
|
|
38
50
|
});
|
|
39
51
|
|
|
40
52
|
// CSRF protection middleware
|
|
@@ -117,8 +129,17 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
117
129
|
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
118
130
|
console.log(`[mbkauthe] Generated session ID for username: ${username}`);
|
|
119
131
|
|
|
120
|
-
//
|
|
132
|
+
// Fix session fixation: Delete old session BEFORE regenerating to prevent timing window
|
|
121
133
|
const oldSessionId = req.sessionID;
|
|
134
|
+
|
|
135
|
+
// Delete old session first to prevent session fixation attacks
|
|
136
|
+
await dblogin.query({
|
|
137
|
+
name: 'login-delete-old-session-before-regen',
|
|
138
|
+
text: 'DELETE FROM "session" WHERE sid = $1',
|
|
139
|
+
values: [oldSessionId]
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Now regenerate with new session ID (timing window closed)
|
|
122
143
|
await new Promise((resolve, reject) => {
|
|
123
144
|
req.session.regenerate((err) => {
|
|
124
145
|
if (err) reject(err);
|
|
@@ -509,6 +530,7 @@ router.get("/login", LoginLimit, csrfProtection, (req, res) => {
|
|
|
509
530
|
return res.render("loginmbkauthe.handlebars", {
|
|
510
531
|
layout: false,
|
|
511
532
|
githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
|
|
533
|
+
googleLoginEnabled: mbkautheVar.GOOGLE_LOGIN_ENABLED,
|
|
512
534
|
customURL: mbkautheVar.loginRedirectURL || '/dashboard',
|
|
513
535
|
userLoggedIn: !!req.session?.user,
|
|
514
536
|
username: req.session?.user?.username || '',
|
package/lib/routes/misc.js
CHANGED
|
@@ -22,6 +22,21 @@ const LoginLimit = rateLimit({
|
|
|
22
22
|
message: { success: false, message: "Too many attempts, please try again later" },
|
|
23
23
|
skip: (req) => {
|
|
24
24
|
return !!req.session.user;
|
|
25
|
+
},
|
|
26
|
+
validate: {
|
|
27
|
+
trustProxy: false,
|
|
28
|
+
xForwardedForHeader: false
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Rate limiter for admin operations
|
|
33
|
+
const AdminOperationLimit = rateLimit({
|
|
34
|
+
windowMs: 5 * 60 * 1000,
|
|
35
|
+
max: 3,
|
|
36
|
+
message: { success: false, message: "Too many admin operations, please try again later" },
|
|
37
|
+
validate: {
|
|
38
|
+
trustProxy: false,
|
|
39
|
+
xForwardedForHeader: false
|
|
25
40
|
}
|
|
26
41
|
});
|
|
27
42
|
|
|
@@ -232,12 +247,18 @@ router.get(["/info", "/i"], LoginLimit, async (req, res) => {
|
|
|
232
247
|
});
|
|
233
248
|
|
|
234
249
|
// Terminate all sessions (admin endpoint)
|
|
235
|
-
router.post("/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
250
|
+
router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
236
251
|
try {
|
|
237
252
|
// Run both operations in parallel for better performance
|
|
238
253
|
await Promise.all([
|
|
239
|
-
dblogin.query({
|
|
240
|
-
|
|
254
|
+
dblogin.query({
|
|
255
|
+
name: 'terminate-all-user-sessions',
|
|
256
|
+
text: 'UPDATE "Users" SET "SessionId" = NULL WHERE "SessionId" IS NOT NULL'
|
|
257
|
+
}),
|
|
258
|
+
dblogin.query({
|
|
259
|
+
name: 'terminate-all-db-sessions',
|
|
260
|
+
text: 'DELETE FROM "session" WHERE expire > NOW()'
|
|
261
|
+
})
|
|
241
262
|
]);
|
|
242
263
|
|
|
243
264
|
req.session.destroy((err) => {
|