mbkauthe 2.4.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 +184 -106
- package/package.json +2 -2
- package/views/info.handlebars +4 -0
- package/views/loginmbkauthe.handlebars +3 -31
- 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,
|
|
@@ -362,81 +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 && 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
|
-
// If a redirect is provided, validate and pass it through
|
|
428
|
-
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
429
|
-
return await completeLoginProcess(req, res, userForSession, requestedRedirect);
|
|
430
|
-
}
|
|
431
|
-
} catch (deviceErr) {
|
|
432
|
-
console.error("[mbkauthe] Error checking trusted device:", deviceErr);
|
|
433
|
-
// Continue with normal login flow if device check fails
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
445
|
// Combined query: fetch user data and 2FA status in one query
|
|
438
446
|
const userQuery = `
|
|
439
|
-
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",
|
|
440
448
|
tfa."TwoFAStatus"
|
|
441
449
|
FROM "Users" u
|
|
442
450
|
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
@@ -452,30 +460,33 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
452
460
|
const user = userResult.rows[0];
|
|
453
461
|
|
|
454
462
|
// Validate user has password field
|
|
455
|
-
if (!user.Password) {
|
|
463
|
+
if (!user.Password && !user.PasswordEnc) {
|
|
456
464
|
console.error("[mbkauthe] User account has no password set");
|
|
457
465
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
458
466
|
}
|
|
459
467
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
console.error("[mbkauthe] Error comparing password:", err);
|
|
470
|
-
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;
|
|
471
477
|
}
|
|
472
478
|
} else {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
479
|
+
// Use raw password comparison
|
|
480
|
+
if (user.Password) {
|
|
481
|
+
passwordMatches = user.Password === password;
|
|
476
482
|
}
|
|
477
483
|
}
|
|
478
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
|
+
|
|
479
490
|
if (!user.Active) {
|
|
480
491
|
console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
|
|
481
492
|
return res.status(403).json({ success: false, message: "Account is inactive" });
|
|
@@ -489,6 +500,23 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
489
500
|
}
|
|
490
501
|
}
|
|
491
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
|
+
|
|
492
520
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
493
521
|
// 2FA is enabled, prompt for token on a separate page
|
|
494
522
|
const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
|
|
@@ -614,13 +642,13 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
|
614
642
|
const operations = [
|
|
615
643
|
dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
|
|
616
644
|
];
|
|
617
|
-
|
|
645
|
+
|
|
618
646
|
if (req.sessionID) {
|
|
619
647
|
operations.push(
|
|
620
648
|
dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
|
|
621
649
|
);
|
|
622
650
|
}
|
|
623
|
-
|
|
651
|
+
|
|
624
652
|
await Promise.all(operations);
|
|
625
653
|
|
|
626
654
|
req.session.destroy((err) => {
|
|
@@ -692,6 +720,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
|
|
|
692
720
|
layout: false,
|
|
693
721
|
mbkautheVar: mbkautheVar,
|
|
694
722
|
version: packageJson.version,
|
|
723
|
+
APP_VERSION: appVersion,
|
|
695
724
|
latestVersion,
|
|
696
725
|
authorized: authorized,
|
|
697
726
|
});
|
|
@@ -908,6 +937,55 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
908
937
|
|
|
909
938
|
const user = userResult.rows[0];
|
|
910
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
|
+
|
|
911
989
|
// Check 2FA if enabled
|
|
912
990
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
913
991
|
// 2FA is enabled, store pre-auth user and redirect to 2FA
|
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/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() {
|
|
@@ -245,39 +244,12 @@
|
|
|
245
244
|
|
|
246
245
|
{{#if githubLoginEnabled }}
|
|
247
246
|
|
|
248
|
-
// GitHub login:
|
|
247
|
+
// GitHub login: Navigate directly to GitHub OAuth flow
|
|
249
248
|
async function startGithubLogin() {
|
|
250
249
|
const urlParams = new URLSearchParams(window.location.search);
|
|
251
250
|
const redirect = urlParams.get('redirect') || '{{customURL}}';
|
|
252
251
|
|
|
253
|
-
|
|
254
|
-
// Try POSTing to the backend so it can establish any session state
|
|
255
|
-
const resp = await fetch('/mbkauthe/api/github/login', {
|
|
256
|
-
method: 'POST',
|
|
257
|
-
headers: { 'Content-Type': 'application/json' },
|
|
258
|
-
credentials: 'include',
|
|
259
|
-
body: JSON.stringify({ redirect })
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// If backend responds with a JSON containing redirectUrl, navigate there
|
|
263
|
-
if (resp.ok) {
|
|
264
|
-
// If server redirected directly (resp.redirected), follow the final URL
|
|
265
|
-
if (resp.redirected) {
|
|
266
|
-
window.location.href = resp.url;
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
const data = await resp.json().catch(() => null);
|
|
270
|
-
if (data && data.redirectUrl) {
|
|
271
|
-
window.location.href = data.redirectUrl;
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
} catch (error) {
|
|
276
|
-
// swallow and fallback to direct navigation
|
|
277
|
-
console.warn('[mbkauthe] GitHub login POST failed, falling back to direct redirect', error);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Fallback: navigate to the backend GET endpoint with redirect query
|
|
252
|
+
// Navigate directly to the backend GET endpoint with redirect query
|
|
281
253
|
window.location.href = `/mbkauthe/api/github/login?redirect=${encodeURIComponent(redirect)}`;
|
|
282
254
|
}
|
|
283
255
|
|
|
@@ -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>
|