mbkauthe 2.3.0 → 2.5.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/.env.example +1 -1
- package/README.md +11 -4
- package/docs/db.md +35 -5
- package/docs/env.md +32 -0
- package/lib/config.js +18 -2
- package/lib/main.js +216 -112
- package/lib/validateSessionAndRole.js +22 -2
- package/package.json +2 -2
- package/views/2fa.handlebars +1 -1
- package/views/info.handlebars +4 -0
- package/views/loginmbkauthe.handlebars +10 -33
- package/views/sharedStyles.handlebars +6 -7
- package/views/showmessage.handlebars +3 -12
package/.env.example
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
mbkautheVar={"APP_NAME":"mbkauthe","Main_SECRET_TOKEN": 123,"SESSION_SECRET_KEY":"123","IS_DEPLOYED":"true","LOGIN_DB":"postgres://","MBKAUTH_TWO_FA_ENABLE":"true","COOKIE_EXPIRE_TIME":2,"DOMAIN":"mbktech.org","loginRedirectURL":"/mbkauthe/test","GITHUB_LOGIN_ENABLED":"true","GITHUB_CLIENT_ID":"","GITHUB_CLIENT_SECRET":"","DEVICE_TRUST_DURATION_DAYS":7}
|
|
1
|
+
mbkautheVar={"APP_NAME":"mbkauthe","Main_SECRET_TOKEN": 123,"SESSION_SECRET_KEY":"123","IS_DEPLOYED":"true","LOGIN_DB":"postgres://","MBKAUTH_TWO_FA_ENABLE":"true","COOKIE_EXPIRE_TIME":2,"DOMAIN":"mbktech.org","loginRedirectURL":"/mbkauthe/test","GITHUB_LOGIN_ENABLED":"true","GITHUB_CLIENT_ID":"","GITHUB_CLIENT_SECRET":"","DEVICE_TRUST_DURATION_DAYS":7,"EncPass":"false"}
|
|
2
2
|
|
|
3
3
|
# See docs/env.md for more details
|
package/README.md
CHANGED
|
@@ -6,11 +6,21 @@
|
|
|
6
6
|
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml)
|
|
7
7
|
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/codeql.yml)
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img height="64px" src="./public/icon.svg" alt="MBK Chat Platform" />
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="https://skillicons.dev/icons?i=nodejs,express,postgres" />
|
|
16
|
+
<img height="48px" src="https://handlebarsjs.com/handlebars-icon.svg" alt="Handlebars" />
|
|
17
|
+
</p>
|
|
18
|
+
|
|
9
19
|
**MBKAuth** is a reusable, production-ready authentication system for Node.js applications built by MBKTech.org. It provides secure session management, two-factor authentication (2FA), role-based access control, and multi-application support out of the box.
|
|
10
20
|
|
|
11
21
|
## ✨ Features
|
|
12
22
|
|
|
13
|
-
- 🔐 **Secure Authentication** -
|
|
23
|
+
- 🔐 **Secure Authentication** - Configurable password encryption (PBKDF2) or raw password support
|
|
14
24
|
- 🔑 **Session Management** - PostgreSQL-backed session storage
|
|
15
25
|
- 📱 **Two-Factor Authentication (2FA)** - Optional TOTP-based 2FA with speakeasy
|
|
16
26
|
- 🔄 **GitHub OAuth Integration** - Login with GitHub accounts (passport-github2)
|
|
@@ -199,9 +209,6 @@ MBKAuth automatically adds these routes to your app:
|
|
|
199
209
|
### CSRF Protection
|
|
200
210
|
All POST routes are protected with CSRF tokens. CSRF tokens are automatically included in rendered forms.
|
|
201
211
|
|
|
202
|
-
### Password Hashing
|
|
203
|
-
Passwords are hashed using bcrypt with a secure salt. Set `EncryptedPassword: "true"` in `mbkautheVar` to enable.
|
|
204
|
-
|
|
205
212
|
### Secure Cookies
|
|
206
213
|
- `httpOnly` flag prevents XSS attacks
|
|
207
214
|
- `sameSite: 'lax'` prevents CSRF attacks
|
package/docs/db.md
CHANGED
|
@@ -138,13 +138,14 @@ The GitHub login feature is now fully integrated into your mbkauthe system and r
|
|
|
138
138
|
|
|
139
139
|
- `id` (INTEGER, auto-increment, primary key): Unique identifier for each user.
|
|
140
140
|
- `UserName` (TEXT): The username of the user.
|
|
141
|
-
- `Password` (TEXT): The
|
|
141
|
+
- `Password` (TEXT): The raw password of the user (used when EncPass=false).
|
|
142
|
+
- `PasswordEnc` (TEXT): The encrypted/hashed password of the user (used when EncPass=true).
|
|
142
143
|
- `Role` (ENUM): The role of the user. Possible values: `SuperAdmin`, `NormalUser`, `Guest`.
|
|
143
144
|
- `Active` (BOOLEAN): Indicates whether the user account is active.
|
|
144
145
|
- `HaveMailAccount` (BOOLEAN)(optional): Indicates if the user has a linked mail account.
|
|
145
146
|
- `SessionId` (TEXT): The session ID associated with the user.
|
|
146
147
|
- `GuestRole` (JSONB): Stores additional guest-specific role information in binary JSON format.
|
|
147
|
-
- `AllowedApps`(JSONB):
|
|
148
|
+
- `AllowedApps`(JSONB): Array of applications the user is authorized to access.
|
|
148
149
|
|
|
149
150
|
- **Schema:**
|
|
150
151
|
```sql
|
|
@@ -153,7 +154,8 @@ CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
|
|
|
153
154
|
CREATE TABLE "Users" (
|
|
154
155
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
155
156
|
"UserName" VARCHAR(50) NOT NULL UNIQUE,
|
|
156
|
-
"Password" VARCHAR(61)
|
|
157
|
+
"Password" VARCHAR(61), -- For raw passwords (when EncPass=false)
|
|
158
|
+
"PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
|
|
157
159
|
"Role" role DEFAULT 'NormalUser' NOT NULL,
|
|
158
160
|
"Active" BOOLEAN DEFAULT FALSE,
|
|
159
161
|
"HaveMailAccount" BOOLEAN DEFAULT FALSE,
|
|
@@ -173,6 +175,12 @@ CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" (last_login);
|
|
|
173
175
|
CREATE INDEX IF NOT EXISTS idx_users_id_sessionid_active_role ON "Users" ("id", LOWER("SessionId"), "Active", "Role");
|
|
174
176
|
```
|
|
175
177
|
|
|
178
|
+
**Password Storage Notes:**
|
|
179
|
+
- When `EncPass=false` (default): The system uses the `Password` column to store and validate raw passwords
|
|
180
|
+
- When `EncPass=true` (recommended for production): The system uses the `PasswordEnc` column to store hashed passwords using PBKDF2 with the username as salt
|
|
181
|
+
- Only one password column should be populated based on your EncPass configuration
|
|
182
|
+
- The PasswordEnc field stores 128-character hex strings when using PBKDF2 hashing
|
|
183
|
+
|
|
176
184
|
### Session Table
|
|
177
185
|
|
|
178
186
|
- **Columns:**
|
|
@@ -255,6 +263,7 @@ CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices"("Expi
|
|
|
255
263
|
|
|
256
264
|
To add new users to the `Users` table, use the following SQL queries:
|
|
257
265
|
|
|
266
|
+
**For Raw Password Storage (EncPass=false):**
|
|
258
267
|
```sql
|
|
259
268
|
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
|
|
260
269
|
VALUES ('support', '12345678', 'SuperAdmin', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
@@ -263,8 +272,29 @@ To add new users to the `Users` table, use the following SQL queries:
|
|
|
263
272
|
VALUES ('test', '12345678', 'NormalUser', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
264
273
|
```
|
|
265
274
|
|
|
275
|
+
**For Encrypted Password Storage (EncPass=true):**
|
|
276
|
+
```sql
|
|
277
|
+
-- Note: You'll need to hash the password using the hashPassword function
|
|
278
|
+
-- Example with pre-hashed password (PBKDF2 with username as salt)
|
|
279
|
+
INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
|
|
280
|
+
VALUES ('support', 'your_hashed_password_here', 'SuperAdmin', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
281
|
+
|
|
282
|
+
INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
|
|
283
|
+
VALUES ('test', 'your_hashed_password_here', 'NormalUser', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Configuration Notes:**
|
|
266
287
|
- Replace `support` and `test` with the desired usernames.
|
|
267
|
-
- Replace `12345678` with the actual passwords.
|
|
288
|
+
- For raw passwords: Replace `12345678` with the actual plain text passwords.
|
|
289
|
+
- For encrypted passwords: Use the hashPassword function to generate the hash before inserting.
|
|
268
290
|
- Adjust the `Role` values as needed (`SuperAdmin`, `NormalUser`, or `Guest`).
|
|
269
291
|
- Modify the `Active` and `HaveMailAccount` values as required.
|
|
270
|
-
- Update the `GuestRole` JSON object if specific permissions are required(this functionality is under construction).
|
|
292
|
+
- Update the `GuestRole` JSON object if specific permissions are required (this functionality is under construction).
|
|
293
|
+
|
|
294
|
+
**Generating Encrypted Passwords:**
|
|
295
|
+
If you're using `EncPass=true`, you can generate encrypted passwords using the hashPassword function:
|
|
296
|
+
```javascript
|
|
297
|
+
import { hashPassword } from './lib/config.js';
|
|
298
|
+
const encryptedPassword = hashPassword('12345678', 'support');
|
|
299
|
+
console.log(encryptedPassword); // Use this value for PasswordEnc column
|
|
300
|
+
```
|
package/docs/env.md
CHANGED
|
@@ -29,6 +29,7 @@ Main_SECRET_TOKEN=your-secure-token-number
|
|
|
29
29
|
SESSION_SECRET_KEY=your-secure-random-key-here
|
|
30
30
|
IS_DEPLOYED=false
|
|
31
31
|
DOMAIN=localhost
|
|
32
|
+
EncPass=false
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
#### Main_SECRET_TOKEN
|
|
@@ -80,6 +81,35 @@ DOMAIN=localhost
|
|
|
80
81
|
IS_DEPLOYED=false
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
#### EncPass
|
|
85
|
+
**Description:** Controls whether passwords are stored and validated in encrypted format.
|
|
86
|
+
|
|
87
|
+
**Values:**
|
|
88
|
+
- `true` - Use encrypted password validation
|
|
89
|
+
- Passwords are hashed using PBKDF2 with the username as salt
|
|
90
|
+
- Compares against `PasswordEnc` column in Users table
|
|
91
|
+
- `false` - Use raw password validation (default)
|
|
92
|
+
- Passwords are stored and compared in plain text
|
|
93
|
+
- Compares against `Password` column in Users table
|
|
94
|
+
|
|
95
|
+
**Default:** `false`
|
|
96
|
+
|
|
97
|
+
**Security Note:** Setting `EncPass=true` is recommended for production environments as it provides better security by storing hashed passwords instead of plain text.
|
|
98
|
+
|
|
99
|
+
**Examples:**
|
|
100
|
+
```env
|
|
101
|
+
# Production (recommended)
|
|
102
|
+
EncPass=true
|
|
103
|
+
|
|
104
|
+
# Development
|
|
105
|
+
EncPass=false
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Database Implications:**
|
|
109
|
+
- When `EncPass=true`: The system uses the `PasswordEnc` column
|
|
110
|
+
- When `EncPass=false`: The system uses the `Password` column
|
|
111
|
+
- Ensure your database schema includes the appropriate column based on your configuration
|
|
112
|
+
|
|
83
113
|
---
|
|
84
114
|
|
|
85
115
|
## 🗄️ Database Configuration
|
|
@@ -278,6 +308,7 @@ Main_SECRET_TOKEN=dev-token-123
|
|
|
278
308
|
SESSION_SECRET_KEY=dev-secret-key-change-in-production
|
|
279
309
|
IS_DEPLOYED=false
|
|
280
310
|
DOMAIN=localhost
|
|
311
|
+
EncPass=false
|
|
281
312
|
LOGIN_DB=postgresql://admin:password@localhost:5432/mbkauth_dev
|
|
282
313
|
MBKAUTH_TWO_FA_ENABLE=false
|
|
283
314
|
COOKIE_EXPIRE_TIME=7
|
|
@@ -296,6 +327,7 @@ Main_SECRET_TOKEN=your-secure-production-token
|
|
|
296
327
|
SESSION_SECRET_KEY=your-super-secure-production-key-here
|
|
297
328
|
IS_DEPLOYED=true
|
|
298
329
|
DOMAIN=yourdomain.com
|
|
330
|
+
EncPass=true
|
|
299
331
|
LOGIN_DB=postgresql://dbuser:securepass@prod-db.example.com:5432/mbkauth_prod
|
|
300
332
|
MBKAUTH_TWO_FA_ENABLE=true
|
|
301
333
|
COOKIE_EXPIRE_TIME=2
|
package/lib/config.js
CHANGED
|
@@ -56,6 +56,11 @@ function validateConfiguration() {
|
|
|
56
56
|
errors.push("mbkautheVar.GITHUB_LOGIN_ENABLED must be either 'true' or 'false' or 'f'");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Validate EncPass value if provided
|
|
60
|
+
if (mbkautheVar.EncPass && !['true', 'false', 'f'].includes(mbkautheVar.EncPass.toLowerCase())) {
|
|
61
|
+
errors.push("mbkautheVar.EncPass must be either 'true' or 'false' or 'f'");
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
// Validate GitHub login configuration
|
|
60
65
|
if (mbkautheVar.GITHUB_LOGIN_ENABLED === "true") {
|
|
61
66
|
if (!mbkautheVar.GITHUB_CLIENT_ID || mbkautheVar.GITHUB_CLIENT_ID.trim() === '') {
|
|
@@ -156,6 +161,9 @@ try {
|
|
|
156
161
|
packageJson = require("../package.json");
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
// Parent project version
|
|
165
|
+
const appVersion = require("../package.json") ?.version || "unknown";
|
|
166
|
+
|
|
159
167
|
// Helper function to render error pages consistently
|
|
160
168
|
const renderError = (res, { code, error, message, page, pagename, details }) => {
|
|
161
169
|
res.status(code);
|
|
@@ -184,6 +192,12 @@ const clearSessionCookies = (res) => {
|
|
|
184
192
|
res.clearCookie("device_token", cachedClearCookieOptions);
|
|
185
193
|
};
|
|
186
194
|
|
|
195
|
+
const hashPassword = (password, username) => {
|
|
196
|
+
const salt = username;
|
|
197
|
+
// 128 characters returned
|
|
198
|
+
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
|
|
199
|
+
};
|
|
200
|
+
|
|
187
201
|
export {
|
|
188
202
|
mbkautheVar,
|
|
189
203
|
getCookieOptions,
|
|
@@ -193,8 +207,10 @@ export {
|
|
|
193
207
|
renderError,
|
|
194
208
|
clearSessionCookies,
|
|
195
209
|
packageJson,
|
|
210
|
+
appVersion,
|
|
196
211
|
DEVICE_TRUST_DURATION_DAYS,
|
|
197
212
|
DEVICE_TRUST_DURATION_MS,
|
|
198
213
|
generateDeviceToken,
|
|
199
|
-
getDeviceTokenCookieOptions
|
|
200
|
-
|
|
214
|
+
getDeviceTokenCookieOptions,
|
|
215
|
+
hashPassword
|
|
216
|
+
};
|
package/lib/main.js
CHANGED
|
@@ -8,7 +8,6 @@ import { dblogin } from "./pool.js";
|
|
|
8
8
|
import { authenticate, validateSession, validateSessionAndRole } from "./validateSessionAndRole.js";
|
|
9
9
|
import fetch from 'node-fetch';
|
|
10
10
|
import cookieParser from "cookie-parser";
|
|
11
|
-
import bcrypt from 'bcrypt';
|
|
12
11
|
import rateLimit from 'express-rate-limit';
|
|
13
12
|
import speakeasy from "speakeasy";
|
|
14
13
|
import passport from 'passport';
|
|
@@ -17,7 +16,11 @@ import GitHubStrategy from 'passport-github2';
|
|
|
17
16
|
import { fileURLToPath } from "url";
|
|
18
17
|
import fs from "fs";
|
|
19
18
|
import path from "path";
|
|
20
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, appVersion,
|
|
21
|
+
renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS,
|
|
22
|
+
hashPassword
|
|
23
|
+
} from "./config.js";
|
|
21
24
|
|
|
22
25
|
const router = express.Router();
|
|
23
26
|
|
|
@@ -37,7 +40,7 @@ router.get('/icon.svg', (req, res) => {
|
|
|
37
40
|
res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
|
|
38
41
|
});
|
|
39
42
|
|
|
40
|
-
router.get(['/favicon.ico','/icon.ico'], (req, res) => {
|
|
43
|
+
router.get(['/favicon.ico', '/icon.ico'], (req, res) => {
|
|
41
44
|
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
42
45
|
res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
|
|
43
46
|
});
|
|
@@ -133,14 +136,14 @@ router.use(async (req, res, next) => {
|
|
|
133
136
|
// Only restore session if not already present and sessionId cookie exists
|
|
134
137
|
if (!req.session.user && req.cookies.sessionId) {
|
|
135
138
|
const sessionId = req.cookies.sessionId;
|
|
136
|
-
|
|
139
|
+
|
|
137
140
|
// Early validation to avoid unnecessary processing
|
|
138
141
|
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
139
142
|
// Clear invalid cookie to prevent repeated attempts
|
|
140
143
|
res.clearCookie('sessionId', cachedClearCookieOptions);
|
|
141
144
|
return next();
|
|
142
145
|
}
|
|
143
|
-
|
|
146
|
+
|
|
144
147
|
try {
|
|
145
148
|
|
|
146
149
|
const normalizedSessionId = sessionId.toLowerCase();
|
|
@@ -170,16 +173,93 @@ router.use(async (req, res, next) => {
|
|
|
170
173
|
router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
|
|
171
174
|
if (req.session?.user) {
|
|
172
175
|
return res.send(`
|
|
173
|
-
<head>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
<head>
|
|
177
|
+
<script src="/mbkauthe/main.js"></script>
|
|
178
|
+
<style>
|
|
179
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background: #f5f5f5; }
|
|
180
|
+
.card { background: white; border-radius: 8px; padding: 25px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
181
|
+
.success { color: #16a085; border-left: 4px solid #16a085; padding-left: 15px; }
|
|
182
|
+
.user-info { background: #ecf0f1; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 14px; }
|
|
183
|
+
button { background: #e74c3c; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; margin: 10px 5px; }
|
|
184
|
+
button:hover { background: #c0392b; }
|
|
185
|
+
a { color: #3498db; text-decoration: none; margin: 0 10px; padding: 8px 12px; border: 1px solid #3498db; border-radius: 4px; display: inline-block; }
|
|
186
|
+
a:hover { background: #3498db; color: white; }
|
|
187
|
+
</style>
|
|
188
|
+
</head>
|
|
189
|
+
<div class="card">
|
|
190
|
+
<p class="success">✅ Authentication successful! User is logged in.</p>
|
|
191
|
+
<p>Welcome, <strong>${req.session.user.username}</strong>! Your role: <strong>${req.session.user.role}</strong></p>
|
|
192
|
+
<div class="user-info">
|
|
193
|
+
User ID: ${req.session.user.id}<br>
|
|
194
|
+
Session ID: ${req.session.user.sessionId.slice(0, 5)}...
|
|
195
|
+
</div>
|
|
196
|
+
<button onclick="logout()">Logout</button>
|
|
197
|
+
<a href="/mbkauthe/info">Info Page</a>
|
|
198
|
+
<a href="/mbkauthe/login">Login Page</a>
|
|
199
|
+
</div>
|
|
179
200
|
`);
|
|
180
201
|
}
|
|
181
202
|
});
|
|
182
203
|
|
|
204
|
+
async function checkTrustedDevice(req, username) {
|
|
205
|
+
const deviceToken = req.cookies.device_token;
|
|
206
|
+
|
|
207
|
+
if (!deviceToken || typeof deviceToken !== 'string') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const deviceQuery = `
|
|
213
|
+
SELECT td."UserName", td."LastUsed", td."ExpiresAt", u."id", u."Active", u."Role", u."AllowedApps"
|
|
214
|
+
FROM "TrustedDevices" td
|
|
215
|
+
JOIN "Users" u ON td."UserName" = u."UserName"
|
|
216
|
+
WHERE td."DeviceToken" = $1 AND td."UserName" = $2 AND td."ExpiresAt" > NOW()
|
|
217
|
+
`;
|
|
218
|
+
const deviceResult = await dblogin.query({
|
|
219
|
+
name: 'check-trusted-device',
|
|
220
|
+
text: deviceQuery,
|
|
221
|
+
values: [deviceToken, username]
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (deviceResult.rows.length > 0) {
|
|
225
|
+
const deviceUser = deviceResult.rows[0];
|
|
226
|
+
|
|
227
|
+
if (!deviceUser.Active) {
|
|
228
|
+
console.log(`[mbkauthe] Trusted device check: inactive account for username: ${username}`);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (deviceUser.Role !== "SuperAdmin") {
|
|
233
|
+
const allowedApps = deviceUser.AllowedApps;
|
|
234
|
+
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
235
|
+
console.warn(`[mbkauthe] Trusted device check: User "${username}" is not authorized to use the application "${mbkautheVar.APP_NAME}"`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update last used timestamp
|
|
241
|
+
await dblogin.query({
|
|
242
|
+
name: 'update-device-last-used',
|
|
243
|
+
text: 'UPDATE "TrustedDevices" SET "LastUsed" = NOW() WHERE "DeviceToken" = $1',
|
|
244
|
+
values: [deviceToken]
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log(`[mbkauthe] Trusted device validated for user: ${username}`);
|
|
248
|
+
return {
|
|
249
|
+
id: deviceUser.id,
|
|
250
|
+
username: username,
|
|
251
|
+
role: deviceUser.Role,
|
|
252
|
+
Role: deviceUser.Role,
|
|
253
|
+
allowedApps: deviceUser.AllowedApps,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
} catch (deviceErr) {
|
|
257
|
+
console.error("[mbkauthe] Error checking trusted device:", deviceErr);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
183
263
|
async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
|
|
184
264
|
try {
|
|
185
265
|
// Ensure both username formats are available for compatibility
|
|
@@ -209,10 +289,10 @@ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDev
|
|
|
209
289
|
text: 'DELETE FROM "session" WHERE (sess->\'user\'->>\'id\')::int = $1',
|
|
210
290
|
values: [user.id]
|
|
211
291
|
}),
|
|
212
|
-
// Update session ID in Users table
|
|
292
|
+
// Update session ID and last login time in Users table
|
|
213
293
|
dblogin.query({
|
|
214
|
-
name: 'login-update-session-
|
|
215
|
-
text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`,
|
|
294
|
+
name: 'login-update-session-and-last-login',
|
|
295
|
+
text: `UPDATE "Users" SET "SessionId" = $1, "last_login" = NOW() WHERE "id" = $2`,
|
|
216
296
|
values: [sessionId, user.id]
|
|
217
297
|
})
|
|
218
298
|
]);
|
|
@@ -245,7 +325,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDev
|
|
|
245
325
|
if (trustDevice) {
|
|
246
326
|
try {
|
|
247
327
|
const deviceToken = generateDeviceToken();
|
|
248
|
-
const deviceName = req.headers['user-agent'] ?
|
|
328
|
+
const deviceName = req.headers['user-agent'] ?
|
|
249
329
|
req.headers['user-agent'].substring(0, 255) : 'Unknown Device';
|
|
250
330
|
const userAgent = req.headers['user-agent'] || 'Unknown';
|
|
251
331
|
const ipAddress = req.ip || req.connection.remoteAddress || 'Unknown';
|
|
@@ -266,7 +346,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null, trustDev
|
|
|
266
346
|
}
|
|
267
347
|
}
|
|
268
348
|
|
|
269
|
-
console.log(`[mbkauthe] User "${username}" logged in successfully`);
|
|
349
|
+
console.log(`[mbkauthe] User "${username}" logged in successfully (last_login updated)`);
|
|
270
350
|
|
|
271
351
|
const responsePayload = {
|
|
272
352
|
success: true,
|
|
@@ -328,7 +408,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
|
|
|
328
408
|
router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
329
409
|
console.log("[mbkauthe] Login request received");
|
|
330
410
|
|
|
331
|
-
const { username, password } = req.body;
|
|
411
|
+
const { username, password, redirect } = req.body;
|
|
332
412
|
|
|
333
413
|
// Input validation
|
|
334
414
|
if (!username || !password) {
|
|
@@ -362,79 +442,9 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
362
442
|
const trimmedUsername = username.trim();
|
|
363
443
|
|
|
364
444
|
try {
|
|
365
|
-
// Check for trusted device token first
|
|
366
|
-
const deviceToken = req.cookies.device_token;
|
|
367
|
-
if (deviceToken && typeof deviceToken === 'string') {
|
|
368
|
-
try {
|
|
369
|
-
const deviceQuery = `
|
|
370
|
-
SELECT td."UserName", td."LastUsed", td."ExpiresAt", u."id", u."Password", u."Active", u."Role", u."AllowedApps"
|
|
371
|
-
FROM "TrustedDevices" td
|
|
372
|
-
JOIN "Users" u ON td."UserName" = u."UserName"
|
|
373
|
-
WHERE td."DeviceToken" = $1 AND td."UserName" = $2 AND td."ExpiresAt" > NOW()
|
|
374
|
-
`;
|
|
375
|
-
const deviceResult = await dblogin.query({
|
|
376
|
-
name: 'login-check-trusted-device',
|
|
377
|
-
text: deviceQuery,
|
|
378
|
-
values: [deviceToken, trimmedUsername]
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
if (deviceResult.rows.length > 0) {
|
|
382
|
-
const deviceUser = deviceResult.rows[0];
|
|
383
|
-
|
|
384
|
-
// Validate password even with trusted device
|
|
385
|
-
let passwordValid = false;
|
|
386
|
-
if (mbkautheVar.EncryptedPassword === "true") {
|
|
387
|
-
passwordValid = await bcrypt.compare(password, deviceUser.Password);
|
|
388
|
-
} else {
|
|
389
|
-
passwordValid = deviceUser.Password === password;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (!passwordValid) {
|
|
393
|
-
console.log("[mbkauthe] Login failed: invalid credentials (trusted device)");
|
|
394
|
-
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (!deviceUser.Active) {
|
|
398
|
-
console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
|
|
399
|
-
return res.status(403).json({ success: false, message: "Account is inactive" });
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (deviceUser.Role !== "SuperAdmin") {
|
|
403
|
-
const allowedApps = deviceUser.AllowedApps;
|
|
404
|
-
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
405
|
-
console.warn(`[mbkauthe] User \"${trimmedUsername}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
406
|
-
return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Update last used timestamp
|
|
411
|
-
await dblogin.query({
|
|
412
|
-
name: 'login-update-device-last-used',
|
|
413
|
-
text: 'UPDATE "TrustedDevices" SET "LastUsed" = NOW() WHERE "DeviceToken" = $1',
|
|
414
|
-
values: [deviceToken]
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
console.log(`[mbkauthe] Trusted device login for user: ${trimmedUsername}, skipping 2FA`);
|
|
418
|
-
|
|
419
|
-
// Skip 2FA and complete login
|
|
420
|
-
const userForSession = {
|
|
421
|
-
id: deviceUser.id,
|
|
422
|
-
username: trimmedUsername,
|
|
423
|
-
role: deviceUser.Role,
|
|
424
|
-
Role: deviceUser.Role,
|
|
425
|
-
allowedApps: deviceUser.AllowedApps,
|
|
426
|
-
};
|
|
427
|
-
return await completeLoginProcess(req, res, userForSession);
|
|
428
|
-
}
|
|
429
|
-
} catch (deviceErr) {
|
|
430
|
-
console.error("[mbkauthe] Error checking trusted device:", deviceErr);
|
|
431
|
-
// Continue with normal login flow if device check fails
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
445
|
// Combined query: fetch user data and 2FA status in one query
|
|
436
446
|
const userQuery = `
|
|
437
|
-
SELECT u.id, u."UserName", u."Password", u."Active", u."Role", u."AllowedApps",
|
|
447
|
+
SELECT u.id, u."UserName", u."Password", u."PasswordEnc", u."Active", u."Role", u."AllowedApps",
|
|
438
448
|
tfa."TwoFAStatus"
|
|
439
449
|
FROM "Users" u
|
|
440
450
|
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
@@ -450,30 +460,33 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
450
460
|
const user = userResult.rows[0];
|
|
451
461
|
|
|
452
462
|
// Validate user has password field
|
|
453
|
-
if (!user.Password) {
|
|
463
|
+
if (!user.Password && !user.PasswordEnc) {
|
|
454
464
|
console.error("[mbkauthe] User account has no password set");
|
|
455
465
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
456
466
|
}
|
|
457
467
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
console.error("[mbkauthe] Error comparing password:", err);
|
|
468
|
-
return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` });
|
|
468
|
+
// Check password based on EncPass configuration - ALWAYS validate password first
|
|
469
|
+
let passwordMatches = false;
|
|
470
|
+
console.log(`mbkautheVar.EncPass is set to: ${mbkautheVar.EncPass}`);
|
|
471
|
+
console.log(mbkautheVar.EncPass === "true" || mbkautheVar.EncPass === true);
|
|
472
|
+
if (mbkautheVar.EncPass === "true" || mbkautheVar.EncPass === true) {
|
|
473
|
+
// Use encrypted password comparison
|
|
474
|
+
if (user.PasswordEnc) {
|
|
475
|
+
const hashedInputPassword = hashPassword(password, user.UserName);
|
|
476
|
+
passwordMatches = user.PasswordEnc === hashedInputPassword;
|
|
469
477
|
}
|
|
470
478
|
} else {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
479
|
+
// Use raw password comparison
|
|
480
|
+
if (user.Password) {
|
|
481
|
+
passwordMatches = user.Password === password;
|
|
474
482
|
}
|
|
475
483
|
}
|
|
476
484
|
|
|
485
|
+
if (!passwordMatches) {
|
|
486
|
+
console.log("[mbkauthe] Login failed: invalid credentials");
|
|
487
|
+
return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
|
|
488
|
+
}
|
|
489
|
+
|
|
477
490
|
if (!user.Active) {
|
|
478
491
|
console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
|
|
479
492
|
return res.status(403).json({ success: false, message: "Account is inactive" });
|
|
@@ -481,22 +494,41 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
481
494
|
|
|
482
495
|
if (user.Role !== "SuperAdmin") {
|
|
483
496
|
const allowedApps = user.AllowedApps;
|
|
484
|
-
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
497
|
+
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
485
498
|
console.warn(`[mbkauthe] User \"${user.UserName}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
486
499
|
return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
|
|
487
500
|
}
|
|
488
501
|
}
|
|
489
502
|
|
|
503
|
+
// Check for trusted device AFTER password validation - trusted devices should only skip 2FA, not password
|
|
504
|
+
const trustedDeviceUser = await checkTrustedDevice(req, trimmedUsername);
|
|
505
|
+
if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
506
|
+
console.log(`[mbkauthe] Trusted device login for user: ${trimmedUsername}, skipping 2FA only`);
|
|
507
|
+
|
|
508
|
+
// Skip only 2FA, password was already validated above
|
|
509
|
+
const userForSession = {
|
|
510
|
+
id: user.id,
|
|
511
|
+
username: user.UserName,
|
|
512
|
+
role: user.Role,
|
|
513
|
+
Role: user.Role,
|
|
514
|
+
allowedApps: user.AllowedApps,
|
|
515
|
+
};
|
|
516
|
+
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
517
|
+
return await completeLoginProcess(req, res, userForSession, requestedRedirect);
|
|
518
|
+
}
|
|
519
|
+
|
|
490
520
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
491
521
|
// 2FA is enabled, prompt for token on a separate page
|
|
522
|
+
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
492
523
|
req.session.preAuthUser = {
|
|
493
524
|
id: user.id,
|
|
494
525
|
username: user.UserName,
|
|
495
526
|
role: user.Role,
|
|
496
527
|
Role: user.Role,
|
|
528
|
+
redirectUrl: requestedRedirect
|
|
497
529
|
};
|
|
498
530
|
console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
|
|
499
|
-
return res.json({ success: true, twoFactorRequired: true });
|
|
531
|
+
return res.json({ success: true, twoFactorRequired: true, redirectUrl: requestedRedirect });
|
|
500
532
|
}
|
|
501
533
|
|
|
502
534
|
// If 2FA is not enabled, proceed with login
|
|
@@ -507,7 +539,8 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
507
539
|
Role: user.Role,
|
|
508
540
|
allowedApps: user.AllowedApps,
|
|
509
541
|
};
|
|
510
|
-
|
|
542
|
+
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
543
|
+
await completeLoginProcess(req, res, userForSession, requestedRedirect);
|
|
511
544
|
|
|
512
545
|
} catch (err) {
|
|
513
546
|
console.error("[mbkauthe] Error during login process:", err);
|
|
@@ -519,9 +552,19 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
519
552
|
if (!req.session.preAuthUser) {
|
|
520
553
|
return res.redirect("/mbkauthe/login");
|
|
521
554
|
}
|
|
555
|
+
|
|
556
|
+
// Prefer explicit redirect from query string, else from session preAuthUser redirectUrl, else fallback value
|
|
557
|
+
let redirectFromQuery = req.query && typeof req.query.redirect === 'string' ? req.query.redirect : null;
|
|
558
|
+
let redirectToUse = redirectFromQuery || req.session.preAuthUser.redirectUrl || (mbkautheVar.loginRedirectURL || '/dashboard');
|
|
559
|
+
|
|
560
|
+
// Validate redirectToUse to prevent open redirect attacks
|
|
561
|
+
if (redirectToUse && !(typeof redirectToUse === 'string' && redirectToUse.startsWith('/') && !redirectToUse.startsWith('//'))) {
|
|
562
|
+
redirectToUse = mbkautheVar.loginRedirectURL || '/dashboard';
|
|
563
|
+
}
|
|
564
|
+
|
|
522
565
|
res.render("2fa.handlebars", {
|
|
523
566
|
layout: false,
|
|
524
|
-
customURL:
|
|
567
|
+
customURL: redirectToUse,
|
|
525
568
|
csrfToken: req.csrfToken(),
|
|
526
569
|
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
527
570
|
DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
|
|
@@ -574,7 +617,14 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
574
617
|
|
|
575
618
|
// 2FA successful, complete login with optional device trust
|
|
576
619
|
const userForSession = { id, username, role, allowedApps };
|
|
577
|
-
|
|
620
|
+
// Prefer redirect stored in preAuthUser or in query/body, fallback to configured default
|
|
621
|
+
let redirectFromSession = req.session.preAuthUser && req.session.preAuthUser.redirectUrl ? req.session.preAuthUser.redirectUrl : null;
|
|
622
|
+
if (redirectFromSession && (!(typeof redirectFromSession === 'string') || !redirectFromSession.startsWith('/') || redirectFromSession.startsWith('//'))) {
|
|
623
|
+
redirectFromSession = null;
|
|
624
|
+
}
|
|
625
|
+
const redirectUrl = redirectFromSession || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
626
|
+
// Clear preAuthUser after successful login
|
|
627
|
+
if (req.session.preAuthUser) delete req.session.preAuthUser;
|
|
578
628
|
await completeLoginProcess(req, res, userForSession, redirectUrl, shouldTrustDevice);
|
|
579
629
|
|
|
580
630
|
} catch (err) {
|
|
@@ -592,13 +642,13 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
|
592
642
|
const operations = [
|
|
593
643
|
dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
|
|
594
644
|
];
|
|
595
|
-
|
|
645
|
+
|
|
596
646
|
if (req.sessionID) {
|
|
597
647
|
operations.push(
|
|
598
648
|
dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
|
|
599
649
|
);
|
|
600
650
|
}
|
|
601
|
-
|
|
651
|
+
|
|
602
652
|
await Promise.all(operations);
|
|
603
653
|
|
|
604
654
|
req.session.destroy((err) => {
|
|
@@ -670,6 +720,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
|
|
|
670
720
|
layout: false,
|
|
671
721
|
mbkautheVar: mbkautheVar,
|
|
672
722
|
version: packageJson.version,
|
|
723
|
+
APP_VERSION: appVersion,
|
|
673
724
|
latestVersion,
|
|
674
725
|
authorized: authorized,
|
|
675
726
|
});
|
|
@@ -724,7 +775,7 @@ passport.use('github-login', new GitHubStrategy({
|
|
|
724
775
|
// Check if user is authorized for this app (same logic as regular login)
|
|
725
776
|
if (user.Role !== "SuperAdmin") {
|
|
726
777
|
const allowedApps = user.AllowedApps;
|
|
727
|
-
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
778
|
+
if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
728
779
|
const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
|
|
729
780
|
error.code = 'NOT_AUTHORIZED';
|
|
730
781
|
return done(error);
|
|
@@ -886,16 +937,69 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
886
937
|
|
|
887
938
|
const user = userResult.rows[0];
|
|
888
939
|
|
|
940
|
+
// Check for trusted device after OAuth authentication - should only skip 2FA, not OAuth validation
|
|
941
|
+
const trustedDeviceUser = await checkTrustedDevice(req, user.UserName);
|
|
942
|
+
if (trustedDeviceUser && (mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
943
|
+
console.log(`[mbkauthe] GitHub trusted device login for user: ${user.UserName}, skipping 2FA only`);
|
|
944
|
+
|
|
945
|
+
// Complete login process using the shared function
|
|
946
|
+
const userForSession = {
|
|
947
|
+
id: user.id,
|
|
948
|
+
username: user.UserName,
|
|
949
|
+
UserName: user.UserName,
|
|
950
|
+
role: user.Role,
|
|
951
|
+
Role: user.Role,
|
|
952
|
+
allowedApps: user.AllowedApps,
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// For OAuth redirect flow, we need to handle redirect differently
|
|
956
|
+
// Store the redirect URL before calling completeLoginProcess
|
|
957
|
+
const oauthRedirect = req.session.oauthRedirect;
|
|
958
|
+
delete req.session.oauthRedirect;
|
|
959
|
+
|
|
960
|
+
// Custom response handler for OAuth flow - wrap the response object
|
|
961
|
+
const originalJson = res.json.bind(res);
|
|
962
|
+
const originalStatus = res.status.bind(res);
|
|
963
|
+
let statusCode = 200;
|
|
964
|
+
|
|
965
|
+
res.status = function (code) {
|
|
966
|
+
statusCode = code;
|
|
967
|
+
return originalStatus(code);
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
res.json = function (data) {
|
|
971
|
+
if (data.success && statusCode === 200) {
|
|
972
|
+
// If login successful, redirect instead of sending JSON
|
|
973
|
+
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
974
|
+
console.log(`[mbkauthe] GitHub trusted device login: Redirecting to ${redirectUrl}`);
|
|
975
|
+
// Restore original methods before redirect
|
|
976
|
+
res.json = originalJson;
|
|
977
|
+
res.status = originalStatus;
|
|
978
|
+
return res.redirect(redirectUrl);
|
|
979
|
+
}
|
|
980
|
+
// Restore original methods for error responses
|
|
981
|
+
res.json = originalJson;
|
|
982
|
+
res.status = originalStatus;
|
|
983
|
+
return originalJson(data);
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
return await completeLoginProcess(req, res, userForSession);
|
|
987
|
+
}
|
|
988
|
+
|
|
889
989
|
// Check 2FA if enabled
|
|
890
990
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
891
991
|
// 2FA is enabled, store pre-auth user and redirect to 2FA
|
|
992
|
+
// If this was an oauth flow, use oauthRedirect set earlier
|
|
993
|
+
const oauthRedirect = req.session.oauthRedirect;
|
|
994
|
+
if (oauthRedirect) delete req.session.oauthRedirect;
|
|
892
995
|
req.session.preAuthUser = {
|
|
893
996
|
id: user.id,
|
|
894
997
|
username: user.UserName,
|
|
895
998
|
UserName: user.UserName,
|
|
896
999
|
role: user.Role,
|
|
897
1000
|
Role: user.Role,
|
|
898
|
-
loginMethod: 'github'
|
|
1001
|
+
loginMethod: 'github',
|
|
1002
|
+
redirectUrl: oauthRedirect || null
|
|
899
1003
|
};
|
|
900
1004
|
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
901
1005
|
return res.redirect('/mbkauthe/2fa');
|
|
@@ -17,6 +17,20 @@ async function validateSession(req, res, next) {
|
|
|
17
17
|
try {
|
|
18
18
|
const { id, sessionId, role, allowedApps } = req.session.user;
|
|
19
19
|
|
|
20
|
+
// Defensive checks for sessionId and allowedApps
|
|
21
|
+
if (!sessionId) {
|
|
22
|
+
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
23
|
+
req.session.destroy();
|
|
24
|
+
clearSessionCookies(res);
|
|
25
|
+
return renderError(res, {
|
|
26
|
+
code: 401,
|
|
27
|
+
error: "Session Expired",
|
|
28
|
+
message: "Your Session Has Expired. Please Log In Again.",
|
|
29
|
+
pagename: "Login",
|
|
30
|
+
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
// Normalize sessionId to lowercase for consistent comparison
|
|
21
35
|
const normalizedSessionId = sessionId.toLowerCase();
|
|
22
36
|
|
|
@@ -24,7 +38,11 @@ async function validateSession(req, res, next) {
|
|
|
24
38
|
const query = `SELECT "SessionId", "Active", "Role" FROM "Users" WHERE "id" = $1`;
|
|
25
39
|
const result = await dblogin.query({ name: 'validate-user-session', text: query, values: [id] });
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
const dbSessionId = result.rows.length > 0 && result.rows[0].SessionId ? String(result.rows[0].SessionId).toLowerCase() : null;
|
|
42
|
+
if (!dbSessionId || dbSessionId !== normalizedSessionId) {
|
|
43
|
+
if (result.rows.length > 0 && !result.rows[0].SessionId) {
|
|
44
|
+
console.warn(`[mbkauthe] DB sessionId is null for user "${req.session.user.username}"`);
|
|
45
|
+
}
|
|
28
46
|
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
29
47
|
req.session.destroy();
|
|
30
48
|
clearSessionCookies(res);
|
|
@@ -51,7 +69,9 @@ async function validateSession(req, res, next) {
|
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
if (role !== "SuperAdmin") {
|
|
54
|
-
|
|
72
|
+
// If allowedApps is not provided or not an array, treat as no access
|
|
73
|
+
const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
|
|
74
|
+
if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
55
75
|
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
56
76
|
req.session.destroy();
|
|
57
77
|
clearSessionCookies(res);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkauthe",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "MBKTech's reusable authentication system for Node.js applications.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -52,4 +52,4 @@
|
|
|
52
52
|
"cross-env": "^7.0.3",
|
|
53
53
|
"nodemon": "^3.1.11"
|
|
54
54
|
}
|
|
55
|
-
}
|
|
55
|
+
}
|
package/views/2fa.handlebars
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
const data = await response.json();
|
|
88
88
|
if (data.success) {
|
|
89
89
|
loginButtonText.textContent = 'Success! Redirecting...';
|
|
90
|
-
window.location.href = data.redirectUrl || '{{
|
|
90
|
+
window.location.href = data.redirectUrl || '{{ customURL }}';
|
|
91
91
|
} else {
|
|
92
92
|
loginButton.disabled = false;
|
|
93
93
|
loginButtonText.textContent = 'Verify';
|
package/views/info.handlebars
CHANGED
|
@@ -201,6 +201,10 @@
|
|
|
201
201
|
<div class="info-label">APP_NAME:</div>
|
|
202
202
|
<div class="info-value">{{mbkautheVar.APP_NAME}}</div>
|
|
203
203
|
</div>
|
|
204
|
+
<div class="info-row">
|
|
205
|
+
<div class="info-label">APP_Version:</div>
|
|
206
|
+
<div class="info-value">{{APP_VERSION}}</div>
|
|
207
|
+
</div>
|
|
204
208
|
<div class="info-row">
|
|
205
209
|
<div class="info-label">Domain:</div>
|
|
206
210
|
<div class="info-value">{{mbkautheVar.DOMAIN}}</div>
|
|
@@ -98,8 +98,7 @@
|
|
|
98
98
|
|
|
99
99
|
// Info dialogs
|
|
100
100
|
function usernameinfo() {
|
|
101
|
-
showMessage(`Your username is the part of your MBKTech.org email before the @ (e.g., abc.xyz@mbktech.org
|
|
102
|
-
→ abc.xyz). For guests or if you’ve forgotten your credentials, contact <a href="https://mbktech.org/Support">Support</a>.`, `What is my username?`);
|
|
101
|
+
showMessage(`Your username is the part of your MBKTech.org email before the @ (e.g., abc.xyz@mbktech.org → abc.xyz). For guests or if you’ve forgotten your credentials, contact <a href="https://mbktech.org/Support">Support</a>.`, `What is my username?`);
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
function tokeninfo() {
|
|
@@ -130,6 +129,8 @@
|
|
|
130
129
|
loginButton.disabled = true;
|
|
131
130
|
loginButtonText.textContent = 'Authenticating...';
|
|
132
131
|
|
|
132
|
+
// Pass redirect query param through to server so it can be used by 2FA flow
|
|
133
|
+
const pageRedirect = new URLSearchParams(window.location.search).get('redirect');
|
|
133
134
|
fetch('/mbkauthe/api/login', {
|
|
134
135
|
method: 'POST',
|
|
135
136
|
headers: {
|
|
@@ -138,14 +139,17 @@
|
|
|
138
139
|
body: JSON.stringify({
|
|
139
140
|
username,
|
|
140
141
|
password
|
|
142
|
+
, redirect: pageRedirect || null
|
|
141
143
|
})
|
|
142
144
|
})
|
|
143
145
|
.then(response => response.json())
|
|
144
146
|
.then(data => {
|
|
145
147
|
if (data.success) {
|
|
146
148
|
if (data.twoFactorRequired) {
|
|
147
|
-
// Redirect to 2FA page
|
|
148
|
-
|
|
149
|
+
// Redirect to 2FA page (= pass along redirect target)
|
|
150
|
+
const redirectTarget = data.redirectUrl || pageRedirect;
|
|
151
|
+
const redirectQuery = redirectTarget ? `?redirect=${encodeURIComponent(redirectTarget)}` : '';
|
|
152
|
+
window.location.href = `/mbkauthe/2fa${redirectQuery}`;
|
|
149
153
|
} else {
|
|
150
154
|
loginButtonText.textContent = 'Success! Redirecting...';
|
|
151
155
|
sessionStorage.setItem('sessionId', data.sessionId);
|
|
@@ -240,39 +244,12 @@
|
|
|
240
244
|
|
|
241
245
|
{{#if githubLoginEnabled }}
|
|
242
246
|
|
|
243
|
-
// GitHub login:
|
|
247
|
+
// GitHub login: Navigate directly to GitHub OAuth flow
|
|
244
248
|
async function startGithubLogin() {
|
|
245
249
|
const urlParams = new URLSearchParams(window.location.search);
|
|
246
250
|
const redirect = urlParams.get('redirect') || '{{customURL}}';
|
|
247
251
|
|
|
248
|
-
|
|
249
|
-
// Try POSTing to the backend so it can establish any session state
|
|
250
|
-
const resp = await fetch('/mbkauthe/api/github/login', {
|
|
251
|
-
method: 'POST',
|
|
252
|
-
headers: { 'Content-Type': 'application/json' },
|
|
253
|
-
credentials: 'include',
|
|
254
|
-
body: JSON.stringify({ redirect })
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// If backend responds with a JSON containing redirectUrl, navigate there
|
|
258
|
-
if (resp.ok) {
|
|
259
|
-
// If server redirected directly (resp.redirected), follow the final URL
|
|
260
|
-
if (resp.redirected) {
|
|
261
|
-
window.location.href = resp.url;
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const data = await resp.json().catch(() => null);
|
|
265
|
-
if (data && data.redirectUrl) {
|
|
266
|
-
window.location.href = data.redirectUrl;
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
} catch (error) {
|
|
271
|
-
// swallow and fallback to direct navigation
|
|
272
|
-
console.warn('[mbkauthe] GitHub login POST failed, falling back to direct redirect', error);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Fallback: navigate to the backend GET endpoint with redirect query
|
|
252
|
+
// Navigate directly to the backend GET endpoint with redirect query
|
|
276
253
|
window.location.href = `/mbkauthe/api/github/login?redirect=${encodeURIComponent(redirect)}`;
|
|
277
254
|
}
|
|
278
255
|
|
|
@@ -279,19 +279,18 @@
|
|
|
279
279
|
width: 100%;
|
|
280
280
|
padding: 12px 20px;
|
|
281
281
|
border-radius: var(--border-radius);
|
|
282
|
-
background: #
|
|
282
|
+
background: #262b30;
|
|
283
283
|
color: white;
|
|
284
|
-
|
|
285
|
-
font-
|
|
284
|
+
border: solid 0.13rem #262b30;
|
|
285
|
+
font-weight: 700;
|
|
286
|
+
font-size: 1rem;
|
|
286
287
|
text-decoration: none;
|
|
287
288
|
transition: var(--transition);
|
|
288
|
-
box-shadow: var(--box-shadow);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
.btn-github:hover {
|
|
292
|
-
background:
|
|
293
|
-
box-shadow:
|
|
294
|
-
transform: translateY(-2px);
|
|
292
|
+
background: transparent;
|
|
293
|
+
box-shadow: var(--box-shadow);
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
.btn-github i {
|
|
@@ -22,24 +22,15 @@
|
|
|
22
22
|
|
|
23
23
|
document.querySelector(".showmessageWindow .error-code").href = `https://mbktech.org/ErrorCode/#${errorCode}`;
|
|
24
24
|
*/
|
|
25
|
-
document
|
|
26
|
-
|
|
27
|
-
.classList
|
|
28
|
-
.add("active");
|
|
29
|
-
document
|
|
30
|
-
.body
|
|
31
|
-
.classList
|
|
32
|
-
.add("blur-active");
|
|
25
|
+
document.querySelector(".showMessageblurWindow").classList.add("active");
|
|
26
|
+
document.body.classList.add("blur-active");
|
|
33
27
|
}
|
|
34
28
|
function hideMessage() {
|
|
35
29
|
const blurWindow = document.querySelector(".showMessageblurWindow");
|
|
36
30
|
blurWindow.classList.add("fade-out");
|
|
37
31
|
setTimeout(() => {
|
|
38
32
|
blurWindow.classList.remove("active", "fade-out");
|
|
39
|
-
document
|
|
40
|
-
.body
|
|
41
|
-
.classList
|
|
42
|
-
.remove("blur-active");
|
|
33
|
+
document.body.classList.remove("blur-active");
|
|
43
34
|
}, 500);
|
|
44
35
|
}
|
|
45
36
|
</script>
|