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