strapi-plugin-magic-sessionmanager 3.2.1 → 3.3.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 +66 -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:
|
|
@@ -367,78 +367,100 @@ Strapi issues new JWT
|
|
|
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-token` 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 ❌
|
|
385
|
+
↓
|
|
386
|
+
User tries to refresh token:
|
|
387
|
+
POST /api/auth/refresh-token
|
|
388
|
+
{ refreshToken: "..." }
|
|
378
389
|
↓
|
|
379
|
-
|
|
390
|
+
[Refresh Token Middleware]
|
|
380
391
|
↓
|
|
381
|
-
|
|
382
|
-
User has refresh token
|
|
392
|
+
Decrypt all active session refresh tokens
|
|
383
393
|
↓
|
|
384
|
-
|
|
394
|
+
Find matching session
|
|
385
395
|
↓
|
|
386
|
-
|
|
396
|
+
Session found but isActive = false?
|
|
397
|
+
→ BLOCK! Return 401 Unauthorized ❌
|
|
398
|
+
→ Message: "Session terminated. Please login again."
|
|
387
399
|
↓
|
|
388
|
-
|
|
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
|
-
This plugin **cannot prevent** users with valid refresh tokens from getting new JWTs. The session termination only affects the current JWT token.
|
|
406
|
+
**Security Benefits:**
|
|
393
407
|
|
|
394
|
-
**
|
|
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:**
|
|
414
|
+
|
|
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
|
-
```typescript
|
|
418
|
-
'users-permissions': {
|
|
419
|
-
config: {
|
|
420
|
-
jwt: {
|
|
421
|
-
expiresIn: '15m', // Short Access Token
|
|
422
|
-
refreshExpiresIn: '30m', // Short Refresh Token
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
}
|
|
426
|
-
```
|
|
439
|
+
**Testing Refresh Token Blocking:**
|
|
427
440
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
-
|
|
431
|
-
-
|
|
432
|
-
-
|
|
433
|
-
- Use session analytics to detect unusual patterns
|
|
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"}'
|
|
434
446
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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-token \
|
|
456
|
+
-H "Content-Type: application/json" \
|
|
457
|
+
-d "{\"refreshToken\":\"$REFRESH_TOKEN\"}"
|
|
458
|
+
|
|
459
|
+
# Expected: 401 Unauthorized
|
|
460
|
+
# "Session terminated. Please login again."
|
|
461
|
+
```
|
|
440
462
|
|
|
441
|
-
This
|
|
463
|
+
**This completely solves the refresh token security gap!** 🔒
|
|
442
464
|
|
|
443
465
|
### Multi-Login Behavior
|
|
444
466
|
|
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-token" && 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-token" && 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
|
|
300
|
+
const isRefreshToken = ctx.path === '/api/auth/refresh-token' && 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
|
|