mbkauthe 4.7.2 → 4.8.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 +3 -3
- package/docs/api.md +12 -12
- package/docs/db.md +3 -1
- package/docs/db.sql +6 -0
- package/docs/env.md +18 -6
- package/index.d.ts +8 -4
- package/lib/config/index.js +8 -5
- package/lib/middleware/auth.js +53 -0
- package/lib/pool.js +12 -8
- package/lib/routes/dbLogs.js +8 -4
- package/lib/routes/oauth.js +361 -355
- package/lib/utils/dbQueryLogger.js +210 -133
- package/package.json +1 -1
- package/test.spec.js +14 -2
- package/views/pages/dbLogs.handlebars +56 -5
- package/views/pages/loginmbkauthe.handlebars +2 -2
package/lib/routes/oauth.js
CHANGED
|
@@ -14,98 +14,100 @@ const router = express.Router();
|
|
|
14
14
|
// CSRF protection middleware
|
|
15
15
|
const csrfProtection = csurf({ cookie: true });
|
|
16
16
|
|
|
17
|
-
// Rate limiter for
|
|
17
|
+
// Rate limiter for social auth routes
|
|
18
18
|
const createOAuthLimit = (provider) => rateLimit({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Use IP from connection if available, otherwise fallback to a default
|
|
32
|
-
return req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || 'unknown';
|
|
33
|
-
}
|
|
19
|
+
windowMs: 5 * 60 * 1000,
|
|
20
|
+
max: 10,
|
|
21
|
+
message: `Too many ${provider} login attempts, please try again later`,
|
|
22
|
+
standardHeaders: true,
|
|
23
|
+
legacyHeaders: false,
|
|
24
|
+
validate: {
|
|
25
|
+
xForwardedForHeader: false,
|
|
26
|
+
trustProxy: false
|
|
27
|
+
},
|
|
28
|
+
keyGenerator: (req) => {
|
|
29
|
+
return req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || 'unknown';
|
|
30
|
+
}
|
|
34
31
|
});
|
|
35
32
|
|
|
36
33
|
const GitHubOAuthLimit = createOAuthLimit('GitHub');
|
|
37
34
|
const GoogleOAuthLimit = createOAuthLimit('Google');
|
|
38
35
|
|
|
36
|
+
const githubClientId = mbkautheVar.GITHUB_APP_CLIENT_ID || mbkautheVar.GITHUB_CLIENT_ID;
|
|
37
|
+
const githubClientSecret = mbkautheVar.GITHUB_APP_CLIENT_SECRET || mbkautheVar.GITHUB_CLIENT_SECRET;
|
|
38
|
+
|
|
39
39
|
// Common OAuth strategy handler
|
|
40
40
|
const createOAuthStrategy = async (provider, profile, done) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
try {
|
|
42
|
+
console.log(`[mbkauthe] ${provider} OAuth callback for user: ${profile.emails?.[0]?.value || profile.id}`);
|
|
43
|
+
|
|
44
|
+
const isGitHub = provider === 'GitHub';
|
|
45
|
+
const tableName = isGitHub ? 'user_github' : 'user_google';
|
|
46
|
+
const idField = isGitHub ? 'github_id' : 'google_id';
|
|
47
|
+
const queryName = isGitHub ? 'github-login-get-user' : 'google-login-get-user';
|
|
48
|
+
|
|
49
|
+
// Check if this OAuth account is linked to any user
|
|
50
|
+
const oauthUser = await dblogin.query({
|
|
51
|
+
name: queryName,
|
|
52
|
+
text: `SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id" FROM ${tableName} ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.${idField} = $1`,
|
|
53
|
+
values: [profile.id]
|
|
54
|
+
});
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
if (oauthUser.rows.length === 0) {
|
|
57
|
+
const error = new Error(`${provider} account not linked to any user`);
|
|
58
|
+
error.code = `${provider.toUpperCase()}_NOT_LINKED`;
|
|
59
|
+
return done(error);
|
|
60
|
+
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
const user = oauthUser.rows[0];
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
// Check if the user account is active
|
|
65
|
+
if (!user.Active) {
|
|
66
|
+
const error = new Error('Account is inactive');
|
|
67
|
+
error.code = 'ACCOUNT_INACTIVE';
|
|
68
|
+
return done(error);
|
|
69
|
+
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
// Check if user is authorized for this app
|
|
72
|
+
if (user.Role !== "SuperAdmin") {
|
|
73
|
+
const allowedApps = user.AllowedApps;
|
|
74
|
+
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
75
|
+
const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
|
|
76
|
+
error.code = 'NOT_AUTHORIZED';
|
|
77
|
+
return done(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
// Return user data for login
|
|
82
|
+
const userData = {
|
|
83
|
+
id: user.id,
|
|
84
|
+
username: user.UserName,
|
|
85
|
+
role: user.Role,
|
|
86
|
+
};
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
if (isGitHub) {
|
|
89
|
+
userData.githubId = user.github_id;
|
|
90
|
+
userData.githubUsername = user.github_username;
|
|
91
|
+
userData.installationId = user.installation_id || null;
|
|
92
|
+
userData.installationTargetType = user.installation_target_type || null;
|
|
93
|
+
} else {
|
|
94
|
+
userData.googleId = user.google_id;
|
|
95
|
+
userData.googleEmail = user.google_email;
|
|
96
|
+
}
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
return done(null, userData);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`[mbkauthe] ${provider} login error:`, err);
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
// Handle specific OAuth errors
|
|
103
|
+
if (err.name === 'TokenError' || err.code === 'invalid_grant') {
|
|
104
|
+
err.code = 'invalid_grant';
|
|
105
|
+
err.message = 'OAuth token validation failed. This may be due to an expired authorization code or clock synchronization issues.';
|
|
106
|
+
} else {
|
|
107
|
+
err.code = err.code || `${provider.toUpperCase()}_AUTH_ERROR`;
|
|
108
|
+
}
|
|
109
|
+
return done(err);
|
|
106
110
|
}
|
|
107
|
-
return done(err);
|
|
108
|
-
}
|
|
109
111
|
};
|
|
110
112
|
|
|
111
113
|
// Configure OAuth strategies and track enabled providers
|
|
@@ -113,347 +115,351 @@ const enabledProviders = [];
|
|
|
113
115
|
|
|
114
116
|
// Configure GitHub Strategy for login (only if enabled and configured)
|
|
115
117
|
if ((mbkautheVar.GITHUB_LOGIN_ENABLED || "").toLowerCase() === "true") {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
if (githubClientId && githubClientSecret) {
|
|
119
|
+
passport.use('github-login', new GitHubStrategy({
|
|
120
|
+
clientID: githubClientId,
|
|
121
|
+
clientSecret: githubClientSecret,
|
|
122
|
+
callbackURL: '/mbkauthe/api/github/login/callback',
|
|
123
|
+
scope: ['user:email']
|
|
124
|
+
}, (accessToken, refreshToken, profile, done) =>
|
|
125
|
+
createOAuthStrategy('GitHub', profile, done)
|
|
126
|
+
));
|
|
127
|
+
enabledProviders.push('GitHub App');
|
|
128
|
+
} else {
|
|
129
|
+
console.warn('[mbkauthe] GITHUB_LOGIN_ENABLED is true but GITHUB_APP_CLIENT_ID/SECRET are missing; skipping GitHub strategy registration');
|
|
130
|
+
}
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
// Configure Google Strategy for login (only if enabled and configured)
|
|
132
134
|
if ((mbkautheVar.GOOGLE_LOGIN_ENABLED || "").toLowerCase() === "true") {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
if (mbkautheVar.GOOGLE_CLIENT_ID && mbkautheVar.GOOGLE_CLIENT_SECRET) {
|
|
136
|
+
passport.use('google-login', new GoogleStrategy({
|
|
137
|
+
clientID: mbkautheVar.GOOGLE_CLIENT_ID,
|
|
138
|
+
clientSecret: mbkautheVar.GOOGLE_CLIENT_SECRET,
|
|
139
|
+
callbackURL: '/mbkauthe/api/google/login/callback',
|
|
140
|
+
scope: ['profile', 'email']
|
|
141
|
+
}, (accessToken, refreshToken, profile, done) =>
|
|
142
|
+
createOAuthStrategy('Google', profile, done)
|
|
143
|
+
));
|
|
144
|
+
enabledProviders.push('Google');
|
|
145
|
+
} else {
|
|
146
|
+
console.warn('[mbkauthe] GOOGLE_LOGIN_ENABLED is true but GOOGLE_CLIENT_ID/SECRET missing; skipping Google strategy registration');
|
|
147
|
+
}
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
// Print consolidated OAuth summary
|
|
149
151
|
if (enabledProviders.length > 0) {
|
|
150
|
-
|
|
152
|
+
console.log(`[mbkauthe] Social providers: ${enabledProviders.join(', ')}`);
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
// Serialize/Deserialize user for OAuth login
|
|
154
156
|
passport.serializeUser((user, done) => {
|
|
155
|
-
|
|
157
|
+
done(null, user);
|
|
156
158
|
});
|
|
157
159
|
|
|
158
160
|
passport.deserializeUser((user, done) => {
|
|
159
|
-
|
|
161
|
+
done(null, user);
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
// Common OAuth initiation handler
|
|
163
165
|
const createOAuthInitiation = (provider, enabledFlag, clientIdFlag, clientSecretFlag) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
166
|
+
return (req, res, next) => {
|
|
167
|
+
const isEnabled = String(enabledFlag || '').toLowerCase() === 'true';
|
|
168
|
+
|
|
169
|
+
if (isEnabled) {
|
|
170
|
+
// Validate OAuth configuration before invoking passport strategy.
|
|
171
|
+
if (!clientIdFlag || !clientSecretFlag) {
|
|
172
|
+
console.error(`[mbkauthe] ${provider} OAuth not properly configured`);
|
|
173
|
+
return renderError(res, req, {
|
|
174
|
+
code: 500,
|
|
175
|
+
error: 'Configuration Error',
|
|
176
|
+
message: `${provider} authentication is not properly configured. Please contact your administrator.`,
|
|
177
|
+
page: '/mbkauthe/login',
|
|
178
|
+
pagename: 'Login'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Store CSRF token for validation on callback
|
|
183
|
+
const csrfToken = req.csrfToken();
|
|
184
|
+
req.session.oauthCsrfToken = csrfToken;
|
|
185
|
+
console.log(`[mbkauthe] ${provider} OAuth initiation started`);
|
|
186
|
+
|
|
187
|
+
// Store redirect parameter in session before OAuth flow
|
|
188
|
+
const redirect = req.query.redirect;
|
|
189
|
+
if (redirect && typeof redirect === 'string') {
|
|
190
|
+
// Only allow relative URLs to prevent open redirect attacks
|
|
191
|
+
if (redirect.startsWith('/') && !redirect.startsWith('//')) {
|
|
192
|
+
req.session.oauthRedirect = redirect;
|
|
193
|
+
} else {
|
|
194
|
+
console.warn(`[mbkauthe] Invalid redirect parameter rejected: ${redirect}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Save session before OAuth redirect to ensure CSRF token is persisted
|
|
199
|
+
req.session.save((err) => {
|
|
200
|
+
if (err) {
|
|
201
|
+
console.error(`[mbkauthe] ${provider} session save error:`, err);
|
|
202
|
+
return renderError(res, req, {
|
|
203
|
+
code: 500,
|
|
204
|
+
error: 'Session Error',
|
|
205
|
+
message: 'Failed to initialize OAuth flow. Please try again.',
|
|
206
|
+
page: '/mbkauthe/login',
|
|
207
|
+
pagename: 'Login'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
console.log(`[mbkauthe] ${provider} OAuth session saved successfully`);
|
|
211
|
+
passport.authenticate(`${provider.toLowerCase()}-login`, { state: csrfToken })(req, res, next);
|
|
212
|
+
});
|
|
191
213
|
} else {
|
|
192
|
-
|
|
214
|
+
return renderError(res, req, {
|
|
215
|
+
code: 403,
|
|
216
|
+
error: `${provider} Login Disabled`,
|
|
217
|
+
message: `${provider} login is currently disabled. Please use your username and password to log in.`,
|
|
218
|
+
page: '/mbkauthe/login',
|
|
219
|
+
pagename: 'Login'
|
|
220
|
+
});
|
|
193
221
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// Save session before OAuth redirect to ensure CSRF token is persisted
|
|
197
|
-
req.session.save((err) => {
|
|
198
|
-
if (err) {
|
|
199
|
-
console.error(`[mbkauthe] ${provider} session save error:`, err);
|
|
200
|
-
return renderError(res, req, {
|
|
201
|
-
code: 500,
|
|
202
|
-
error: 'Session Error',
|
|
203
|
-
message: 'Failed to initialize OAuth flow. Please try again.',
|
|
204
|
-
page: '/mbkauthe/login',
|
|
205
|
-
pagename: 'Login'
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
console.log(`[mbkauthe] ${provider} OAuth session saved successfully`);
|
|
209
|
-
passport.authenticate(`${provider.toLowerCase()}-login`, { state: csrfToken })(req, res, next);
|
|
210
|
-
});
|
|
211
|
-
} else {
|
|
212
|
-
return renderError(res, req, {
|
|
213
|
-
code: 403,
|
|
214
|
-
error: `${provider} Login Disabled`,
|
|
215
|
-
message: `${provider} login is currently disabled. Please use your username and password to log in.`,
|
|
216
|
-
page: '/mbkauthe/login',
|
|
217
|
-
pagename: 'Login'
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
};
|
|
222
|
+
};
|
|
221
223
|
};
|
|
222
224
|
|
|
223
225
|
// Common OAuth error handler
|
|
224
226
|
const createOAuthErrorHandler = (provider) => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
227
|
+
return (err) => {
|
|
228
|
+
const providerUpper = provider.toUpperCase();
|
|
229
|
+
switch (err.code) {
|
|
230
|
+
case 'invalid_grant':
|
|
231
|
+
case 'OAUTH_TOKEN_ERROR':
|
|
232
|
+
return {
|
|
233
|
+
code: 400,
|
|
234
|
+
error: 'OAuth Token Error',
|
|
235
|
+
message: `The ${provider} authentication token has expired or is invalid. Please try signing in again.`,
|
|
236
|
+
page: '/mbkauthe/login',
|
|
237
|
+
pagename: 'Login',
|
|
238
|
+
details: process.env.NODE_ENV === 'development' ? `OAuth Error: ${err.message}` : 'Please refresh and try again'
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
case `${providerUpper}_NOT_LINKED`:
|
|
242
|
+
return {
|
|
243
|
+
code: 403,
|
|
244
|
+
error: `${provider} Account Not Linked`,
|
|
245
|
+
message: `Your ${provider} account is not linked to any user in our system. To link your ${provider} account, a User must connect their ${provider} account to mbktech account through the user settings.`,
|
|
246
|
+
page: '/mbkauthe/login',
|
|
247
|
+
pagename: 'Login'
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
case 'ACCOUNT_INACTIVE':
|
|
251
|
+
return {
|
|
252
|
+
code: 403,
|
|
253
|
+
error: 'Account Inactive',
|
|
254
|
+
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
255
|
+
page: '/mbkauthe/login',
|
|
256
|
+
pagename: 'Login'
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
case 'NOT_AUTHORIZED':
|
|
260
|
+
return {
|
|
261
|
+
code: 403,
|
|
262
|
+
error: 'Not Authorized',
|
|
263
|
+
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
264
|
+
page: '/mbkauthe/login',
|
|
265
|
+
pagename: 'Login'
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
default:
|
|
269
|
+
return {
|
|
270
|
+
code: 500,
|
|
271
|
+
error: 'Authentication Error',
|
|
272
|
+
message: `An error occurred during ${provider} authentication. Please try again.`,
|
|
273
|
+
page: '/mbkauthe/login',
|
|
274
|
+
pagename: 'Login'
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
};
|
|
276
278
|
};
|
|
277
279
|
|
|
278
280
|
// Common OAuth callback validation
|
|
279
281
|
const validateOAuthCallback = (req, res) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
code: 403,
|
|
291
|
-
error: 'Invalid Request',
|
|
292
|
-
message: 'OAuth security validation failed. Please try again.',
|
|
293
|
-
page: '/mbkauthe/login',
|
|
294
|
-
pagename: 'Login'
|
|
295
|
-
});
|
|
296
|
-
return false; // Invalid - response already sent
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
console.log(`[mbkauthe] OAuth CSRF validation passed`);
|
|
300
|
-
delete req.session.oauthCsrfToken;
|
|
301
|
-
return true; // Valid
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
// Common OAuth callback handler
|
|
305
|
-
const createOAuthCallback = (provider, strategy) => {
|
|
306
|
-
const errorHandler = createOAuthErrorHandler(provider);
|
|
307
|
-
|
|
308
|
-
return [
|
|
309
|
-
(req, res, next) => {
|
|
310
|
-
const isValid = validateOAuthCallback(req, res);
|
|
311
|
-
if (!isValid) return; // Response already sent
|
|
312
|
-
|
|
313
|
-
passport.authenticate(strategy, { session: false }, (err, user, info) => {
|
|
314
|
-
if (err) {
|
|
315
|
-
console.error(`[mbkauthe] ${provider} authentication error:`, err);
|
|
316
|
-
const errorData = errorHandler(err);
|
|
317
|
-
renderError(res, req, errorData);
|
|
318
|
-
return; // Stop execution after sending error
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!user) {
|
|
322
|
-
console.error(`[mbkauthe] ${provider} callback: No user data received`);
|
|
323
|
-
renderError(res, req, {
|
|
324
|
-
code: 401,
|
|
325
|
-
error: 'Authentication Failed',
|
|
326
|
-
message: `${provider} authentication failed. Please try again.`,
|
|
282
|
+
const state = req.query.state;
|
|
283
|
+
const sessionCsrfToken = req.session.oauthCsrfToken;
|
|
284
|
+
|
|
285
|
+
if (!state || !sessionCsrfToken || state !== sessionCsrfToken) {
|
|
286
|
+
console.warn('[mbkauthe] OAuth CSRF token mismatch - possible CSRF attack');
|
|
287
|
+
delete req.session.oauthCsrfToken;
|
|
288
|
+
renderError(res, req, {
|
|
289
|
+
code: 403,
|
|
290
|
+
error: 'Invalid Request',
|
|
291
|
+
message: 'Authentication security validation failed. Please try again.',
|
|
327
292
|
page: '/mbkauthe/login',
|
|
328
293
|
pagename: 'Login'
|
|
329
|
-
});
|
|
330
|
-
return; // Stop execution after sending error
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
req.user = user;
|
|
334
|
-
next();
|
|
335
|
-
})(req, res, next);
|
|
336
|
-
},
|
|
337
|
-
async (req, res) => {
|
|
338
|
-
try {
|
|
339
|
-
const oauthUser = req.user;
|
|
340
|
-
const userQuery = `
|
|
341
|
-
SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps",
|
|
342
|
-
tfa."TwoFAStatus"
|
|
343
|
-
FROM "Users" u
|
|
344
|
-
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
345
|
-
WHERE u."UserName" = $1
|
|
346
|
-
`;
|
|
347
|
-
|
|
348
|
-
const userResult = await dblogin.query({
|
|
349
|
-
name: `${provider.toLowerCase()}-callback-get-user`,
|
|
350
|
-
text: userQuery,
|
|
351
|
-
values: [oauthUser.username]
|
|
352
294
|
});
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
delete req.session.oauthCsrfToken;
|
|
299
|
+
return true;
|
|
300
|
+
};
|
|
353
301
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
302
|
+
const finishProviderLogin = async (req, res, provider, username, detailValue = '') => {
|
|
303
|
+
const userQuery = `
|
|
304
|
+
SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps",
|
|
305
|
+
tfa."TwoFAStatus"
|
|
306
|
+
FROM "Users" u
|
|
307
|
+
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
308
|
+
WHERE u."UserName" = $1
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
const userResult = await dblogin.query({
|
|
312
|
+
name: `${provider.toLowerCase()}-callback-get-user`,
|
|
313
|
+
text: userQuery,
|
|
314
|
+
values: [username]
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (userResult.rows.length === 0) {
|
|
318
|
+
console.error(`[mbkauthe] ${provider} login: User not found: ${username}`);
|
|
319
|
+
return renderError(res, req, {
|
|
357
320
|
code: 404,
|
|
358
321
|
error: 'User Not Found',
|
|
359
322
|
message: `Your ${provider} account is linked, but the user account no longer exists in our system.`,
|
|
360
323
|
page: '/mbkauthe/login',
|
|
361
324
|
pagename: 'Login',
|
|
362
|
-
details: `${provider}
|
|
363
|
-
|
|
364
|
-
|
|
325
|
+
details: `${provider} identifier: ${detailValue}\nPlease contact your administrator.`
|
|
326
|
+
});
|
|
327
|
+
}
|
|
365
328
|
|
|
366
|
-
|
|
329
|
+
const user = userResult.rows[0];
|
|
367
330
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
331
|
+
// Check for trusted device
|
|
332
|
+
const trustedDeviceUser = await checkTrustedDevice(req, user.UserName);
|
|
333
|
+
if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
334
|
+
console.log(`[mbkauthe] ${provider} trusted device login for user: ${user.UserName}, skipping 2FA only`);
|
|
335
|
+
return await handleOAuthRedirect(req, res, user, 'trusted', provider.toLowerCase());
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check 2FA if enabled
|
|
339
|
+
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
340
|
+
const oauthRedirect = req.session.oauthRedirect;
|
|
341
|
+
if (oauthRedirect) delete req.session.oauthRedirect;
|
|
342
|
+
req.session.preAuthUser = {
|
|
379
343
|
id: user.id,
|
|
380
344
|
username: user.UserName,
|
|
381
345
|
role: user.Role,
|
|
382
346
|
allowedApps: user.AllowedApps,
|
|
383
347
|
loginMethod: provider.toLowerCase(),
|
|
384
348
|
redirectUrl: oauthRedirect || null
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
349
|
+
};
|
|
350
|
+
console.log(`[mbkauthe] ${provider} login: 2FA required for user: ${username}`);
|
|
351
|
+
return res.redirect('/mbkauthe/2fa');
|
|
352
|
+
}
|
|
389
353
|
|
|
390
|
-
|
|
391
|
-
|
|
354
|
+
// Complete login process
|
|
355
|
+
await handleOAuthRedirect(req, res, user, 'complete', provider.toLowerCase());
|
|
356
|
+
};
|
|
392
357
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
358
|
+
const createOAuthCallback = (provider, strategy) => {
|
|
359
|
+
const errorHandler = createOAuthErrorHandler(provider);
|
|
360
|
+
|
|
361
|
+
return [
|
|
362
|
+
(req, res, next) => {
|
|
363
|
+
const isValid = validateOAuthCallback(req, res);
|
|
364
|
+
if (!isValid) return;
|
|
365
|
+
|
|
366
|
+
passport.authenticate(strategy, { session: false }, (err, user) => {
|
|
367
|
+
if (err) {
|
|
368
|
+
console.error(`[mbkauthe] ${provider} authentication error:`, err);
|
|
369
|
+
const errorData = errorHandler(err);
|
|
370
|
+
renderError(res, req, errorData);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!user) {
|
|
375
|
+
console.error(`[mbkauthe] ${provider} callback: No user data received`);
|
|
376
|
+
renderError(res, req, {
|
|
377
|
+
code: 401,
|
|
378
|
+
error: 'Authentication Failed',
|
|
379
|
+
message: `${provider} authentication failed. Please try again.`,
|
|
380
|
+
page: '/mbkauthe/login',
|
|
381
|
+
pagename: 'Login'
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
req.user = user;
|
|
387
|
+
next();
|
|
388
|
+
})(req, res, next);
|
|
389
|
+
},
|
|
390
|
+
async (req, res) => {
|
|
391
|
+
try {
|
|
392
|
+
const oauthUser = req.user;
|
|
393
|
+
await finishProviderLogin(
|
|
394
|
+
req,
|
|
395
|
+
res,
|
|
396
|
+
provider,
|
|
397
|
+
oauthUser.username,
|
|
398
|
+
provider === 'GitHub' ? (oauthUser.githubUsername || oauthUser.username) : (oauthUser.googleEmail || oauthUser.username)
|
|
399
|
+
);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.error(`[mbkauthe] ${provider} login callback error:`, err);
|
|
402
|
+
return renderError(res, req, {
|
|
403
|
+
code: 500,
|
|
404
|
+
error: 'Internal Server Error',
|
|
405
|
+
message: `An error occurred during ${provider} authentication. Please try again.`,
|
|
406
|
+
page: '/mbkauthe/login',
|
|
407
|
+
pagename: 'Login'
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
];
|
|
405
412
|
};
|
|
406
413
|
|
|
407
414
|
// Helper function to handle OAuth redirect flow
|
|
408
415
|
const handleOAuthRedirect = async (req, res, user, type, method = null) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const oauthRedirect = req.session.oauthRedirect;
|
|
417
|
-
delete req.session.oauthRedirect;
|
|
418
|
-
|
|
419
|
-
// Custom response handler for OAuth flow
|
|
420
|
-
const originalJson = res.json.bind(res);
|
|
421
|
-
const originalStatus = res.status.bind(res);
|
|
422
|
-
let statusCode = 200;
|
|
423
|
-
|
|
424
|
-
res.status = function (code) {
|
|
425
|
-
statusCode = code;
|
|
426
|
-
return originalStatus(code);
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
res.json = function (data) {
|
|
430
|
-
if (data.success && statusCode === 200) {
|
|
431
|
-
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
432
|
-
console.log(`[mbkauthe] OAuth ${type} login: Redirecting to ${redirectUrl}`);
|
|
433
|
-
res.json = originalJson;
|
|
434
|
-
res.status = originalStatus;
|
|
435
|
-
return res.redirect(redirectUrl);
|
|
436
|
-
}
|
|
437
|
-
res.json = originalJson;
|
|
438
|
-
res.status = originalStatus;
|
|
439
|
-
return originalJson(data);
|
|
440
|
-
};
|
|
416
|
+
const userForSession = {
|
|
417
|
+
id: user.id,
|
|
418
|
+
username: user.UserName,
|
|
419
|
+
role: user.Role,
|
|
420
|
+
allowedApps: user.AllowedApps,
|
|
421
|
+
};
|
|
441
422
|
|
|
442
|
-
|
|
423
|
+
const oauthRedirect = req.session.oauthRedirect;
|
|
424
|
+
delete req.session.oauthRedirect;
|
|
425
|
+
|
|
426
|
+
// Custom response handler for OAuth flow
|
|
427
|
+
const originalJson = res.json.bind(res);
|
|
428
|
+
const originalStatus = res.status.bind(res);
|
|
429
|
+
let statusCode = 200;
|
|
430
|
+
|
|
431
|
+
res.status = function (code) {
|
|
432
|
+
statusCode = code;
|
|
433
|
+
return originalStatus(code);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
res.json = function (data) {
|
|
437
|
+
if (data.success && statusCode === 200) {
|
|
438
|
+
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
439
|
+
console.log(`[mbkauthe] ${method || 'social'} ${type} login: Redirecting to ${redirectUrl}`);
|
|
440
|
+
res.json = originalJson;
|
|
441
|
+
res.status = originalStatus;
|
|
442
|
+
return res.redirect(redirectUrl);
|
|
443
|
+
}
|
|
444
|
+
res.json = originalJson;
|
|
445
|
+
res.status = originalStatus;
|
|
446
|
+
return originalJson(data);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
return await completeLoginProcess(req, res, userForSession, null, false, method);
|
|
443
450
|
};
|
|
444
451
|
|
|
445
|
-
// GitHub login
|
|
452
|
+
// GitHub App user login (OAuth authorize endpoint with app client credentials)
|
|
446
453
|
router.get('/api/github/login', GitHubOAuthLimit, csrfProtection,
|
|
447
|
-
|
|
454
|
+
createOAuthInitiation('GitHub', mbkautheVar.GITHUB_LOGIN_ENABLED, githubClientId, githubClientSecret)
|
|
448
455
|
);
|
|
449
456
|
|
|
450
|
-
// Google login initiation
|
|
457
|
+
// Google login initiation
|
|
451
458
|
router.get('/api/google/login', GoogleOAuthLimit, csrfProtection,
|
|
452
|
-
|
|
459
|
+
createOAuthInitiation('Google', mbkautheVar.GOOGLE_LOGIN_ENABLED, mbkautheVar.GOOGLE_CLIENT_ID, mbkautheVar.GOOGLE_CLIENT_SECRET)
|
|
453
460
|
);
|
|
454
461
|
|
|
455
|
-
|
|
456
|
-
// OAuth callback routes
|
|
462
|
+
// Callback routes
|
|
457
463
|
router.get('/api/github/login/callback', GitHubOAuthLimit, ...createOAuthCallback('GitHub', 'github-login'));
|
|
458
464
|
router.get('/api/google/login/callback', GoogleOAuthLimit, ...createOAuthCallback('Google', 'google-login'));
|
|
459
465
|
|