mbkauthe 3.5.0 → 4.0.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 +54 -304
- package/docs/api.md +2 -2
- package/docs/db.md +26 -20
- package/docs/db.sql +116 -0
- package/docs/env.md +10 -0
- package/index.d.ts +3 -3
- package/lib/config/cookies.js +6 -0
- package/lib/config/index.js +20 -4
- package/lib/config/security.js +1 -1
- package/lib/middleware/auth.js +64 -30
- package/lib/middleware/index.js +37 -28
- package/lib/routes/auth.js +59 -36
- package/lib/routes/misc.js +22 -9
- package/package.json +1 -1
- package/views/loginmbkauthe.handlebars +0 -2
package/README.md
CHANGED
|
@@ -1,35 +1,28 @@
|
|
|
1
|
-
# MBKAuthe - Authentication System
|
|
1
|
+
# MBKAuthe - Node.js Authentication System
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/mbkauthe)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
|
-
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/codeql.yml)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<p align="center">
|
|
11
|
-
<img height="64px" src="./public/icon.svg" alt="MBK Chat Platform" />
|
|
12
|
-
</p>
|
|
6
|
+
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml)
|
|
13
7
|
|
|
14
8
|
<p align="center">
|
|
15
|
-
<img src="
|
|
16
|
-
<img height="48px" src="https://handlebarsjs.com/handlebars-icon.svg" alt="Handlebars" />
|
|
9
|
+
<img height="64px" src="./public/icon.svg" alt="MBKAuthe" />
|
|
17
10
|
</p>
|
|
18
11
|
|
|
19
|
-
**MBKAuthe** is a production-ready authentication system for Node.js
|
|
12
|
+
**MBKAuthe** is a production-ready authentication system for Node.js with Express and PostgreSQL. Features include secure login, 2FA, role-based access, OAuth (GitHub & Google), multi-session support, and multi-app user management.
|
|
20
13
|
|
|
21
14
|
## ✨ Key Features
|
|
22
15
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
16
|
+
- Secure password authentication (PBKDF2)
|
|
17
|
+
- PostgreSQL session management
|
|
18
|
+
- Multi-session support (configurable concurrent sessions per user)
|
|
19
|
+
- Optional TOTP-based 2FA with trusted devices
|
|
20
|
+
- OAuth login (GitHub & Google)
|
|
21
|
+
- Role-based access: SuperAdmin, NormalUser, Guest
|
|
22
|
+
- CSRF protection & rate limiting
|
|
23
|
+
- Easy Express.js integration
|
|
24
|
+
- Customizable Handlebars templates
|
|
25
|
+
- Session fixation prevention
|
|
33
26
|
|
|
34
27
|
## 📦 Installation
|
|
35
28
|
|
|
@@ -39,49 +32,15 @@ npm install mbkauthe
|
|
|
39
32
|
|
|
40
33
|
## 🚀 Quick Start
|
|
41
34
|
|
|
42
|
-
**1. Configure Environment
|
|
35
|
+
**1. Configure Environment**
|
|
43
36
|
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
SESSION_SECRET_KEY=your-secret-key
|
|
47
|
-
MAIN_SECRET_TOKEN=api-token
|
|
48
|
-
IS_DEPLOYED=false
|
|
49
|
-
DOMAIN=localhost
|
|
50
|
-
LOGIN_DB=postgresql://user:pass@localhost:5432/db
|
|
51
|
-
|
|
52
|
-
# Optional Features
|
|
53
|
-
MBKAUTH_TWO_FA_ENABLE=false
|
|
54
|
-
COOKIE_EXPIRE_TIME=2
|
|
55
|
-
|
|
56
|
-
# OAuth Configuration (Optional)
|
|
57
|
-
GITHUB_LOGIN_ENABLED=false
|
|
58
|
-
GITHUB_CLIENT_ID=
|
|
59
|
-
GITHUB_CLIENT_SECRET=
|
|
60
|
-
|
|
61
|
-
GOOGLE_LOGIN_ENABLED=false
|
|
62
|
-
GOOGLE_CLIENT_ID=
|
|
63
|
-
GOOGLE_CLIENT_SECRET=
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
**2. Set Up Database**
|
|
67
|
-
|
|
68
|
-
```sql
|
|
69
|
-
CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
|
|
70
|
-
|
|
71
|
-
CREATE TABLE "Users" (
|
|
72
|
-
id SERIAL PRIMARY KEY,
|
|
73
|
-
"UserName" VARCHAR(50) NOT NULL UNIQUE,
|
|
74
|
-
"Password" VARCHAR(61) NOT NULL,
|
|
75
|
-
"Role" role DEFAULT 'NormalUser',
|
|
76
|
-
"Active" BOOLEAN DEFAULT FALSE,
|
|
77
|
-
"AllowedApps" JSONB DEFAULT '["mbkauthe"]',
|
|
78
|
-
"SessionId" VARCHAR(213),
|
|
79
|
-
created_at TIMESTAMP DEFAULT NOW(),
|
|
80
|
-
updated_at TIMESTAMP DEFAULT NOW()
|
|
81
|
-
);
|
|
37
|
+
```bash
|
|
38
|
+
Copy-Item .env.example .env
|
|
82
39
|
```
|
|
40
|
+
See [docs/env.md](docs/env.md).
|
|
83
41
|
|
|
84
|
-
|
|
42
|
+
**2. Set Up Database**
|
|
43
|
+
Run [docs/db.sql](docs/db.sql) to create tables and a default SuperAdmin (`support` / `12345678`). Change the password immediately. See [docs/db.md](docs/db.md).
|
|
85
44
|
|
|
86
45
|
**3. Integrate with Express**
|
|
87
46
|
|
|
@@ -89,295 +48,86 @@ See [docs/db.md](docs/db.md) for complete schemas.
|
|
|
89
48
|
import express from 'express';
|
|
90
49
|
import mbkauthe, { validateSession, checkRolePermission } from 'mbkauthe';
|
|
91
50
|
import dotenv from 'dotenv';
|
|
92
|
-
|
|
93
51
|
dotenv.config();
|
|
94
52
|
|
|
95
|
-
// App-specific configuration (as JSON string)
|
|
96
|
-
process.env.mbkautheVar = JSON.stringify({
|
|
97
|
-
APP_NAME: process.env.APP_NAME,
|
|
98
|
-
SESSION_SECRET_KEY: process.env.SESSION_SECRET_KEY,
|
|
99
|
-
Main_SECRET_TOKEN: process.env.MAIN_SECRET_TOKEN,
|
|
100
|
-
IS_DEPLOYED: process.env.IS_DEPLOYED,
|
|
101
|
-
DOMAIN: process.env.DOMAIN,
|
|
102
|
-
LOGIN_DB: process.env.LOGIN_DB,
|
|
103
|
-
loginRedirectURL: '/dashboard'
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Optional shared configuration (useful for shared OAuth credentials across multiple projects)
|
|
107
|
-
process.env.mbkauthShared = JSON.stringify({
|
|
108
|
-
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
|
109
|
-
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
|
110
|
-
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
|
111
|
-
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// MBKAuth prioritizes values in mbkautheVar, then mbkauthShared, then built-in defaults.
|
|
115
|
-
|
|
116
53
|
const app = express();
|
|
117
|
-
|
|
118
|
-
// Mount authentication routes
|
|
119
54
|
app.use(mbkauthe);
|
|
120
55
|
|
|
121
|
-
|
|
122
|
-
app.get('/
|
|
123
|
-
res.send(`Welcome ${req.session.user.username}!`);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
app.get('/admin', validateSession, checkRolePermission(['SuperAdmin']), (req, res) => {
|
|
127
|
-
res.send('Admin Panel');
|
|
128
|
-
});
|
|
56
|
+
app.get('/dashboard', validateSession, (req, res) => res.send(`Welcome ${req.session.user.username}!`));
|
|
57
|
+
app.get('/admin', validateSession, checkRolePermission(['SuperAdmin']), (req, res) => res.send('Admin Panel'));
|
|
129
58
|
|
|
130
59
|
app.listen(3000);
|
|
131
60
|
```
|
|
132
61
|
|
|
133
|
-
|
|
134
|
-
## 🧪 Testing & Git Hooks
|
|
135
|
-
|
|
136
|
-
MBKAuthe includes comprehensive test coverage for all authentication features. **A pre-commit hook is provided to ensure code quality:**
|
|
137
|
-
|
|
138
|
-
### Pre-commit Hook (Automatic Test Runner)
|
|
139
|
-
|
|
140
|
-
- Located at `scripts/pre-commit` and `scripts/pre-commit` (Node.js, cross-platform)
|
|
141
|
-
- Starts the dev server, runs all tests, and blocks commits if any test fails
|
|
142
|
-
- The dev server is automatically stopped after tests complete
|
|
143
|
-
- Ensures you never commit code that breaks tests
|
|
144
|
-
|
|
145
|
-
### Git Hook Setup
|
|
146
|
-
|
|
147
|
-
Hooks are auto-configured every time you run `npm run dev`, `npm test`, or `npm run test:watch` (see `scripts/setup-hooks.js`).
|
|
148
|
-
|
|
149
|
-
If you ever need to manually set up hooks:
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
node scripts/setup-hooks.js
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Running Tests
|
|
62
|
+
## 🧪 Testing
|
|
156
63
|
|
|
157
64
|
```bash
|
|
158
|
-
# Run all tests (auto-configures hooks)
|
|
159
65
|
npm test
|
|
160
|
-
|
|
161
|
-
# Run tests in watch mode (auto-configures hooks)
|
|
162
66
|
npm run test:watch
|
|
163
|
-
|
|
164
|
-
# Run with development flags (auto-configures hooks)
|
|
165
67
|
npm run dev
|
|
166
68
|
```
|
|
167
69
|
|
|
168
|
-
**Test Coverage:**
|
|
169
|
-
- ✅ Authentication flows (login, 2FA, logout)
|
|
170
|
-
- ✅ OAuth integration (GitHub)
|
|
171
|
-
- ✅ Session management and security
|
|
172
|
-
- ✅ Role-based access control
|
|
173
|
-
- ✅ API endpoints and error handling
|
|
174
|
-
- ✅ CSRF protection and rate limiting
|
|
175
|
-
- ✅ Static asset serving
|
|
176
|
-
|
|
177
|
-
## 📂 Architecture (v3.0)
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
lib/
|
|
181
|
-
├── config/ # Configuration & security
|
|
182
|
-
├── database/ # PostgreSQL pool
|
|
183
|
-
├── utils/ # Errors & response helpers
|
|
184
|
-
├── middleware/ # Auth & session middleware
|
|
185
|
-
└── routes/ # Auth, OAuth, misc routes
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
**Key Improvements in v3.0:**
|
|
189
|
-
- Modular structure with clear separation of concerns
|
|
190
|
-
- Organized config, database, utils, middleware, and routes
|
|
191
|
-
- Better maintainability and scalability
|
|
192
|
-
|
|
193
70
|
## 🔧 Core API
|
|
194
71
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
app.get('/protected', validateSession, handler);
|
|
200
|
-
|
|
201
|
-
// Role checking
|
|
202
|
-
app.get('/admin', validateSession, checkRolePermission(['SuperAdmin']), handler);
|
|
72
|
+
- **Session Validation:** `validateSession`
|
|
73
|
+
- **Role Check:** `checkRolePermission(['Role'])`
|
|
74
|
+
- **Combined:** `validateSessionAndRole(['SuperAdmin', 'NormalUser'])`
|
|
75
|
+
- **API Token Auth:** `authenticate(process.env.API_TOKEN)`
|
|
203
76
|
|
|
204
|
-
|
|
205
|
-
import { validateSessionAndRole } from 'mbkauthe';
|
|
206
|
-
app.get('/mod', validateSessionAndRole(['SuperAdmin', 'NormalUser']), handler);
|
|
207
|
-
|
|
208
|
-
// API token auth
|
|
209
|
-
import { authenticate } from 'mbkauthe';
|
|
210
|
-
app.post('/api/data', authenticate(process.env.API_TOKEN), handler);
|
|
211
|
-
```
|
|
77
|
+
## 🔐 Security
|
|
212
78
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
- `GET /mbkauthe/login` - Login page (8/min rate limit)
|
|
218
|
-
- `POST /mbkauthe/api/login` - Login endpoint (8/min rate limit)
|
|
219
|
-
- `POST /mbkauthe/api/logout` - Logout endpoint (10/min rate limit)
|
|
220
|
-
- `GET /mbkauthe/2fa` - 2FA verification page (if enabled)
|
|
221
|
-
- `POST /mbkauthe/api/verify-2fa` - 2FA verification API (5/min rate limit)
|
|
222
|
-
|
|
223
|
-
**OAuth Routes:**
|
|
224
|
-
- `GET /mbkauthe/api/github/login` - GitHub OAuth initiation (10/5min rate limit)
|
|
225
|
-
- `GET /mbkauthe/api/github/login/callback` - GitHub OAuth callback
|
|
226
|
-
- `GET /mbkauthe/api/google/login` - Google OAuth initiation (10/5min rate limit)
|
|
227
|
-
- `GET /mbkauthe/api/google/login/callback` - Google OAuth callback
|
|
228
|
-
|
|
229
|
-
**Information & Utility Routes:**
|
|
230
|
-
- `GET /mbkauthe/info`, `/mbkauthe/i` - Version & config info (8/min rate limit)
|
|
231
|
-
- `GET /mbkauthe/ErrorCode` - Error code documentation
|
|
232
|
-
- `GET /mbkauthe/test` - Test authentication status (8/min rate limit)
|
|
233
|
-
|
|
234
|
-
**Static Asset Routes:**
|
|
235
|
-
- `GET /mbkauthe/main.js` - Client-side JavaScript utilities
|
|
236
|
-
- `GET /mbkauthe/bg.webp` - Background image for auth pages
|
|
237
|
-
- `GET /icon.svg` - Application SVG icon (root level)
|
|
238
|
-
- `GET /favicon.ico`, `/icon.ico` - Application favicon
|
|
239
|
-
|
|
240
|
-
**Admin API Routes:**
|
|
241
|
-
- `POST /mbkauthe/api/terminateAllSessions` - Terminate all sessions (admin only)
|
|
242
|
-
|
|
243
|
-
## 🔐 Security Features
|
|
244
|
-
|
|
245
|
-
- **Rate Limiting**: Login (8/min), Logout (10/min), 2FA (5/min), OAuth (10/5min), Admin (3/5min)
|
|
246
|
-
- **CSRF Protection**: All state-changing routes protected with token validation
|
|
247
|
-
- **Secure Cookies**: httpOnly, sameSite, secure in production
|
|
248
|
-
- **Password Hashing**: PBKDF2 with 100k iterations
|
|
249
|
-
- **Session Security**: PostgreSQL-backed, automatic cleanup, session fixation prevention
|
|
250
|
-
- **OAuth Security**: State validation, token expiry handling, secure callback validation
|
|
79
|
+
- Rate limiting, CSRF protection, secure cookies
|
|
80
|
+
- Password hashing (PBKDF2, 100k iterations)
|
|
81
|
+
- PostgreSQL-backed sessions with automatic cleanup
|
|
82
|
+
- OAuth with state validation and secure callbacks
|
|
251
83
|
|
|
252
84
|
## 📱 Two-Factor Authentication
|
|
253
85
|
|
|
254
|
-
Enable
|
|
255
|
-
|
|
256
|
-
```sql
|
|
257
|
-
CREATE TABLE "TwoFA" (
|
|
258
|
-
"UserName" VARCHAR(50) PRIMARY KEY REFERENCES "Users"("UserName"),
|
|
259
|
-
"TwoFAStatus" BOOLEAN NOT NULL,
|
|
260
|
-
"TwoFASecret" TEXT
|
|
261
|
-
);
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Users can mark devices as trusted to skip 2FA for configurable duration.
|
|
86
|
+
Enable via `MBKAUTH_TWO_FA_ENABLE=true`. Trusted devices can skip 2FA for a set duration.
|
|
265
87
|
|
|
266
88
|
## 🔄 OAuth Integration
|
|
267
89
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
**Setup:**
|
|
271
|
-
|
|
272
|
-
1. Create GitHub OAuth App with callback: `https://yourdomain.com/mbkauthe/api/github/login/callback`
|
|
273
|
-
2. Configure environment:
|
|
274
|
-
```env
|
|
275
|
-
GITHUB_LOGIN_ENABLED=true
|
|
276
|
-
GITHUB_CLIENT_ID=your_client_id
|
|
277
|
-
GITHUB_CLIENT_SECRET=your_client_secret
|
|
278
|
-
```
|
|
279
|
-
3. Create table:
|
|
280
|
-
```sql
|
|
281
|
-
CREATE TABLE user_github (
|
|
282
|
-
id SERIAL PRIMARY KEY,
|
|
283
|
-
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
284
|
-
github_id VARCHAR(255) UNIQUE,
|
|
285
|
-
github_username VARCHAR(255),
|
|
286
|
-
access_token VARCHAR(255),
|
|
287
|
-
created_at TIMESTAMP DEFAULT NOW()
|
|
288
|
-
);
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Google OAuth
|
|
292
|
-
|
|
293
|
-
**Setup:**
|
|
294
|
-
|
|
295
|
-
1. Create Google OAuth 2.0 Client in [Google Cloud Console](https://console.cloud.google.com/)
|
|
296
|
-
2. Add authorized redirect URI: `https://yourdomain.com/mbkauthe/api/google/login/callback`
|
|
297
|
-
3. Configure environment:
|
|
298
|
-
```env
|
|
299
|
-
GOOGLE_LOGIN_ENABLED=true
|
|
300
|
-
GOOGLE_CLIENT_ID=your_client_id
|
|
301
|
-
GOOGLE_CLIENT_SECRET=your_client_secret
|
|
302
|
-
```
|
|
303
|
-
4. Create table:
|
|
304
|
-
```sql
|
|
305
|
-
CREATE TABLE user_google (
|
|
306
|
-
id SERIAL PRIMARY KEY,
|
|
307
|
-
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
308
|
-
google_id VARCHAR(255) UNIQUE,
|
|
309
|
-
google_email VARCHAR(255),
|
|
310
|
-
access_token VARCHAR(255),
|
|
311
|
-
created_at TIMESTAMP DEFAULT NOW()
|
|
312
|
-
);
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
**Note:** Users must link their OAuth accounts before they can use OAuth login.
|
|
90
|
+
**GitHub / Google OAuth:** Configure apps and credentials via `.env` or `mbkautheVar`. Users must link accounts before login.
|
|
316
91
|
|
|
317
92
|
## 🎨 Customization
|
|
318
93
|
|
|
319
|
-
**Redirect URL:**
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// ...
|
|
323
|
-
loginRedirectURL: '/dashboard'
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
**Custom Views:** Create in `views/` directory:
|
|
328
|
-
- `loginmbkauthe.handlebars` - Login page
|
|
329
|
-
- `2fa.handlebars` - 2FA page
|
|
330
|
-
- `Error/dError.handlebars` - Error page
|
|
331
|
-
|
|
332
|
-
**Database Access:**
|
|
333
|
-
```javascript
|
|
334
|
-
import { dblogin } from 'mbkauthe';
|
|
335
|
-
const result = await dblogin.query('SELECT * FROM "Users"');
|
|
336
|
-
```
|
|
94
|
+
- **Redirect URL:** `mbkautheVar={"loginRedirectURL":"/dashboard"}`
|
|
95
|
+
- **Custom Views:** `views/loginmbkauthe.handlebars`, `2fa.handlebars`, `Error/dError.handlebars`
|
|
96
|
+
- **Database Access:** `import { dblogin } from 'mbkauthe'; const result = await dblogin.query('SELECT * FROM "Users"');`
|
|
337
97
|
|
|
338
98
|
## 🚢 Deployment
|
|
339
99
|
|
|
340
|
-
|
|
341
|
-
-
|
|
342
|
-
-
|
|
343
|
-
-
|
|
344
|
-
-
|
|
345
|
-
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
**Vercel:**
|
|
349
|
-
|
|
350
|
-
Tip: On Vercel you can set `mbkauthShared` at the project or team level to share common OAuth credentials across multiple deployments. MBKAuth will use values from `mbkautheVar` first and fall back to `mbkauthShared`.
|
|
351
|
-
```json
|
|
352
|
-
{
|
|
353
|
-
"version": 2,
|
|
354
|
-
"builds": [{ "src": "index.js", "use": "@vercel/node" }],
|
|
355
|
-
"routes": [{ "src": "/(.*)", "dest": "/index.js" }]
|
|
356
|
-
}
|
|
357
|
-
```
|
|
100
|
+
Checklist for production:
|
|
101
|
+
- `IS_DEPLOYED=true`
|
|
102
|
+
- Strong secrets for SESSION_SECRET_KEY & Main_SECRET_TOKEN
|
|
103
|
+
- HTTPS enabled
|
|
104
|
+
- Correct DOMAIN & COOKIE_EXPIRE_TIME
|
|
105
|
+
- Use environment variables for all secrets
|
|
106
|
+
|
|
107
|
+
**Vercel:** Supports shared OAuth credentials via `mbkauthShared`.
|
|
358
108
|
|
|
359
109
|
## 📚 Documentation
|
|
360
110
|
|
|
361
|
-
- [API
|
|
362
|
-
- [Database
|
|
363
|
-
- [Environment Config](docs/env.md)
|
|
364
|
-
- [Error
|
|
111
|
+
- [API Reference](docs/api.md)
|
|
112
|
+
- [Database Schema](docs/db.md)
|
|
113
|
+
- [Environment Config](docs/env.md)
|
|
114
|
+
- [Error Codes](docs/error-messages.md)
|
|
365
115
|
|
|
366
116
|
## 📝 License
|
|
367
117
|
|
|
368
|
-
|
|
118
|
+
GPL v2.0 — see [LICENSE](LICENSE)
|
|
369
119
|
|
|
370
120
|
## 👨💻 Author
|
|
371
121
|
|
|
372
122
|
**Muhammad Bin Khalid**
|
|
373
123
|
📧 [support@mbktech.org](mailto:support@mbktech.org) | [chmuhammadbinkhalid28@gmail.com](mailto:chmuhammadbinkhalid28@gmail.com)
|
|
374
|
-
🔗 [@MIbnEKhalid](https://github.com/MIbnEKhalid)
|
|
124
|
+
🔗 [GitHub @MIbnEKhalid](https://github.com/MIbnEKhalid)
|
|
375
125
|
|
|
376
126
|
## 🔗 Links
|
|
377
127
|
|
|
378
|
-
- [npm
|
|
379
|
-
- [GitHub
|
|
380
|
-
- [
|
|
128
|
+
- [npm](https://www.npmjs.com/package/mbkauthe)
|
|
129
|
+
- [GitHub](https://github.com/MIbnEKhalid/mbkauthe)
|
|
130
|
+
- [Support](https://github.com/MIbnEKhalid/mbkauthe/issues)
|
|
381
131
|
|
|
382
132
|
---
|
|
383
133
|
|
package/docs/api.md
CHANGED
|
@@ -38,7 +38,7 @@ When a user logs in, MBKAuthe creates a session and sets the following cookies:
|
|
|
38
38
|
| Cookie Name | Description | HttpOnly | Secure | SameSite |
|
|
39
39
|
|------------|-------------|----------|--------|----------|
|
|
40
40
|
| `mbkauthe.sid` | Session identifier | ✓ | Auto* | lax |
|
|
41
|
-
| `sessionId` |
|
|
41
|
+
| `sessionId` | Opaque session token (backed by `Sessions` table) | ✓ | Auto* | lax |
|
|
42
42
|
| `username` | Username | ✗ | Auto* | lax |
|
|
43
43
|
|
|
44
44
|
\* `secure` flag is automatically set to `true` in production when `IS_DEPLOYED=true`
|
|
@@ -46,7 +46,7 @@ When a user logs in, MBKAuthe creates a session and sets the following cookies:
|
|
|
46
46
|
### Session Lifetime
|
|
47
47
|
|
|
48
48
|
- Default: 2 days (configurable via `COOKIE_EXPIRE_TIME`)
|
|
49
|
-
-
|
|
49
|
+
- Application sessions are stored in the `Sessions` table in PostgreSQL
|
|
50
50
|
- Sessions persist across subdomains in production
|
|
51
51
|
|
|
52
52
|
---
|
package/docs/db.md
CHANGED
|
@@ -170,11 +170,6 @@ GITHUB_CLIENT_SECRET=your_github_client_secret
|
|
|
170
170
|
|
|
171
171
|
The GitHub login feature is now fully integrated into your mbkauthe system and ready to use!
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
173
|
## Database structure
|
|
179
174
|
|
|
180
175
|
[<- Back](README.md)
|
|
@@ -198,7 +193,7 @@ The GitHub login feature is now fully integrated into your mbkauthe system and r
|
|
|
198
193
|
- `Role` (ENUM): The role of the user. Possible values: `SuperAdmin`, `NormalUser`, `Guest`.
|
|
199
194
|
- `Active` (BOOLEAN): Indicates whether the user account is active.
|
|
200
195
|
- `HaveMailAccount` (BOOLEAN)(optional): Indicates if the user has a linked mail account.
|
|
201
|
-
-
|
|
196
|
+
- (SessionId removed) The application now stores multiple concurrent sessions in the `Sessions` table.
|
|
202
197
|
- `GuestRole` (JSONB): Stores additional guest-specific role information in binary JSON format.
|
|
203
198
|
- `AllowedApps`(JSONB): Array of applications the user is authorized to access.
|
|
204
199
|
|
|
@@ -215,19 +210,32 @@ CREATE TABLE "Users" (
|
|
|
215
210
|
"Active" BOOLEAN DEFAULT FALSE,
|
|
216
211
|
"HaveMailAccount" BOOLEAN DEFAULT FALSE,
|
|
217
212
|
"AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
|
|
218
|
-
"SessionId" VARCHAR(213),
|
|
219
213
|
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
220
214
|
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
221
215
|
"last_login" TIMESTAMP WITH TIME ZONE
|
|
222
216
|
);
|
|
223
217
|
|
|
224
218
|
-- Add indexes for performance optimization
|
|
225
|
-
CREATE INDEX IF NOT EXISTS idx_users_sessionid ON "Users" (LOWER("SessionId"));
|
|
226
219
|
CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" ("UserName");
|
|
227
220
|
CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" ("Active");
|
|
228
221
|
CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" ("Role");
|
|
229
222
|
CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" (last_login);
|
|
230
|
-
|
|
223
|
+
|
|
224
|
+
-- Application Sessions table (stores multiple concurrent sessions per user)
|
|
225
|
+
-- Note: this is separate from the express-session store table named "session"
|
|
226
|
+
CREATE TABLE "Sessions" (
|
|
227
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- requires pgcrypto or uuid-ossp
|
|
228
|
+
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
229
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
230
|
+
expires_at TIMESTAMP WITH TIME ZONE,
|
|
231
|
+
meta JSONB
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
-- Indexes optimized by username instead of numeric user id
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_username ON "Sessions" ("UserName");
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_created ON "Sessions" ("UserName", created_at);
|
|
237
|
+
|
|
238
|
+
**Multi-session behavior:** MBKAuthe supports multiple concurrent application sessions per user. The maximum number of concurrent sessions is controlled by `mbkautheVar.MAX_SESSIONS_PER_USER` (default: 5). When a new session would exceed that limit, the system prunes the oldest session(s) for that user (ordered by `created_at`) to keep the count within the configured maximum.
|
|
231
239
|
```
|
|
232
240
|
|
|
233
241
|
**Password Storage Notes:**
|
|
@@ -250,12 +258,10 @@ CREATE TABLE "session" (
|
|
|
250
258
|
sid VARCHAR(33) PRIMARY KEY NOT NULL,
|
|
251
259
|
sess JSONB NOT NULL,
|
|
252
260
|
expire TimeStamp WITH TIME ZONE Not Null,
|
|
253
|
-
last_activity TimeStamp WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
254
261
|
);
|
|
255
262
|
|
|
256
263
|
-- Add indexes for performance optimization
|
|
257
264
|
CREATE INDEX IF NOT EXISTS idx_session_expire ON "session" ("expire");
|
|
258
|
-
CREATE INDEX IF NOT EXISTS idx_session_last_activity ON "session" (last_activity);
|
|
259
265
|
CREATE INDEX IF NOT EXISTS idx_session_user_id ON "session" ((sess->'user'->>'id'));
|
|
260
266
|
```
|
|
261
267
|
|
|
@@ -286,7 +292,7 @@ CREATE INDEX IF NOT EXISTS idx_twofa_username_status ON "TwoFA" ("UserName", "Tw
|
|
|
286
292
|
|
|
287
293
|
- `id` (INTEGER, auto-increment, primary key): Unique identifier for each trusted device.
|
|
288
294
|
- `UserName` (VARCHAR): The username of the device owner (foreign key to Users).
|
|
289
|
-
- `DeviceToken` (VARCHAR):
|
|
295
|
+
- `DeviceToken` (VARCHAR): **HMAC-SHA256 hash** of the device token (raw token is only sent to the client in an httpOnly cookie and **not** stored in plaintext).
|
|
290
296
|
- `DeviceName` (VARCHAR): Optional friendly name for the device.
|
|
291
297
|
- `UserAgent` (TEXT): Browser/client user agent string.
|
|
292
298
|
- `IpAddress` (VARCHAR): IP address when device was trusted.
|
|
@@ -320,22 +326,22 @@ To add new users to the `Users` table, use the following SQL queries:
|
|
|
320
326
|
|
|
321
327
|
**For Raw Password Storage (EncPass=false):**
|
|
322
328
|
```sql
|
|
323
|
-
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "
|
|
324
|
-
VALUES ('support', '12345678', 'SuperAdmin', true, false,
|
|
329
|
+
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "GuestRole")
|
|
330
|
+
VALUES ('support', '12345678', 'SuperAdmin', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
325
331
|
|
|
326
|
-
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "
|
|
327
|
-
VALUES ('test', '12345678', 'NormalUser', true, false,
|
|
332
|
+
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "GuestRole")
|
|
333
|
+
VALUES ('test', '12345678', 'NormalUser', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
328
334
|
```
|
|
329
335
|
|
|
330
336
|
**For Encrypted Password Storage (EncPass=true):**
|
|
331
337
|
```sql
|
|
332
338
|
-- Note: You'll need to hash the password using the hashPassword function
|
|
333
339
|
-- Example with pre-hashed password (PBKDF2 with username as salt)
|
|
334
|
-
INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "
|
|
335
|
-
VALUES ('support', 'your_hashed_password_here', 'SuperAdmin', true, false,
|
|
340
|
+
INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "GuestRole")
|
|
341
|
+
VALUES ('support', 'your_hashed_password_here', 'SuperAdmin', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
336
342
|
|
|
337
|
-
INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "
|
|
338
|
-
VALUES ('test', 'your_hashed_password_here', 'NormalUser', true, false,
|
|
343
|
+
INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "GuestRole")
|
|
344
|
+
VALUES ('test', 'your_hashed_password_here', 'NormalUser', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
|
339
345
|
```
|
|
340
346
|
|
|
341
347
|
**Configuration Notes:**
|
package/docs/db.sql
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
|
|
2
|
+
-- GitHub users table
|
|
3
|
+
CREATE TABLE user_github (
|
|
4
|
+
id SERIAL PRIMARY KEY,
|
|
5
|
+
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
6
|
+
github_id VARCHAR(255) UNIQUE,
|
|
7
|
+
github_username VARCHAR(255),
|
|
8
|
+
access_token TEXT,
|
|
9
|
+
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
10
|
+
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
-- Add indexes for performance optimization
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_user_github_github_id ON user_github (github_id);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_user_github_user_name ON user_github (user_name);
|
|
16
|
+
|
|
17
|
+
-- Google users table
|
|
18
|
+
CREATE TABLE user_google (
|
|
19
|
+
id SERIAL PRIMARY KEY,
|
|
20
|
+
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
21
|
+
google_id VARCHAR(255) UNIQUE,
|
|
22
|
+
google_email VARCHAR(255),
|
|
23
|
+
access_token TEXT,
|
|
24
|
+
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
25
|
+
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- Add indexes for performance optimization
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_user_google_google_id ON user_google (google_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest');
|
|
36
|
+
|
|
37
|
+
CREATE TABLE "Users" (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
"UserName" VARCHAR(50) NOT NULL UNIQUE,
|
|
40
|
+
"Password" VARCHAR(61), -- For raw passwords (when EncPass=false)
|
|
41
|
+
"PasswordEnc" VARCHAR(128), -- For encrypted passwords (when EncPass=true)
|
|
42
|
+
"Role" role DEFAULT 'NormalUser' NOT NULL,
|
|
43
|
+
"Active" BOOLEAN DEFAULT FALSE,
|
|
44
|
+
"HaveMailAccount" BOOLEAN DEFAULT FALSE,
|
|
45
|
+
"AllowedApps" JSONB DEFAULT '["mbkauthe", "portal"]',
|
|
46
|
+
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
48
|
+
"last_login" TIMESTAMP WITH TIME ZONE
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- Add indexes for performance optimization
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON "Users" ("UserName");
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" ("Active");
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" ("Role");
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" (last_login);
|
|
56
|
+
|
|
57
|
+
-- Application Sessions table (stores multiple concurrent sessions per user)
|
|
58
|
+
-- Note: this is separate from the express-session store table named "session"
|
|
59
|
+
CREATE TABLE "Sessions" (
|
|
60
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- requires pgcrypto or uuid-ossp
|
|
61
|
+
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
62
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
63
|
+
expires_at TIMESTAMP WITH TIME ZONE,
|
|
64
|
+
meta JSONB
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
-- Indexes optimized by username instead of numeric user id
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_username ON "Sessions" ("UserName");
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_created ON "Sessions" ("UserName", created_at);
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
CREATE TABLE "session" (
|
|
73
|
+
sid VARCHAR(33) PRIMARY KEY NOT NULL,
|
|
74
|
+
sess JSONB NOT NULL,
|
|
75
|
+
expire TimeStamp WITH TIME ZONE Not Null,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- Add indexes for performance optimization
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_session_expire ON "session" ("expire");
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_session_user_id ON "session" ((sess->'user'->>'id'));
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
CREATE TABLE "TwoFA" (
|
|
85
|
+
"UserName" VARCHAR(50) primary key REFERENCES "Users"("UserName"),
|
|
86
|
+
"TwoFAStatus" boolean NOT NULL,
|
|
87
|
+
"TwoFASecret" TEXT
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- Add indexes for performance optimization
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_twofa_username ON "TwoFA" ("UserName");
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_twofa_username_status ON "TwoFA" ("UserName", "TwoFAStatus");
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
CREATE TABLE "TrustedDevices" (
|
|
97
|
+
"id" SERIAL PRIMARY KEY,
|
|
98
|
+
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
99
|
+
"DeviceToken" VARCHAR(64) UNIQUE NOT NULL,
|
|
100
|
+
"DeviceName" VARCHAR(255),
|
|
101
|
+
"UserAgent" TEXT,
|
|
102
|
+
"IpAddress" VARCHAR(45),
|
|
103
|
+
"CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
104
|
+
"ExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
105
|
+
"LastUsed" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
-- Add indexes for performance optimization
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_trusted_devices_token ON "TrustedDevices"("DeviceToken");
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_trusted_devices_username ON "TrustedDevices"("UserName");
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices"("ExpiresAt");
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
-- No Encrypted password for 'support' user
|
|
115
|
+
INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "GuestRole")
|
|
116
|
+
VALUES ('support', '12345678', 'SuperAdmin', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
|
package/docs/env.md
CHANGED
|
@@ -42,6 +42,7 @@ This document describes the environment variables MBKAuth expects and keeps brie
|
|
|
42
42
|
- Description: PostgreSQL connection string for auth (must start with `postgresql://` or `postgres://`).
|
|
43
43
|
- Example: `"LOGIN_DB":"postgresql://user:pass@localhost:5432/mbkauth"`
|
|
44
44
|
- Required: Yes
|
|
45
|
+
- Create free postgres db: https://neon.com/
|
|
45
46
|
|
|
46
47
|
- MBKAUTH_TWO_FA_ENABLE
|
|
47
48
|
- Description: Enable Two-Factor Authentication.
|
|
@@ -67,6 +68,13 @@ This document describes the environment variables MBKAuth expects and keeps brie
|
|
|
67
68
|
- Example: `"DEVICE_TRUST_DURATION_DAYS":30`
|
|
68
69
|
- Required: No
|
|
69
70
|
|
|
71
|
+
- MAX_SESSIONS_PER_USER
|
|
72
|
+
- Description: Maximum number of concurrent application sessions allowed per user. When creating a new session that would exceed this number, the oldest session(s) for that user are pruned to make room for the new session.
|
|
73
|
+
- Default: `5`
|
|
74
|
+
- Example: `"MAX_SESSIONS_PER_USER": 10`
|
|
75
|
+
- Notes: Must be a positive integer. Validation is performed at startup by `lib/config/index.js`.
|
|
76
|
+
- Required: No
|
|
77
|
+
|
|
70
78
|
- loginRedirectURL
|
|
71
79
|
- Description: Post-login redirect path.
|
|
72
80
|
- Default: `/dashboard`
|
|
@@ -81,6 +89,8 @@ This document describes the environment variables MBKAuth expects and keeps brie
|
|
|
81
89
|
- GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
|
|
82
90
|
- Description: OAuth credentials (put in `mbkautheVar` preferred, or `mbkauthShared`).
|
|
83
91
|
- Required when provider enabled.
|
|
92
|
+
- Create Github OAuth App: https://github.com/settings/developers
|
|
93
|
+
- Create Google OAuth: https://console.cloud.google.com/
|
|
84
94
|
|
|
85
95
|
---
|
|
86
96
|
|
package/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ declare global {
|
|
|
23
23
|
username: string;
|
|
24
24
|
fullname?: string;
|
|
25
25
|
role: 'SuperAdmin' | 'NormalUser' | 'Guest';
|
|
26
|
-
sessionId
|
|
26
|
+
sessionId?: string;
|
|
27
27
|
allowedApps?: string[];
|
|
28
28
|
};
|
|
29
29
|
preAuthUser?: {
|
|
@@ -79,7 +79,7 @@ declare module 'mbkauthe' {
|
|
|
79
79
|
username: string;
|
|
80
80
|
fullname?: string;
|
|
81
81
|
role: UserRole;
|
|
82
|
-
sessionId
|
|
82
|
+
sessionId?: string;
|
|
83
83
|
allowedApps?: string[];
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -101,7 +101,7 @@ declare module 'mbkauthe' {
|
|
|
101
101
|
Role: UserRole;
|
|
102
102
|
Active: boolean;
|
|
103
103
|
AllowedApps: string[];
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
created_at?: Date;
|
|
106
106
|
updated_at?: Date;
|
|
107
107
|
last_login?: Date;
|
package/lib/config/cookies.js
CHANGED
|
@@ -32,6 +32,12 @@ export const generateDeviceToken = () => {
|
|
|
32
32
|
return crypto.randomBytes(32).toString('hex');
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Hash a device token for safe storage in the database
|
|
36
|
+
export const hashDeviceToken = (token) => {
|
|
37
|
+
if (!token || typeof token !== 'string') return null;
|
|
38
|
+
return crypto.createHmac('sha256').update(token).digest('hex');
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
export const getDeviceTokenCookieOptions = () => ({
|
|
36
42
|
maxAge: DEVICE_TRUST_DURATION_MS,
|
|
37
43
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
package/lib/config/index.js
CHANGED
|
@@ -62,9 +62,10 @@ function validateConfiguration() {
|
|
|
62
62
|
|
|
63
63
|
// Ensure specific keys are checked in mbkautheVar first, then mbkauthShared, then apply config defaults
|
|
64
64
|
const keysToCheck = [
|
|
65
|
-
"APP_NAME","DEVICE_TRUST_DURATION_DAYS","EncPass","Main_SECRET_TOKEN","SESSION_SECRET_KEY",
|
|
66
|
-
"LOGIN_DB","MBKAUTH_TWO_FA_ENABLE","COOKIE_EXPIRE_TIME","DOMAIN","loginRedirectURL",
|
|
67
|
-
"GITHUB_LOGIN_ENABLED","GITHUB_CLIENT_ID","GITHUB_CLIENT_SECRET","GOOGLE_LOGIN_ENABLED","GOOGLE_CLIENT_ID",
|
|
65
|
+
"APP_NAME","DEVICE_TRUST_DURATION_DAYS","EncPass","Main_SECRET_TOKEN","SESSION_SECRET_KEY",
|
|
66
|
+
"IS_DEPLOYED","LOGIN_DB","MBKAUTH_TWO_FA_ENABLE","COOKIE_EXPIRE_TIME","DOMAIN","loginRedirectURL",
|
|
67
|
+
"GITHUB_LOGIN_ENABLED","GITHUB_CLIENT_ID","GITHUB_CLIENT_SECRET","GOOGLE_LOGIN_ENABLED","GOOGLE_CLIENT_ID",
|
|
68
|
+
"GOOGLE_CLIENT_SECRET","MAX_SESSIONS_PER_USER"
|
|
68
69
|
];
|
|
69
70
|
|
|
70
71
|
const defaults = {
|
|
@@ -75,7 +76,8 @@ function validateConfiguration() {
|
|
|
75
76
|
COOKIE_EXPIRE_TIME: 2,
|
|
76
77
|
loginRedirectURL: '/dashboard',
|
|
77
78
|
GITHUB_LOGIN_ENABLED: 'false',
|
|
78
|
-
GOOGLE_LOGIN_ENABLED: 'false'
|
|
79
|
+
GOOGLE_LOGIN_ENABLED: 'false',
|
|
80
|
+
MAX_SESSIONS_PER_USER: 5
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
keysToCheck.forEach(key => {
|
|
@@ -183,6 +185,20 @@ function validateConfiguration() {
|
|
|
183
185
|
mbkautheVar.DEVICE_TRUST_DURATION_DAYS = 7;
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
// Validate MAX_SESSIONS_PER_USER if provided (must be positive integer)
|
|
189
|
+
if (mbkautheVar.MAX_SESSIONS_PER_USER !== undefined) {
|
|
190
|
+
const maxSessions = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
|
|
191
|
+
if (isNaN(maxSessions) || maxSessions <= 0) {
|
|
192
|
+
errors.push("mbkautheVar.MAX_SESSIONS_PER_USER must be a valid positive integer");
|
|
193
|
+
} else {
|
|
194
|
+
// Normalize to integer
|
|
195
|
+
mbkautheVar.MAX_SESSIONS_PER_USER = maxSessions;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Ensure default value is set
|
|
199
|
+
mbkautheVar.MAX_SESSIONS_PER_USER = 5;
|
|
200
|
+
}
|
|
201
|
+
|
|
186
202
|
// Validate LOGIN_DB connection string format
|
|
187
203
|
if (mbkautheVar.LOGIN_DB && !mbkautheVar.LOGIN_DB.startsWith('postgresql://') && !mbkautheVar.LOGIN_DB.startsWith('postgres://')) {
|
|
188
204
|
errors.push("mbkautheVar.LOGIN_DB must be a valid PostgreSQL connection string");
|
package/lib/config/security.js
CHANGED
package/lib/middleware/auth.js
CHANGED
|
@@ -33,19 +33,18 @@ async function validateSession(req, res, next) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// Normalize sessionId
|
|
37
|
-
const normalizedSessionId = sessionId
|
|
36
|
+
// Normalize sessionId (DB id) for consistent comparison
|
|
37
|
+
const normalizedSessionId = sessionId;
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
const query = `SELECT
|
|
41
|
-
|
|
39
|
+
// Validate session by DB primary key id and join to user
|
|
40
|
+
const query = `SELECT s.id as sid, s.expires_at, u."Active", u."Role"
|
|
41
|
+
FROM "Sessions" s
|
|
42
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
43
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
44
|
+
const result = await dblogin.query({ name: 'validate-app-session', text: query, values: [normalizedSessionId] });
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (result.rows.length > 0 && !result.rows[0].SessionId) {
|
|
46
|
-
console.warn(`[mbkauthe] DB sessionId is null for user "${req.session.user.username}"`);
|
|
47
|
-
}
|
|
48
|
-
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
46
|
+
if (result.rows.length === 0) {
|
|
47
|
+
console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
|
|
49
48
|
req.session.destroy();
|
|
50
49
|
clearSessionCookies(res);
|
|
51
50
|
return renderError(res, {
|
|
@@ -57,7 +56,25 @@ async function validateSession(req, res, next) {
|
|
|
57
56
|
});
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
const sessionRow = result.rows[0];
|
|
60
|
+
|
|
61
|
+
// Check expired
|
|
62
|
+
if (sessionRow.expires_at && new Date(sessionRow.expires_at) <= new Date()) {
|
|
63
|
+
console.log(`[mbkauthe] Session invalidated (expired) for user "${req.session.user.username}"`);
|
|
64
|
+
// destroy and clear cookies
|
|
65
|
+
req.session.destroy();
|
|
66
|
+
clearSessionCookies(res);
|
|
67
|
+
return renderError(res, {
|
|
68
|
+
code: 401,
|
|
69
|
+
error: "Session Expired",
|
|
70
|
+
message: "Your Session Has Expired. Please Log In Again.",
|
|
71
|
+
pagename: "Login",
|
|
72
|
+
page: `/mbkauthe/login?redirect=${encodeURIComponent(req.originalUrl)}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if (!sessionRow.Active) {
|
|
61
78
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
62
79
|
req.session.destroy();
|
|
63
80
|
clearSessionCookies(res);
|
|
@@ -117,24 +134,32 @@ async function validateApiSession(req, res, next) {
|
|
|
117
134
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
118
135
|
}
|
|
119
136
|
|
|
120
|
-
// Normalize sessionId
|
|
121
|
-
const normalizedSessionId = sessionId
|
|
137
|
+
// Normalize sessionId (DB id) for consistent comparison
|
|
138
|
+
const normalizedSessionId = sessionId;
|
|
122
139
|
|
|
123
|
-
//
|
|
124
|
-
const query = `SELECT
|
|
125
|
-
|
|
140
|
+
// Validate session by DB primary key id and join to user
|
|
141
|
+
const query = `SELECT s.id as sid, s.expires_at, u."Active", u."Role"
|
|
142
|
+
FROM "Sessions" s
|
|
143
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
144
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
145
|
+
const result = await dblogin.query({ name: 'validate-app-session-for-api', text: query, values: [normalizedSessionId] });
|
|
126
146
|
|
|
127
|
-
|
|
128
|
-
if (!dbSessionId || dbSessionId !== normalizedSessionId) {
|
|
129
|
-
if (result.rows.length > 0 && !result.rows[0].SessionId) {
|
|
130
|
-
console.warn(`[mbkauthe] DB sessionId is null for user "${req.session.user.username}"`);
|
|
131
|
-
}
|
|
132
|
-
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
147
|
+
if (result.rows.length === 0) {
|
|
133
148
|
req.session.destroy();
|
|
134
149
|
clearSessionCookies(res);
|
|
135
150
|
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_INVALID));
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
const sessionRow = result.rows[0];
|
|
154
|
+
|
|
155
|
+
// Check expired
|
|
156
|
+
if (sessionRow.expires_at && new Date(sessionRow.expires_at) <= new Date()) {
|
|
157
|
+
req.session.destroy();
|
|
158
|
+
clearSessionCookies(res);
|
|
159
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
138
163
|
if (!result.rows[0].Active) {
|
|
139
164
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
140
165
|
req.session.destroy();
|
|
@@ -176,21 +201,30 @@ export async function reloadSessionUser(req, res) {
|
|
|
176
201
|
try {
|
|
177
202
|
const { id, sessionId: currentSessionId } = req.session.user;
|
|
178
203
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
204
|
+
if (!currentSessionId) {
|
|
205
|
+
req.session.destroy(() => {});
|
|
206
|
+
clearSessionCookies(res);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const normalizedSessionId = String(currentSessionId);
|
|
211
|
+
const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
|
|
212
|
+
FROM "Sessions" s
|
|
213
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
214
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
215
|
+
const result = await dblogin.query({ name: 'reload-session-user', text: query, values: [normalizedSessionId] });
|
|
182
216
|
|
|
183
217
|
if (result.rows.length === 0) {
|
|
184
|
-
//
|
|
218
|
+
// Session not found — invalidate session
|
|
185
219
|
req.session.destroy(() => {});
|
|
186
220
|
clearSessionCookies(res);
|
|
187
221
|
return false;
|
|
188
222
|
}
|
|
189
223
|
|
|
190
224
|
const row = result.rows[0];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
225
|
+
|
|
226
|
+
// Check expired
|
|
227
|
+
if (row.expires_at && new Date(row.expires_at) <= new Date()) {
|
|
194
228
|
req.session.destroy(() => {});
|
|
195
229
|
clearSessionCookies(res);
|
|
196
230
|
return false;
|
package/lib/middleware/index.js
CHANGED
|
@@ -57,8 +57,8 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
57
57
|
if (!req.session.user && req.cookies.sessionId) {
|
|
58
58
|
const sessionId = req.cookies.sessionId;
|
|
59
59
|
|
|
60
|
-
// Early validation to avoid unnecessary processing
|
|
61
|
-
if (typeof sessionId !== 'string' || !/^[
|
|
60
|
+
// Early validation to avoid unnecessary processing (expect DB UUID id)
|
|
61
|
+
if (typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
62
62
|
// Clear invalid cookie to prevent repeated attempts
|
|
63
63
|
res.clearCookie('sessionId', {
|
|
64
64
|
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
@@ -71,37 +71,46 @@ export async function sessionRestorationMiddleware(req, res, next) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
// Validate session by DB primary key id and join to user
|
|
75
|
+
const query = `SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps", s.expires_at
|
|
76
|
+
FROM "Sessions" s
|
|
77
|
+
JOIN "Users" u ON s."UserName" = u."UserName"
|
|
78
|
+
WHERE s.id = $1 LIMIT 1`;
|
|
79
|
+
const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [sessionId] });
|
|
78
80
|
|
|
79
81
|
if (result.rows.length > 0) {
|
|
80
|
-
const
|
|
81
|
-
req.session.user = {
|
|
82
|
-
id: user.id,
|
|
83
|
-
username: user.UserName,
|
|
84
|
-
role: user.Role,
|
|
85
|
-
sessionId: normalizedSessionId,
|
|
86
|
-
allowedApps: user.AllowedApps,
|
|
87
|
-
};
|
|
82
|
+
const row = result.rows[0];
|
|
88
83
|
|
|
89
|
-
//
|
|
90
|
-
if (
|
|
91
|
-
|
|
84
|
+
// Reject expired sessions or inactive users
|
|
85
|
+
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
86
|
+
// leave cookies cleared and don't restore session
|
|
92
87
|
} else {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
const normalizedSessionId = String(sessionId);
|
|
89
|
+
req.session.user = {
|
|
90
|
+
id: row.id,
|
|
91
|
+
username: row.UserName,
|
|
92
|
+
role: row.Role,
|
|
93
|
+
sessionId: normalizedSessionId,
|
|
94
|
+
allowedApps: row.AllowedApps,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Use cached FullName from client cookie when available to avoid extra DB queries
|
|
98
|
+
if (req.cookies.fullName && typeof req.cookies.fullName === 'string') {
|
|
99
|
+
req.session.user.fullname = req.cookies.fullName;
|
|
100
|
+
} else {
|
|
101
|
+
// Fallback: attempt to fetch FullName from profiledata to populate session
|
|
102
|
+
try {
|
|
103
|
+
const profileRes = await dblogin.query({
|
|
104
|
+
name: 'restore-get-fullname',
|
|
105
|
+
text: 'SELECT "FullName" FROM "profiledata" WHERE "UserName" = $1 LIMIT 1',
|
|
106
|
+
values: [row.UserName]
|
|
107
|
+
});
|
|
108
|
+
if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {
|
|
109
|
+
req.session.user.fullname = profileRes.rows[0].FullName;
|
|
110
|
+
}
|
|
111
|
+
} catch (profileErr) {
|
|
112
|
+
console.error("[mbkauthe] Error fetching FullName during session restore:", profileErr);
|
|
102
113
|
}
|
|
103
|
-
} catch (profileErr) {
|
|
104
|
-
console.error("[mbkauthe] Error fetching FullName during session restore:", profileErr);
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
}
|
package/lib/routes/auth.js
CHANGED
|
@@ -7,7 +7,7 @@ import { dblogin } from "../database/pool.js";
|
|
|
7
7
|
import { mbkautheVar } from "../config/index.js";
|
|
8
8
|
import {
|
|
9
9
|
cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies,
|
|
10
|
-
generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS
|
|
10
|
+
generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS, hashDeviceToken
|
|
11
11
|
} from "../config/cookies.js";
|
|
12
12
|
import { packageJson } from "../config/index.js";
|
|
13
13
|
import { hashPassword } from "../config/security.js";
|
|
@@ -63,6 +63,8 @@ export async function checkTrustedDevice(req, username) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
try {
|
|
66
|
+
// Hash the provided device token before querying DB (we store token hashes in DB)
|
|
67
|
+
const deviceTokenHash = hashDeviceToken(deviceToken);
|
|
66
68
|
const deviceQuery = `
|
|
67
69
|
SELECT td."UserName", td."LastUsed", td."ExpiresAt", u."id", u."Active", u."Role", u."AllowedApps"
|
|
68
70
|
FROM "TrustedDevices" td
|
|
@@ -72,7 +74,7 @@ export async function checkTrustedDevice(req, username) {
|
|
|
72
74
|
const deviceResult = await dblogin.query({
|
|
73
75
|
name: 'check-trusted-device',
|
|
74
76
|
text: deviceQuery,
|
|
75
|
-
values: [
|
|
77
|
+
values: [deviceTokenHash, username]
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
if (deviceResult.rows.length > 0) {
|
|
@@ -95,7 +97,7 @@ export async function checkTrustedDevice(req, username) {
|
|
|
95
97
|
await dblogin.query({
|
|
96
98
|
name: 'update-device-last-used',
|
|
97
99
|
text: 'UPDATE "TrustedDevices" SET "LastUsed" = NOW() WHERE "DeviceToken" = $1',
|
|
98
|
-
values: [
|
|
100
|
+
values: [deviceTokenHash]
|
|
99
101
|
});
|
|
100
102
|
|
|
101
103
|
console.log(`[mbkauthe] Trusted device validated for user: ${username}`);
|
|
@@ -124,13 +126,9 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
124
126
|
throw new Error('Username is required in user object');
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
// smaller session id is sufficient and faster to generate/serialize
|
|
128
|
-
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
129
|
-
console.log(`[mbkauthe] Generated session ID for username: ${username}`);
|
|
130
|
-
|
|
131
129
|
// Fix session fixation: Delete old session BEFORE regenerating to prevent timing window
|
|
132
130
|
const oldSessionId = req.sessionID;
|
|
133
|
-
|
|
131
|
+
|
|
134
132
|
// Delete old session first to prevent session fixation attacks
|
|
135
133
|
await dblogin.query({
|
|
136
134
|
name: 'login-delete-old-session-before-regen',
|
|
@@ -146,27 +144,50 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
146
144
|
});
|
|
147
145
|
});
|
|
148
146
|
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
})
|
|
163
|
-
|
|
147
|
+
// Enforce max sessions per user (configurable via mbkautheVar.MAX_SESSIONS_PER_USER) and persist a new application session record (keyed by username)
|
|
148
|
+
const configuredMax = parseInt(mbkautheVar.MAX_SESSIONS_PER_USER, 10);
|
|
149
|
+
const MAX_SESSIONS = Number.isInteger(configuredMax) && configuredMax > 0 ? configuredMax : 5;
|
|
150
|
+
|
|
151
|
+
// Count active sessions for this user (by username)
|
|
152
|
+
const countRes = await dblogin.query({
|
|
153
|
+
name: 'count-user-sessions',
|
|
154
|
+
text: `SELECT id FROM "Sessions" WHERE "UserName" = $1 AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at ASC`,
|
|
155
|
+
values: [username]
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const currentSessions = countRes.rows.length;
|
|
159
|
+
if (currentSessions >= MAX_SESSIONS) {
|
|
160
|
+
console.log(`[mbkauthe] User "${username}" has ${currentSessions} active sessions, exceeding max of ${MAX_SESSIONS}. Pruning oldest sessions.`);
|
|
161
|
+
// prune the oldest session(s) to make room for the new one
|
|
162
|
+
await dblogin.query({
|
|
163
|
+
name: 'prune-oldest-user-session',
|
|
164
|
+
text: `DELETE FROM "Sessions" WHERE id IN (SELECT id FROM "Sessions" WHERE "UserName" = $1 AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at ASC LIMIT $2)` ,
|
|
165
|
+
values: [username, (currentSessions - (MAX_SESSIONS - 1))]
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
|
|
170
|
+
|
|
171
|
+
// Insert new session record for the user (store username) and return the DB id
|
|
172
|
+
const insertRes = await dblogin.query({
|
|
173
|
+
name: 'insert-app-session',
|
|
174
|
+
text: `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`,
|
|
175
|
+
values: [username, expiresAt, JSON.stringify({ ip: req.ip, ua: req.headers['user-agent'] || null })]
|
|
176
|
+
});
|
|
177
|
+
const dbSessionId = insertRes.rows[0].id;
|
|
178
|
+
|
|
179
|
+
// Update last_login timestamp for the user
|
|
180
|
+
await dblogin.query({
|
|
181
|
+
name: 'login-update-last-login',
|
|
182
|
+
text: `UPDATE "Users" SET "last_login" = NOW() WHERE "id" = $1`,
|
|
183
|
+
values: [user.id]
|
|
184
|
+
});
|
|
164
185
|
|
|
165
186
|
req.session.user = {
|
|
166
187
|
id: user.id,
|
|
167
188
|
username: username,
|
|
168
189
|
role: user.role || user.Role,
|
|
169
|
-
sessionId,
|
|
190
|
+
sessionId: dbSessionId,
|
|
170
191
|
allowedApps: user.allowedApps || user.AllowedApps,
|
|
171
192
|
};
|
|
172
193
|
|
|
@@ -194,11 +215,11 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
194
215
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
195
216
|
}
|
|
196
217
|
|
|
197
|
-
// Expose
|
|
198
|
-
res.cookie("sessionId",
|
|
218
|
+
// Expose DB session id and display name to client for UI (fullName falls back to username)
|
|
219
|
+
res.cookie("sessionId", dbSessionId, cachedCookieOptions);
|
|
199
220
|
res.cookie("fullName", req.session.user.fullname || username, { ...cachedCookieOptions, httpOnly: false });
|
|
200
221
|
|
|
201
|
-
// Handle trusted device if requested
|
|
222
|
+
// Handle trusted device if requested (token no longer stored in DB as token_hash)
|
|
202
223
|
if (trustDevice) {
|
|
203
224
|
try {
|
|
204
225
|
const deviceToken = generateDeviceToken();
|
|
@@ -208,13 +229,16 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
208
229
|
const ipAddress = req.ip || req.connection.remoteAddress || 'Unknown';
|
|
209
230
|
const expiresAt = new Date(Date.now() + DEVICE_TRUST_DURATION_MS);
|
|
210
231
|
|
|
232
|
+
// Store only the HASH of the device token in DB; send the raw token to the client (httpOnly cookie)
|
|
233
|
+
const deviceTokenHash = hashDeviceToken(deviceToken);
|
|
211
234
|
await dblogin.query({
|
|
212
235
|
name: 'insert-trusted-device',
|
|
213
236
|
text: `INSERT INTO "TrustedDevices" ("UserName", "DeviceToken", "DeviceName", "UserAgent", "IpAddress", "ExpiresAt")
|
|
214
237
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
215
|
-
values: [username,
|
|
238
|
+
values: [username, deviceTokenHash, deviceName, userAgent, ipAddress, expiresAt]
|
|
216
239
|
});
|
|
217
240
|
|
|
241
|
+
// Send raw token to client as httpOnly cookie only
|
|
218
242
|
res.cookie("device_token", deviceToken, getDeviceTokenCookieOptions());
|
|
219
243
|
console.log(`[mbkauthe] Trusted device token created for user: ${username}`);
|
|
220
244
|
} catch (deviceErr) {
|
|
@@ -228,7 +252,7 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
228
252
|
const responsePayload = {
|
|
229
253
|
success: true,
|
|
230
254
|
message: "Login successful",
|
|
231
|
-
sessionId,
|
|
255
|
+
sessionId: dbSessionId,
|
|
232
256
|
};
|
|
233
257
|
|
|
234
258
|
if (redirectUrl) {
|
|
@@ -506,15 +530,14 @@ router.post("/api/logout", LogoutLimit, async (req, res) => {
|
|
|
506
530
|
try {
|
|
507
531
|
const { id, username } = req.session.user;
|
|
508
532
|
|
|
509
|
-
//
|
|
510
|
-
const operations = [
|
|
511
|
-
|
|
512
|
-
|
|
533
|
+
// Remove the application session record for this token (if present)
|
|
534
|
+
const operations = [];
|
|
535
|
+
if (req.session && req.session.user && req.session.user.sessionId) {
|
|
536
|
+
operations.push(dblogin.query({ name: 'logout-delete-app-session', text: 'DELETE FROM "Sessions" WHERE id = $1', values: [req.session.user.sessionId] }));
|
|
537
|
+
}
|
|
513
538
|
|
|
514
539
|
if (req.sessionID) {
|
|
515
|
-
operations.push(
|
|
516
|
-
dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
|
|
517
|
-
);
|
|
540
|
+
operations.push(dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] }));
|
|
518
541
|
}
|
|
519
542
|
|
|
520
543
|
await Promise.all(operations);
|
package/lib/routes/misc.js
CHANGED
|
@@ -79,10 +79,15 @@ router.get('/test', validateSession, LoginLimit, async (req, res) => {
|
|
|
79
79
|
<p class="success">✅ Authentication successful! User is logged in.</p>
|
|
80
80
|
<p>Welcome, <strong>${req.session.user.username}</strong>! Your role: <strong>${req.session.user.role}</strong></p>
|
|
81
81
|
<div class="user-info">
|
|
82
|
+
Username: ${req.session.user.username}<br>
|
|
83
|
+
Role: ${req.session.user.role}<br>
|
|
84
|
+
Full Name: ${req.session.user.fullname || 'N/A'}<br>
|
|
82
85
|
User ID: ${req.session.user.id}<br>
|
|
83
86
|
Session ID: ${req.session.user.sessionId.slice(0, 5)}...
|
|
84
87
|
</div>
|
|
85
88
|
<button onclick="logout()">Logout</button>
|
|
89
|
+
<a href="https://portal.mbktech.org/">Web Portal</a>
|
|
90
|
+
<a href="https://portal.mbktech.org/user/settings">User Settings</a>
|
|
86
91
|
<a href="/mbkauthe/info">Info Page</a>
|
|
87
92
|
<a href="/mbkauthe/login">Login Page</a>
|
|
88
93
|
</div>
|
|
@@ -104,19 +109,27 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
|
|
|
104
109
|
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
const
|
|
108
|
-
const result = await dblogin.query({ name: 'check-session-validity', text: 'SELECT "SessionId", "Active" FROM "Users" WHERE id = $1', values: [id] });
|
|
112
|
+
const result = await dblogin.query({ name: 'check-session-validity', text: `SELECT s.expires_at, u."Active" FROM "Sessions" s JOIN "Users" u ON s."UserName" = u."UserName" WHERE s.id = $1 LIMIT 1`, values: [sessionId] });
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
if (!dbSessionId || dbSessionId !== normalizedSessionId || !result.rows[0].Active) {
|
|
114
|
+
if (result.rows.length === 0) {
|
|
112
115
|
req.session.destroy(() => { });
|
|
113
116
|
clearSessionCookies(res);
|
|
114
117
|
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
const row = result.rows[0];
|
|
121
|
+
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
|
|
122
|
+
req.session.destroy(() => { });
|
|
123
|
+
clearSessionCookies(res);
|
|
124
|
+
return res.status(200).json({ sessionValid: false, expiry: null });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Determine expiry: prefer application session expiry if present else fallback to connect-pg-simple expire
|
|
128
|
+
let expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
|
|
129
|
+
if (!expiry) {
|
|
130
|
+
const sessResult = await dblogin.query({ name: 'get-session-expiry', text: 'SELECT expire FROM "session" WHERE sid = $1', values: [req.sessionID] });
|
|
131
|
+
expiry = sessResult.rows.length > 0 && sessResult.rows[0].expire ? new Date(sessResult.rows[0].expire).toISOString() : null;
|
|
132
|
+
}
|
|
120
133
|
|
|
121
134
|
return res.status(200).json({ sessionValid: true, expiry });
|
|
122
135
|
} catch (err) {
|
|
@@ -287,8 +300,8 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
|
|
|
287
300
|
// Run both operations in parallel for better performance
|
|
288
301
|
await Promise.all([
|
|
289
302
|
dblogin.query({
|
|
290
|
-
name: 'terminate-all-
|
|
291
|
-
text: '
|
|
303
|
+
name: 'terminate-all-app-sessions',
|
|
304
|
+
text: 'DELETE FROM "Sessions"'
|
|
292
305
|
}),
|
|
293
306
|
dblogin.query({
|
|
294
307
|
name: 'terminate-all-db-sessions',
|
package/package.json
CHANGED
|
@@ -167,8 +167,6 @@
|
|
|
167
167
|
window.location.href = `/mbkauthe/2fa${redirectQuery}`;
|
|
168
168
|
} else {
|
|
169
169
|
loginButtonText.textContent = 'Success! Redirecting...';
|
|
170
|
-
sessionStorage.setItem('sessionId', data.sessionId);
|
|
171
|
-
|
|
172
170
|
if (rememberMe) {
|
|
173
171
|
setCookie('rememberedUsername', username, 30); // 30 days
|
|
174
172
|
} else {
|