strapi-plugin-magic-sessionmanager 3.0.2 → 3.2.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 +121 -1
- package/admin/src/pages/Settings.jsx +146 -0
- package/dist/_chunks/{Analytics-Bp1cPJx1.mjs → Analytics-CwyLwdOZ.mjs} +2 -2
- package/dist/_chunks/{Analytics-CjqdGXnQ.js → Analytics-DRzCKaDF.js} +2 -2
- package/dist/_chunks/{App-DONYhluL.mjs → App-Zhs_vt59.mjs} +2 -2
- package/dist/_chunks/{App-DtfZMVae.js → App-nGu2Eb87.js} +2 -2
- package/dist/_chunks/{License-lcVK7rtT.mjs → License-CPI0p_W8.mjs} +1 -1
- package/dist/_chunks/{License-IWH6ClOx.js → License-k5vvhgKr.js} +1 -1
- package/dist/_chunks/{Settings-JaiqQ_7r.mjs → Settings-CL2im8M3.mjs} +154 -6
- package/dist/_chunks/{Settings-JixgQiB_.js → Settings-Lkmxisuv.js} +152 -4
- package/dist/_chunks/{index--JzOiQNw.mjs → index-B-0VPfeF.mjs} +4 -4
- package/dist/_chunks/{index-DqtQaEBL.js → index-W_QbTAYU.js} +4 -4
- package/dist/_chunks/{useLicense-DA-averf.js → useLicense-C_Rneohy.js} +1 -1
- package/dist/_chunks/{useLicense-W1cxUaca.mjs → useLicense-DUGjNbQ9.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +103 -14
- package/dist/server/index.mjs +103 -14
- package/package.json +1 -1
- package/server/src/bootstrap.js +18 -7
- package/server/src/content-types/session/schema.json +5 -0
- package/server/src/controllers/session.js +17 -5
- package/server/src/services/session.js +13 -2
- package/server/src/utils/encryption.js +121 -0
package/server/src/bootstrap.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const getClientIp = require('./utils/getClientIp');
|
|
11
|
+
const { encryptToken, decryptToken } = require('./utils/encryption');
|
|
11
12
|
|
|
12
13
|
module.exports = async ({ strapi }) => {
|
|
13
14
|
strapi.log.info('[magic-sessionmanager] 🚀 Bootstrap starting...');
|
|
@@ -101,18 +102,28 @@ module.exports = async ({ strapi }) => {
|
|
|
101
102
|
return;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
// Find
|
|
105
|
-
|
|
105
|
+
// Find session by decrypting tokens and matching
|
|
106
|
+
// Since tokens are encrypted, we need to get all active sessions and check each one
|
|
107
|
+
const allSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
106
108
|
filters: {
|
|
107
|
-
token: token,
|
|
108
109
|
isActive: true,
|
|
109
110
|
},
|
|
110
|
-
limit: 1,
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
// Find matching session by decrypting and comparing tokens
|
|
114
|
+
const matchingSession = allSessions.find(session => {
|
|
115
|
+
if (!session.token) return false;
|
|
116
|
+
try {
|
|
117
|
+
const decrypted = decryptToken(session.token);
|
|
118
|
+
return decrypted === token;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (matchingSession) {
|
|
125
|
+
await sessionService.terminateSession({ sessionId: matchingSession.id });
|
|
126
|
+
strapi.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${matchingSession.id} terminated`);
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
ctx.status = 200;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { decryptToken } = require('../utils/encryption');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Session Controller
|
|
5
7
|
* Handles HTTP requests for session management
|
|
@@ -103,19 +105,29 @@ module.exports = {
|
|
|
103
105
|
.plugin('magic-sessionmanager')
|
|
104
106
|
.service('session');
|
|
105
107
|
|
|
106
|
-
// Find current session by
|
|
108
|
+
// Find current session by decrypting and comparing tokens
|
|
107
109
|
const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
108
110
|
filters: {
|
|
109
111
|
user: { id: userId },
|
|
110
|
-
token: token,
|
|
111
112
|
isActive: true,
|
|
112
113
|
},
|
|
113
114
|
});
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
// Find matching session by decrypting tokens
|
|
117
|
+
const matchingSession = sessions.find(session => {
|
|
118
|
+
if (!session.token) return false;
|
|
119
|
+
try {
|
|
120
|
+
const decrypted = decryptToken(session.token);
|
|
121
|
+
return decrypted === token;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (matchingSession) {
|
|
116
128
|
// Terminate only the current session
|
|
117
|
-
await sessionService.terminateSession({ sessionId:
|
|
118
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${
|
|
129
|
+
await sessionService.terminateSession({ sessionId: matchingSession.id });
|
|
130
|
+
strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.id})`);
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
ctx.body = {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { encryptToken, decryptToken, generateSessionId } = require('../utils/encryption');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Session Service
|
|
5
7
|
* Uses plugin::magic-sessionmanager.session content type with relation to users
|
|
6
8
|
* All session tracking happens in the Session collection
|
|
7
9
|
*
|
|
10
|
+
* SECURITY: JWT tokens are encrypted before storing in database using AES-256-GCM
|
|
11
|
+
*
|
|
8
12
|
* TODO: For production multi-instance deployments, use Redis for:
|
|
9
13
|
* - Session store instead of DB
|
|
10
14
|
* - Rate limiting locks
|
|
@@ -20,6 +24,12 @@ module.exports = ({ strapi }) => ({
|
|
|
20
24
|
try {
|
|
21
25
|
const now = new Date();
|
|
22
26
|
|
|
27
|
+
// Generate unique session ID
|
|
28
|
+
const sessionId = generateSessionId(userId);
|
|
29
|
+
|
|
30
|
+
// Encrypt JWT token before storing
|
|
31
|
+
const encryptedToken = token ? encryptToken(token) : null;
|
|
32
|
+
|
|
23
33
|
const session = await strapi.entityService.create('plugin::magic-sessionmanager.session', {
|
|
24
34
|
data: {
|
|
25
35
|
user: userId,
|
|
@@ -28,11 +38,12 @@ module.exports = ({ strapi }) => ({
|
|
|
28
38
|
loginTime: now,
|
|
29
39
|
lastActive: now,
|
|
30
40
|
isActive: true,
|
|
31
|
-
token:
|
|
41
|
+
token: encryptedToken, // ✅ Encrypted JWT for security
|
|
42
|
+
sessionId: sessionId, // ✅ Unique identifier
|
|
32
43
|
},
|
|
33
44
|
});
|
|
34
45
|
|
|
35
|
-
strapi.log.info(`[magic-sessionmanager] ✅ Session ${session.id} created for user ${userId}`);
|
|
46
|
+
strapi.log.info(`[magic-sessionmanager] ✅ Session ${session.id} (${sessionId}) created for user ${userId}`);
|
|
36
47
|
|
|
37
48
|
return session;
|
|
38
49
|
} catch (err) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* JWT Encryption Utility
|
|
7
|
+
* Uses AES-256-GCM for secure token storage
|
|
8
|
+
*
|
|
9
|
+
* SECURITY: Tokens are encrypted before storing in database
|
|
10
|
+
* This prevents exposure if database is compromised
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
14
|
+
const IV_LENGTH = 16;
|
|
15
|
+
const AUTH_TAG_LENGTH = 16;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get encryption key from environment or generate one
|
|
19
|
+
* IMPORTANT: Set SESSION_ENCRYPTION_KEY in .env for production!
|
|
20
|
+
*/
|
|
21
|
+
function getEncryptionKey() {
|
|
22
|
+
const envKey = process.env.SESSION_ENCRYPTION_KEY;
|
|
23
|
+
|
|
24
|
+
if (envKey) {
|
|
25
|
+
// Use provided key (must be 32 bytes for AES-256)
|
|
26
|
+
const key = crypto.createHash('sha256').update(envKey).digest();
|
|
27
|
+
return key;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback: Use Strapi's app keys (not recommended for production)
|
|
31
|
+
const strapiKeys = process.env.APP_KEYS || process.env.API_TOKEN_SALT || 'default-insecure-key';
|
|
32
|
+
const key = crypto.createHash('sha256').update(strapiKeys).digest();
|
|
33
|
+
|
|
34
|
+
console.warn('[magic-sessionmanager/encryption] ⚠️ No SESSION_ENCRYPTION_KEY found. Using fallback (not recommended for production).');
|
|
35
|
+
console.warn('[magic-sessionmanager/encryption] Set SESSION_ENCRYPTION_KEY in .env for better security.');
|
|
36
|
+
|
|
37
|
+
return key;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Encrypt JWT token before storing in database
|
|
42
|
+
* @param {string} token - JWT token to encrypt
|
|
43
|
+
* @returns {string} Encrypted token with IV and auth tag
|
|
44
|
+
*/
|
|
45
|
+
function encryptToken(token) {
|
|
46
|
+
if (!token) return null;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const key = getEncryptionKey();
|
|
50
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
51
|
+
|
|
52
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
53
|
+
|
|
54
|
+
let encrypted = cipher.update(token, 'utf8', 'hex');
|
|
55
|
+
encrypted += cipher.final('hex');
|
|
56
|
+
|
|
57
|
+
const authTag = cipher.getAuthTag();
|
|
58
|
+
|
|
59
|
+
// Format: iv:authTag:encryptedData
|
|
60
|
+
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('[magic-sessionmanager/encryption] Encryption failed:', err);
|
|
63
|
+
throw new Error('Failed to encrypt token');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decrypt JWT token from database
|
|
69
|
+
* @param {string} encryptedToken - Encrypted token from database
|
|
70
|
+
* @returns {string} Decrypted JWT token
|
|
71
|
+
*/
|
|
72
|
+
function decryptToken(encryptedToken) {
|
|
73
|
+
if (!encryptedToken) return null;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const key = getEncryptionKey();
|
|
77
|
+
|
|
78
|
+
// Parse: iv:authTag:encryptedData
|
|
79
|
+
const parts = encryptedToken.split(':');
|
|
80
|
+
|
|
81
|
+
if (parts.length !== 3) {
|
|
82
|
+
throw new Error('Invalid encrypted token format');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
86
|
+
const authTag = Buffer.from(parts[1], 'hex');
|
|
87
|
+
const encrypted = parts[2];
|
|
88
|
+
|
|
89
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
90
|
+
decipher.setAuthTag(authTag);
|
|
91
|
+
|
|
92
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
93
|
+
decrypted += decipher.final('utf8');
|
|
94
|
+
|
|
95
|
+
return decrypted;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('[magic-sessionmanager/encryption] Decryption failed:', err);
|
|
98
|
+
return null; // Return null if decryption fails (invalid/tampered token)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate unique session ID
|
|
104
|
+
* Combines timestamp + random bytes + user ID for uniqueness
|
|
105
|
+
* @param {number} userId - User ID
|
|
106
|
+
* @returns {string} Unique session identifier
|
|
107
|
+
*/
|
|
108
|
+
function generateSessionId(userId) {
|
|
109
|
+
const timestamp = Date.now().toString(36);
|
|
110
|
+
const randomBytes = crypto.randomBytes(8).toString('hex');
|
|
111
|
+
const userHash = crypto.createHash('sha256').update(userId.toString()).digest('hex').substring(0, 8);
|
|
112
|
+
|
|
113
|
+
return `sess_${timestamp}_${userHash}_${randomBytes}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
encryptToken,
|
|
118
|
+
decryptToken,
|
|
119
|
+
generateSessionId,
|
|
120
|
+
};
|
|
121
|
+
|