strapi-plugin-magic-sessionmanager 3.2.1 → 3.3.1
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 +154 -44
- package/dist/server/index.js +92 -5
- package/dist/server/index.mjs +92 -5
- package/package.json +1 -1
- package/server/src/bootstrap.js +103 -1
- package/server/src/content-types/session/schema.json +4 -0
- package/server/src/services/session.js +7 -5
package/README.md
CHANGED
|
@@ -349,7 +349,7 @@ Log: "Cleaned up X inactive sessions"
|
|
|
349
349
|
2. Inactivity timeout triggers cleanup
|
|
350
350
|
3. Admin terminates the session
|
|
351
351
|
|
|
352
|
-
#### Refresh Tokens
|
|
352
|
+
#### Refresh Tokens ✅ **SOLVED!**
|
|
353
353
|
|
|
354
354
|
**What are Refresh Tokens?**
|
|
355
355
|
Refresh tokens allow users to get new Access Tokens (JWTs) without re-entering credentials. This enables longer sessions:
|
|
@@ -360,85 +360,195 @@ Access Token expires after 30 min
|
|
|
360
360
|
User still has Refresh Token
|
|
361
361
|
↓
|
|
362
362
|
User requests new Access Token:
|
|
363
|
-
POST /api/auth/refresh
|
|
363
|
+
POST /api/auth/refresh
|
|
364
364
|
↓
|
|
365
365
|
Strapi issues new JWT
|
|
366
366
|
↓
|
|
367
367
|
User continues without re-login
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
-
**The
|
|
371
|
-
- **Stored:**
|
|
372
|
-
- **
|
|
373
|
-
- **
|
|
370
|
+
**The Solution (v3.2+):**
|
|
371
|
+
- **Stored:** YES - Refresh tokens are encrypted and stored with sessions ✅
|
|
372
|
+
- **Tracked:** YES - Middleware intercepts `/api/auth/refresh` requests ✅
|
|
373
|
+
- **Validated:** YES - Checks if session is still active before issuing new tokens ✅
|
|
374
|
+
|
|
375
|
+
**How It Works:**
|
|
374
376
|
|
|
375
|
-
**Scenario:**
|
|
376
377
|
```
|
|
377
|
-
|
|
378
|
+
Login: User gets JWT + Refresh Token
|
|
379
|
+
↓
|
|
380
|
+
Both tokens encrypted and stored in session
|
|
381
|
+
↓
|
|
382
|
+
Admin terminates session
|
|
383
|
+
↓
|
|
384
|
+
Session: isActive = false ❌
|
|
378
385
|
↓
|
|
379
|
-
|
|
386
|
+
User tries to refresh token:
|
|
387
|
+
POST /api/auth/refresh
|
|
388
|
+
{ refreshToken: "..." }
|
|
380
389
|
↓
|
|
381
|
-
|
|
382
|
-
User has refresh token
|
|
390
|
+
[Refresh Token Middleware]
|
|
383
391
|
↓
|
|
384
|
-
|
|
392
|
+
Decrypt all active session refresh tokens
|
|
385
393
|
↓
|
|
386
|
-
|
|
394
|
+
Find matching session
|
|
387
395
|
↓
|
|
388
|
-
|
|
396
|
+
Session found but isActive = false?
|
|
397
|
+
→ BLOCK! Return 401 Unauthorized ❌
|
|
398
|
+
→ Message: "Session terminated. Please login again."
|
|
399
|
+
↓
|
|
400
|
+
Session found and isActive = true?
|
|
401
|
+
→ ALLOW! ✅
|
|
402
|
+
→ Strapi issues new tokens
|
|
403
|
+
→ Session updated with new encrypted tokens
|
|
389
404
|
```
|
|
390
405
|
|
|
391
|
-
**
|
|
392
|
-
|
|
406
|
+
**Security Benefits:**
|
|
407
|
+
|
|
408
|
+
✅ **Session termination is FINAL** - User cannot get new tokens
|
|
409
|
+
✅ **Refresh tokens tracked** - Encrypted & stored securely
|
|
410
|
+
✅ **Token rotation** - New tokens automatically updated in session
|
|
411
|
+
✅ **Admin control** - Force logout works even with refresh tokens
|
|
412
|
+
|
|
413
|
+
**Configuration:**
|
|
393
414
|
|
|
394
|
-
|
|
415
|
+
Enable refresh tokens in Strapi:
|
|
395
416
|
|
|
396
|
-
**Option 1: Disable Refresh Tokens (Strict Control)**
|
|
397
417
|
```typescript
|
|
398
418
|
// src/config/plugins.ts
|
|
399
419
|
export default () => ({
|
|
400
420
|
'users-permissions': {
|
|
401
421
|
config: {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
//
|
|
422
|
+
jwtManagement: 'refresh', // Enable refresh tokens
|
|
423
|
+
sessions: {
|
|
424
|
+
accessTokenLifespan: 3600, // 1 hour (in seconds)
|
|
425
|
+
maxRefreshTokenLifespan: 2592000, // 30 days
|
|
426
|
+
idleRefreshTokenLifespan: 604800, // 7 days idle
|
|
405
427
|
},
|
|
406
428
|
},
|
|
407
429
|
},
|
|
408
430
|
'magic-sessionmanager': {
|
|
431
|
+
enabled: true,
|
|
409
432
|
config: {
|
|
410
|
-
inactivityTimeout: 15 * 60 * 1000, //
|
|
433
|
+
inactivityTimeout: 15 * 60 * 1000, // 15 minutes
|
|
411
434
|
},
|
|
412
435
|
},
|
|
413
436
|
});
|
|
414
437
|
```
|
|
415
438
|
|
|
416
|
-
**
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
439
|
+
**Testing Refresh Token Blocking:**
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
# 1. Login and get tokens
|
|
443
|
+
curl -X POST http://localhost:1337/api/auth/local \
|
|
444
|
+
-H "Content-Type: application/json" \
|
|
445
|
+
-d '{"identifier":"user@example.com","password":"pass"}'
|
|
446
|
+
|
|
447
|
+
# Save both tokens:
|
|
448
|
+
ACCESS_TOKEN="eyJhbGci..."
|
|
449
|
+
REFRESH_TOKEN="abc123..."
|
|
450
|
+
|
|
451
|
+
# 2. Admin terminates session
|
|
452
|
+
# Go to Admin → Sessions → Find session → Terminate
|
|
453
|
+
|
|
454
|
+
# 3. Try to refresh token
|
|
455
|
+
curl -X POST http://localhost:1337/api/auth/refresh \
|
|
456
|
+
-H "Content-Type: application/json" \
|
|
457
|
+
-d "{\"refreshToken\":\"$REFRESH_TOKEN\"}"
|
|
458
|
+
|
|
459
|
+
# Expected: 401 Unauthorized
|
|
460
|
+
# "Session terminated. Please login again."
|
|
426
461
|
```
|
|
427
462
|
|
|
428
|
-
**
|
|
429
|
-
Understand that:
|
|
430
|
-
- Session termination stops the **current** JWT
|
|
431
|
-
- Users with refresh tokens can get **new** JWTs
|
|
432
|
-
- New JWTs create **new** sessions
|
|
433
|
-
- Use session analytics to detect unusual patterns
|
|
463
|
+
**This completely solves the refresh token security gap!** 🔒
|
|
434
464
|
|
|
435
|
-
|
|
436
|
-
To fully block users, the plugin would need to:
|
|
437
|
-
1. Track refresh tokens (complex)
|
|
438
|
-
2. Hook into Strapi's token refresh endpoint
|
|
439
|
-
3. Validate against active sessions before issuing new JWTs
|
|
465
|
+
### Without Refresh Tokens (Default Behavior)
|
|
440
466
|
|
|
441
|
-
|
|
467
|
+
If you **don't enable** refresh tokens (`jwtManagement: 'refresh'`):
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
Login: User gets JWT (no refresh token)
|
|
471
|
+
↓
|
|
472
|
+
JWT stored in session (encrypted)
|
|
473
|
+
↓
|
|
474
|
+
JWT expires after 30 min (or configured time)
|
|
475
|
+
↓
|
|
476
|
+
User must re-login ❌
|
|
477
|
+
↓
|
|
478
|
+
No automatic token refresh
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Behavior:**
|
|
482
|
+
- ✅ Session Manager works normally
|
|
483
|
+
- ✅ Sessions tracked, logout works
|
|
484
|
+
- ✅ Force logout works (no refresh token bypass possible)
|
|
485
|
+
- ⚠️ Users must re-login when JWT expires
|
|
486
|
+
- ℹ️ No refresh token middleware runs (skipped)
|
|
487
|
+
|
|
488
|
+
**Logs when refresh tokens disabled:**
|
|
489
|
+
```
|
|
490
|
+
[magic-sessionmanager] ✅ Session created for user 1 (IP: 192.168.1.1)
|
|
491
|
+
[magic-sessionmanager] ℹ️ No refresh token in response (JWT management not enabled)
|
|
492
|
+
[magic-sessionmanager] ✅ Refresh Token interceptor middleware mounted
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**If you try to call `/api/auth/refresh` without enabling it:**
|
|
496
|
+
- Endpoint returns **404 Not Found** (Strapi doesn't create the route)
|
|
497
|
+
- Or returns **401 Unauthorized** if route exists but tokens not configured
|
|
498
|
+
- This is expected and correct behavior
|
|
499
|
+
|
|
500
|
+
**Trade-offs:**
|
|
501
|
+
|
|
502
|
+
| Feature | With Refresh Tokens | Without Refresh Tokens |
|
|
503
|
+
|---------|---------------------|------------------------|
|
|
504
|
+
| User Experience | ✅ Seamless (auto-refresh) | ⚠️ Must re-login |
|
|
505
|
+
| Security | ✅ Tracked & blockable | ✅ No bypass risk |
|
|
506
|
+
| Session Duration | Long (days/weeks) | Short (hours) |
|
|
507
|
+
| Force Logout | ✅ Complete | ✅ Complete |
|
|
508
|
+
|
|
509
|
+
**Recommendation:**
|
|
510
|
+
|
|
511
|
+
**Enable refresh tokens** for better UX + use this plugin to secure them! 🔒
|
|
512
|
+
|
|
513
|
+
**Testing in Postman:**
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
1. Login (get JWT + refreshToken)
|
|
517
|
+
POST /api/auth/local
|
|
518
|
+
→ Save: jwt, refreshToken, session_id
|
|
519
|
+
|
|
520
|
+
2. Refresh Token (should work)
|
|
521
|
+
POST /api/auth/refresh
|
|
522
|
+
Body: { "refreshToken": "..." }
|
|
523
|
+
→ Returns: New jwt + refreshToken ✅
|
|
524
|
+
|
|
525
|
+
3. Admin terminates session
|
|
526
|
+
POST /magic-sessionmanager/sessions/:id/terminate
|
|
527
|
+
|
|
528
|
+
4. Try refresh token again
|
|
529
|
+
POST /api/auth/refresh
|
|
530
|
+
Body: { "refreshToken": "..." }
|
|
531
|
+
→ Returns: 401 Unauthorized ✅
|
|
532
|
+
→ Message: "Session terminated. Please login again."
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Run Automated Test:**
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
cd /path/to/magic-sessionmanager
|
|
539
|
+
|
|
540
|
+
# Set environment variables
|
|
541
|
+
export TEST_USER_EMAIL=user@example.com
|
|
542
|
+
export TEST_USER_PASSWORD=password123
|
|
543
|
+
export ADMIN_EMAIL=admin@example.com
|
|
544
|
+
export ADMIN_PASSWORD=adminpass
|
|
545
|
+
|
|
546
|
+
# Run test suite
|
|
547
|
+
node test-session-manager.js
|
|
548
|
+
|
|
549
|
+
# Look for "USER TEST 5: Blocked Refresh Token Test"
|
|
550
|
+
# Should show: ✅ Refresh token BLOCKED as expected!
|
|
551
|
+
```
|
|
442
552
|
|
|
443
553
|
### Multi-Login Behavior
|
|
444
554
|
|
package/dist/server/index.js
CHANGED
|
@@ -350,8 +350,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
350
350
|
userId: user.id,
|
|
351
351
|
ip,
|
|
352
352
|
userAgent,
|
|
353
|
-
token: ctx.body.jwt
|
|
354
|
-
// Store
|
|
353
|
+
token: ctx.body.jwt,
|
|
354
|
+
// Store Access Token (encrypted)
|
|
355
|
+
refreshToken: ctx.body.refreshToken
|
|
356
|
+
// Store Refresh Token (encrypted) if exists
|
|
355
357
|
});
|
|
356
358
|
strapi2.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
|
|
357
359
|
if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
|
|
@@ -396,6 +398,84 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
396
398
|
}
|
|
397
399
|
});
|
|
398
400
|
strapi2.log.info("[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted");
|
|
401
|
+
strapi2.server.use(async (ctx, next) => {
|
|
402
|
+
const isRefreshToken = ctx.path === "/api/auth/refresh" && ctx.method === "POST";
|
|
403
|
+
if (isRefreshToken) {
|
|
404
|
+
try {
|
|
405
|
+
const refreshToken = ctx.request.body?.refreshToken;
|
|
406
|
+
if (refreshToken) {
|
|
407
|
+
const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
|
|
408
|
+
filters: {
|
|
409
|
+
isActive: true
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const matchingSession = allSessions.find((session2) => {
|
|
413
|
+
if (!session2.refreshToken) return false;
|
|
414
|
+
try {
|
|
415
|
+
const decrypted = decryptToken$2(session2.refreshToken);
|
|
416
|
+
return decrypted === refreshToken;
|
|
417
|
+
} catch (err) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
if (!matchingSession) {
|
|
422
|
+
strapi2.log.warn("[magic-sessionmanager] 🚫 Blocked refresh token request - no active session");
|
|
423
|
+
ctx.status = 401;
|
|
424
|
+
ctx.body = {
|
|
425
|
+
error: {
|
|
426
|
+
status: 401,
|
|
427
|
+
message: "Session terminated. Please login again.",
|
|
428
|
+
name: "UnauthorizedError"
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
strapi2.log.info(`[magic-sessionmanager] ✅ Refresh token allowed for session ${matchingSession.id}`);
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
strapi2.log.error("[magic-sessionmanager] Error checking refresh token:", err);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
await next();
|
|
440
|
+
if (isRefreshToken && ctx.status === 200 && ctx.body && ctx.body.jwt) {
|
|
441
|
+
try {
|
|
442
|
+
const oldRefreshToken = ctx.request.body?.refreshToken;
|
|
443
|
+
const newAccessToken = ctx.body.jwt;
|
|
444
|
+
const newRefreshToken = ctx.body.refreshToken;
|
|
445
|
+
if (oldRefreshToken) {
|
|
446
|
+
const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
|
|
447
|
+
filters: {
|
|
448
|
+
isActive: true
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
const matchingSession = allSessions.find((session2) => {
|
|
452
|
+
if (!session2.refreshToken) return false;
|
|
453
|
+
try {
|
|
454
|
+
const decrypted = decryptToken$2(session2.refreshToken);
|
|
455
|
+
return decrypted === oldRefreshToken;
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
if (matchingSession) {
|
|
461
|
+
const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
|
|
462
|
+
const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
|
|
463
|
+
await strapi2.entityService.update("plugin::magic-sessionmanager.session", matchingSession.id, {
|
|
464
|
+
data: {
|
|
465
|
+
token: encryptedToken,
|
|
466
|
+
refreshToken: encryptedRefreshToken,
|
|
467
|
+
lastActive: /* @__PURE__ */ new Date()
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
strapi2.log.info(`[magic-sessionmanager] 🔄 Tokens refreshed for session ${matchingSession.id}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch (err) {
|
|
474
|
+
strapi2.log.error("[magic-sessionmanager] Error updating refreshed tokens:", err);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
strapi2.log.info("[magic-sessionmanager] ✅ Refresh Token interceptor middleware mounted");
|
|
399
479
|
strapi2.server.use(
|
|
400
480
|
lastSeen({ strapi: strapi2, sessionService })
|
|
401
481
|
);
|
|
@@ -480,6 +560,10 @@ const attributes = {
|
|
|
480
560
|
type: "text",
|
|
481
561
|
"private": true
|
|
482
562
|
},
|
|
563
|
+
refreshToken: {
|
|
564
|
+
type: "text",
|
|
565
|
+
"private": true
|
|
566
|
+
},
|
|
483
567
|
loginTime: {
|
|
484
568
|
type: "datetime",
|
|
485
569
|
required: true
|
|
@@ -1317,14 +1401,15 @@ const { encryptToken, decryptToken, generateSessionId } = encryption;
|
|
|
1317
1401
|
var session$1 = ({ strapi: strapi2 }) => ({
|
|
1318
1402
|
/**
|
|
1319
1403
|
* Create a new session record
|
|
1320
|
-
* @param {Object} params - { userId, ip, userAgent, token }
|
|
1404
|
+
* @param {Object} params - { userId, ip, userAgent, token, refreshToken }
|
|
1321
1405
|
* @returns {Promise<Object>} Created session
|
|
1322
1406
|
*/
|
|
1323
|
-
async createSession({ userId, ip = "unknown", userAgent = "unknown", token }) {
|
|
1407
|
+
async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken }) {
|
|
1324
1408
|
try {
|
|
1325
1409
|
const now = /* @__PURE__ */ new Date();
|
|
1326
1410
|
const sessionId = generateSessionId(userId);
|
|
1327
1411
|
const encryptedToken = token ? encryptToken(token) : null;
|
|
1412
|
+
const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
|
|
1328
1413
|
const session2 = await strapi2.entityService.create("plugin::magic-sessionmanager.session", {
|
|
1329
1414
|
data: {
|
|
1330
1415
|
user: userId,
|
|
@@ -1334,7 +1419,9 @@ var session$1 = ({ strapi: strapi2 }) => ({
|
|
|
1334
1419
|
lastActive: now,
|
|
1335
1420
|
isActive: true,
|
|
1336
1421
|
token: encryptedToken,
|
|
1337
|
-
// ✅ Encrypted
|
|
1422
|
+
// ✅ Encrypted Access Token
|
|
1423
|
+
refreshToken: encryptedRefreshToken,
|
|
1424
|
+
// ✅ Encrypted Refresh Token
|
|
1338
1425
|
sessionId
|
|
1339
1426
|
// ✅ Unique identifier
|
|
1340
1427
|
}
|
package/dist/server/index.mjs
CHANGED
|
@@ -346,8 +346,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
346
346
|
userId: user.id,
|
|
347
347
|
ip,
|
|
348
348
|
userAgent,
|
|
349
|
-
token: ctx.body.jwt
|
|
350
|
-
// Store
|
|
349
|
+
token: ctx.body.jwt,
|
|
350
|
+
// Store Access Token (encrypted)
|
|
351
|
+
refreshToken: ctx.body.refreshToken
|
|
352
|
+
// Store Refresh Token (encrypted) if exists
|
|
351
353
|
});
|
|
352
354
|
strapi2.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
|
|
353
355
|
if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
|
|
@@ -392,6 +394,84 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
392
394
|
}
|
|
393
395
|
});
|
|
394
396
|
strapi2.log.info("[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted");
|
|
397
|
+
strapi2.server.use(async (ctx, next) => {
|
|
398
|
+
const isRefreshToken = ctx.path === "/api/auth/refresh" && ctx.method === "POST";
|
|
399
|
+
if (isRefreshToken) {
|
|
400
|
+
try {
|
|
401
|
+
const refreshToken = ctx.request.body?.refreshToken;
|
|
402
|
+
if (refreshToken) {
|
|
403
|
+
const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
|
|
404
|
+
filters: {
|
|
405
|
+
isActive: true
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
const matchingSession = allSessions.find((session2) => {
|
|
409
|
+
if (!session2.refreshToken) return false;
|
|
410
|
+
try {
|
|
411
|
+
const decrypted = decryptToken$2(session2.refreshToken);
|
|
412
|
+
return decrypted === refreshToken;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
if (!matchingSession) {
|
|
418
|
+
strapi2.log.warn("[magic-sessionmanager] 🚫 Blocked refresh token request - no active session");
|
|
419
|
+
ctx.status = 401;
|
|
420
|
+
ctx.body = {
|
|
421
|
+
error: {
|
|
422
|
+
status: 401,
|
|
423
|
+
message: "Session terminated. Please login again.",
|
|
424
|
+
name: "UnauthorizedError"
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
strapi2.log.info(`[magic-sessionmanager] ✅ Refresh token allowed for session ${matchingSession.id}`);
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
strapi2.log.error("[magic-sessionmanager] Error checking refresh token:", err);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await next();
|
|
436
|
+
if (isRefreshToken && ctx.status === 200 && ctx.body && ctx.body.jwt) {
|
|
437
|
+
try {
|
|
438
|
+
const oldRefreshToken = ctx.request.body?.refreshToken;
|
|
439
|
+
const newAccessToken = ctx.body.jwt;
|
|
440
|
+
const newRefreshToken = ctx.body.refreshToken;
|
|
441
|
+
if (oldRefreshToken) {
|
|
442
|
+
const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
|
|
443
|
+
filters: {
|
|
444
|
+
isActive: true
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
const matchingSession = allSessions.find((session2) => {
|
|
448
|
+
if (!session2.refreshToken) return false;
|
|
449
|
+
try {
|
|
450
|
+
const decrypted = decryptToken$2(session2.refreshToken);
|
|
451
|
+
return decrypted === oldRefreshToken;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
if (matchingSession) {
|
|
457
|
+
const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
|
|
458
|
+
const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
|
|
459
|
+
await strapi2.entityService.update("plugin::magic-sessionmanager.session", matchingSession.id, {
|
|
460
|
+
data: {
|
|
461
|
+
token: encryptedToken,
|
|
462
|
+
refreshToken: encryptedRefreshToken,
|
|
463
|
+
lastActive: /* @__PURE__ */ new Date()
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
strapi2.log.info(`[magic-sessionmanager] 🔄 Tokens refreshed for session ${matchingSession.id}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
strapi2.log.error("[magic-sessionmanager] Error updating refreshed tokens:", err);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
strapi2.log.info("[magic-sessionmanager] ✅ Refresh Token interceptor middleware mounted");
|
|
395
475
|
strapi2.server.use(
|
|
396
476
|
lastSeen({ strapi: strapi2, sessionService })
|
|
397
477
|
);
|
|
@@ -476,6 +556,10 @@ const attributes = {
|
|
|
476
556
|
type: "text",
|
|
477
557
|
"private": true
|
|
478
558
|
},
|
|
559
|
+
refreshToken: {
|
|
560
|
+
type: "text",
|
|
561
|
+
"private": true
|
|
562
|
+
},
|
|
479
563
|
loginTime: {
|
|
480
564
|
type: "datetime",
|
|
481
565
|
required: true
|
|
@@ -1313,14 +1397,15 @@ const { encryptToken, decryptToken, generateSessionId } = encryption;
|
|
|
1313
1397
|
var session$1 = ({ strapi: strapi2 }) => ({
|
|
1314
1398
|
/**
|
|
1315
1399
|
* Create a new session record
|
|
1316
|
-
* @param {Object} params - { userId, ip, userAgent, token }
|
|
1400
|
+
* @param {Object} params - { userId, ip, userAgent, token, refreshToken }
|
|
1317
1401
|
* @returns {Promise<Object>} Created session
|
|
1318
1402
|
*/
|
|
1319
|
-
async createSession({ userId, ip = "unknown", userAgent = "unknown", token }) {
|
|
1403
|
+
async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken }) {
|
|
1320
1404
|
try {
|
|
1321
1405
|
const now = /* @__PURE__ */ new Date();
|
|
1322
1406
|
const sessionId = generateSessionId(userId);
|
|
1323
1407
|
const encryptedToken = token ? encryptToken(token) : null;
|
|
1408
|
+
const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
|
|
1324
1409
|
const session2 = await strapi2.entityService.create("plugin::magic-sessionmanager.session", {
|
|
1325
1410
|
data: {
|
|
1326
1411
|
user: userId,
|
|
@@ -1330,7 +1415,9 @@ var session$1 = ({ strapi: strapi2 }) => ({
|
|
|
1330
1415
|
lastActive: now,
|
|
1331
1416
|
isActive: true,
|
|
1332
1417
|
token: encryptedToken,
|
|
1333
|
-
// ✅ Encrypted
|
|
1418
|
+
// ✅ Encrypted Access Token
|
|
1419
|
+
refreshToken: encryptedRefreshToken,
|
|
1420
|
+
// ✅ Encrypted Refresh Token
|
|
1334
1421
|
sessionId
|
|
1335
1422
|
// ✅ Unique identifier
|
|
1336
1423
|
}
|
package/package.json
CHANGED
package/server/src/bootstrap.js
CHANGED
|
@@ -235,7 +235,8 @@ module.exports = async ({ strapi }) => {
|
|
|
235
235
|
userId: user.id,
|
|
236
236
|
ip,
|
|
237
237
|
userAgent,
|
|
238
|
-
token: ctx.body.jwt,
|
|
238
|
+
token: ctx.body.jwt, // Store Access Token (encrypted)
|
|
239
|
+
refreshToken: ctx.body.refreshToken, // Store Refresh Token (encrypted) if exists
|
|
239
240
|
});
|
|
240
241
|
|
|
241
242
|
strapi.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
|
|
@@ -293,6 +294,107 @@ module.exports = async ({ strapi }) => {
|
|
|
293
294
|
|
|
294
295
|
strapi.log.info('[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted');
|
|
295
296
|
|
|
297
|
+
// Middleware to block refresh token requests for terminated sessions
|
|
298
|
+
strapi.server.use(async (ctx, next) => {
|
|
299
|
+
// Check if this is a refresh token request (Strapi v5: /api/auth/refresh)
|
|
300
|
+
const isRefreshToken = ctx.path === '/api/auth/refresh' && ctx.method === 'POST';
|
|
301
|
+
|
|
302
|
+
if (isRefreshToken) {
|
|
303
|
+
try {
|
|
304
|
+
const refreshToken = ctx.request.body?.refreshToken;
|
|
305
|
+
|
|
306
|
+
if (refreshToken) {
|
|
307
|
+
// Find session with this refresh token
|
|
308
|
+
const allSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
309
|
+
filters: {
|
|
310
|
+
isActive: true,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Find matching session by decrypting and comparing refresh tokens
|
|
315
|
+
const matchingSession = allSessions.find(session => {
|
|
316
|
+
if (!session.refreshToken) return false;
|
|
317
|
+
try {
|
|
318
|
+
const decrypted = decryptToken(session.refreshToken);
|
|
319
|
+
return decrypted === refreshToken;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (!matchingSession) {
|
|
326
|
+
// No active session with this refresh token → Block!
|
|
327
|
+
strapi.log.warn('[magic-sessionmanager] 🚫 Blocked refresh token request - no active session');
|
|
328
|
+
ctx.status = 401;
|
|
329
|
+
ctx.body = {
|
|
330
|
+
error: {
|
|
331
|
+
status: 401,
|
|
332
|
+
message: 'Session terminated. Please login again.',
|
|
333
|
+
name: 'UnauthorizedError'
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
return; // Don't continue
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
strapi.log.info(`[magic-sessionmanager] ✅ Refresh token allowed for session ${matchingSession.id}`);
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
strapi.log.error('[magic-sessionmanager] Error checking refresh token:', err);
|
|
343
|
+
// On error, allow request to continue (fail-open for availability)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Continue with request
|
|
348
|
+
await next();
|
|
349
|
+
|
|
350
|
+
// AFTER: If refresh token response was successful, update session with new tokens
|
|
351
|
+
if (isRefreshToken && ctx.status === 200 && ctx.body && ctx.body.jwt) {
|
|
352
|
+
try {
|
|
353
|
+
const oldRefreshToken = ctx.request.body?.refreshToken;
|
|
354
|
+
const newAccessToken = ctx.body.jwt;
|
|
355
|
+
const newRefreshToken = ctx.body.refreshToken;
|
|
356
|
+
|
|
357
|
+
if (oldRefreshToken) {
|
|
358
|
+
// Find session and update with new tokens
|
|
359
|
+
const allSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
360
|
+
filters: {
|
|
361
|
+
isActive: true,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const matchingSession = allSessions.find(session => {
|
|
366
|
+
if (!session.refreshToken) return false;
|
|
367
|
+
try {
|
|
368
|
+
const decrypted = decryptToken(session.refreshToken);
|
|
369
|
+
return decrypted === oldRefreshToken;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (matchingSession) {
|
|
376
|
+
const encryptedToken = newAccessToken ? encryptToken(newAccessToken) : matchingSession.token;
|
|
377
|
+
const encryptedRefreshToken = newRefreshToken ? encryptToken(newRefreshToken) : matchingSession.refreshToken;
|
|
378
|
+
|
|
379
|
+
await strapi.entityService.update('plugin::magic-sessionmanager.session', matchingSession.id, {
|
|
380
|
+
data: {
|
|
381
|
+
token: encryptedToken,
|
|
382
|
+
refreshToken: encryptedRefreshToken,
|
|
383
|
+
lastActive: new Date(),
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
strapi.log.info(`[magic-sessionmanager] 🔄 Tokens refreshed for session ${matchingSession.id}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
strapi.log.error('[magic-sessionmanager] Error updating refreshed tokens:', err);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
strapi.log.info('[magic-sessionmanager] ✅ Refresh Token interceptor middleware mounted');
|
|
397
|
+
|
|
296
398
|
// Mount lastSeen update middleware
|
|
297
399
|
strapi.server.use(
|
|
298
400
|
require('./middlewares/last-seen')({ strapi, sessionService })
|
|
@@ -17,18 +17,19 @@ const { encryptToken, decryptToken, generateSessionId } = require('../utils/encr
|
|
|
17
17
|
module.exports = ({ strapi }) => ({
|
|
18
18
|
/**
|
|
19
19
|
* Create a new session record
|
|
20
|
-
* @param {Object} params - { userId, ip, userAgent, token }
|
|
20
|
+
* @param {Object} params - { userId, ip, userAgent, token, refreshToken }
|
|
21
21
|
* @returns {Promise<Object>} Created session
|
|
22
22
|
*/
|
|
23
|
-
async createSession({ userId, ip = 'unknown', userAgent = 'unknown', token }) {
|
|
23
|
+
async createSession({ userId, ip = 'unknown', userAgent = 'unknown', token, refreshToken }) {
|
|
24
24
|
try {
|
|
25
25
|
const now = new Date();
|
|
26
26
|
|
|
27
27
|
// Generate unique session ID
|
|
28
28
|
const sessionId = generateSessionId(userId);
|
|
29
29
|
|
|
30
|
-
// Encrypt JWT
|
|
30
|
+
// Encrypt JWT tokens before storing (both access and refresh)
|
|
31
31
|
const encryptedToken = token ? encryptToken(token) : null;
|
|
32
|
+
const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
|
|
32
33
|
|
|
33
34
|
const session = await strapi.entityService.create('plugin::magic-sessionmanager.session', {
|
|
34
35
|
data: {
|
|
@@ -38,8 +39,9 @@ module.exports = ({ strapi }) => ({
|
|
|
38
39
|
loginTime: now,
|
|
39
40
|
lastActive: now,
|
|
40
41
|
isActive: true,
|
|
41
|
-
token: encryptedToken,
|
|
42
|
-
|
|
42
|
+
token: encryptedToken, // ✅ Encrypted Access Token
|
|
43
|
+
refreshToken: encryptedRefreshToken, // ✅ Encrypted Refresh Token
|
|
44
|
+
sessionId: sessionId, // ✅ Unique identifier
|
|
43
45
|
},
|
|
44
46
|
});
|
|
45
47
|
|