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.
@@ -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 and terminate session by token
105
- const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
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
- if (sessions.length > 0) {
114
- await sessionService.terminateSession({ sessionId: sessions[0].id });
115
- strapi.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${sessions[0].id} terminated`);
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;
@@ -20,6 +20,11 @@
20
20
  }
21
21
  },
22
22
  "attributes": {
23
+ "sessionId": {
24
+ "type": "string",
25
+ "unique": true,
26
+ "required": true
27
+ },
23
28
  "user": {
24
29
  "type": "relation",
25
30
  "relation": "manyToOne",
@@ -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 token
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
- if (sessions.length > 0) {
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: sessions[0].id });
118
- strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${sessions[0].id})`);
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: token, // Store JWT for logout matching
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
+