Skip to content

Commit 88631ed

Browse files
committed
feat: enhanced session token security with hashing, rate limiting, and auto-refresh
1 parent d8b30e2 commit 88631ed

File tree

10 files changed

+946
-90
lines changed

10 files changed

+946
-90
lines changed

README.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,130 @@ Configure permissions in the Strapi admin panel:
770770

771771
---
772772

773+
## Security
774+
775+
The plugin implements multiple security layers to protect your real-time connections.
776+
777+
### Admin Session Tokens
778+
779+
For admin panel connections (Live Presence), the plugin uses secure session tokens:
780+
781+
```
782+
+------------------+ +------------------+ +------------------+
783+
| Admin Browser | ---> | Session Endpoint| ---> | Socket.IO |
784+
| (Strapi Admin) | | /io/presence/ | | Server |
785+
+------------------+ +------------------+ +------------------+
786+
| | |
787+
| 1. Request session | |
788+
| (Admin JWT in header) | |
789+
+------------------------>| |
790+
| | |
791+
| 2. Return session token | |
792+
| (UUID, 10 min TTL) | |
793+
|<------------------------+ |
794+
| | |
795+
| 3. Connect Socket.IO | |
796+
| (Session token in auth) | |
797+
+-------------------------------------------------->|
798+
| | |
799+
| 4. Validate & connect | |
800+
|<--------------------------------------------------+
801+
```
802+
803+
**Security Features:**
804+
- **Token Hashing**: Tokens stored as SHA-256 hashes (plaintext never persisted)
805+
- **Short TTL**: 10-minute expiration with automatic refresh at 70%
806+
- **Usage Limits**: Max 10 reconnects per token to prevent replay attacks
807+
- **Rate Limiting**: 30-second cooldown between token requests
808+
- **Minimal Data**: Only essential user info stored (ID, firstname, lastname)
809+
810+
### Rate Limiting
811+
812+
Prevent abuse with configurable rate limits:
813+
814+
```javascript
815+
// In config/plugins.js
816+
module.exports = {
817+
io: {
818+
enabled: true,
819+
config: {
820+
security: {
821+
rateLimit: {
822+
enabled: true,
823+
maxEventsPerSecond: 10, // Max events per socket per second
824+
maxConnectionsPerIp: 50 // Max connections from single IP
825+
}
826+
}
827+
}
828+
}
829+
};
830+
```
831+
832+
### IP Whitelisting/Blacklisting
833+
834+
Restrict access by IP address:
835+
836+
```javascript
837+
// In config/plugins.js
838+
module.exports = {
839+
io: {
840+
enabled: true,
841+
config: {
842+
security: {
843+
ipWhitelist: ['192.168.1.0/24', '10.0.0.1'], // Only these IPs allowed
844+
ipBlacklist: ['203.0.113.50'], // These IPs blocked
845+
requireAuthentication: true // Require JWT/API token
846+
}
847+
}
848+
}
849+
};
850+
```
851+
852+
### Security Monitoring API
853+
854+
Monitor active sessions via admin API:
855+
856+
```bash
857+
# Get session statistics
858+
GET /io/security/sessions
859+
Authorization: Bearer <admin-jwt>
860+
861+
# Response:
862+
{
863+
"data": {
864+
"activeSessions": 5,
865+
"expiringSoon": 1,
866+
"activeSocketConnections": 3,
867+
"sessionTTL": 600000,
868+
"refreshCooldown": 30000
869+
}
870+
}
871+
872+
# Force logout a user (invalidate all their sessions)
873+
POST /io/security/invalidate/:userId
874+
Authorization: Bearer <admin-jwt>
875+
876+
# Response:
877+
{
878+
"data": {
879+
"userId": 1,
880+
"invalidatedSessions": 2,
881+
"message": "Successfully invalidated 2 session(s)"
882+
}
883+
}
884+
```
885+
886+
### Best Practices
887+
888+
1. **Always use HTTPS** in production for encrypted WebSocket connections
889+
2. **Enable authentication** for sensitive content types
890+
3. **Configure CORS** to only allow your frontend domains
891+
4. **Monitor connections** via the admin dashboard
892+
5. **Set reasonable rate limits** based on your use case
893+
6. **Review access logs** periodically for suspicious activity
894+
895+
---
896+
773897
## Admin Panel
774898

775899
The plugin provides a full admin interface for configuration and monitoring.
@@ -1327,6 +1451,9 @@ Copyright (c) 2024 Strapi Community
13271451
- **Admin Panel Sidebar** - Live presence panel integrated into edit view
13281452
- **Admin Session Authentication** - Secure session tokens for Socket.IO
13291453
- **Admin JWT Strategy** - New authentication strategy for admin users
1454+
- **Enhanced Security** - Token hashing (SHA-256), usage limits, rate limiting
1455+
- **Automatic Token Refresh** - Tokens auto-refresh at 70% of TTL
1456+
- **Security Monitoring API** - Session stats and force-logout endpoints
13301457

13311458
### v5.0.0
13321459
- Strapi v5 support

admin/src/components/LivePresencePanel.jsx

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,18 +234,25 @@ const LivePresencePanel = ({ documentId, model, document }) => {
234234
// Get the content type UID from model
235235
const uid = model?.uid || model;
236236

237-
// Step 1: Get session token from server
237+
// Step 1: Get session token from server with automatic refresh
238238
useEffect(() => {
239239
if (!uid || !documentId) {
240240
setPresenceState(prev => ({ ...prev, status: 'disconnected', error: 'No content' }));
241241
return;
242242
}
243243

244244
let cancelled = false;
245+
let refreshTimeoutId = null;
245246

246-
const getSession = async () => {
247+
/**
248+
* Fetches a new session token from the server
249+
* @param {boolean} isRefresh - Whether this is a refresh request
250+
*/
251+
const getSession = async (isRefresh = false) => {
247252
try {
248-
setPresenceState(prev => ({ ...prev, status: 'requesting' }));
253+
if (!isRefresh) {
254+
setPresenceState(prev => ({ ...prev, status: 'requesting' }));
255+
}
249256

250257
// Use useFetchClient to get session token (automatically includes admin auth)
251258
const { data } = await post(`/${PLUGIN_ID}/presence/session`, {});
@@ -256,11 +263,42 @@ const LivePresencePanel = ({ documentId, model, document }) => {
256263
throw new Error('Invalid session response');
257264
}
258265

259-
console.log(`[${PLUGIN_ID}] Presence session obtained for:`, data.user?.email);
266+
console.log(`[${PLUGIN_ID}] Session ${isRefresh ? 'refreshed' : 'obtained'}:`, {
267+
expiresIn: Math.round((data.expiresAt - Date.now()) / 1000) + 's',
268+
refreshAfter: Math.round((data.refreshAfter - Date.now()) / 1000) + 's',
269+
});
270+
260271
setSessionData(data);
261-
setPresenceState(prev => ({ ...prev, status: 'connecting' }));
272+
273+
if (!isRefresh) {
274+
setPresenceState(prev => ({ ...prev, status: 'connecting' }));
275+
}
276+
277+
// Schedule token refresh at 70% of TTL (when server suggests)
278+
if (data.refreshAfter) {
279+
const refreshIn = data.refreshAfter - Date.now();
280+
if (refreshIn > 0) {
281+
console.log(`[${PLUGIN_ID}] Token refresh scheduled in ${Math.round(refreshIn / 1000)}s`);
282+
refreshTimeoutId = setTimeout(() => {
283+
if (!cancelled) {
284+
console.log(`[${PLUGIN_ID}] Refreshing session token...`);
285+
getSession(true);
286+
}
287+
}, refreshIn);
288+
}
289+
}
262290
} catch (error) {
263291
if (cancelled) return;
292+
293+
// Handle rate limiting gracefully
294+
if (error.response?.status === 429) {
295+
console.warn(`[${PLUGIN_ID}] Rate limited, retrying in 30s...`);
296+
refreshTimeoutId = setTimeout(() => {
297+
if (!cancelled) getSession(isRefresh);
298+
}, 30000);
299+
return;
300+
}
301+
264302
console.error(`[${PLUGIN_ID}] Failed to get presence session:`, error);
265303
setPresenceState(prev => ({
266304
...prev,
@@ -274,6 +312,9 @@ const LivePresencePanel = ({ documentId, model, document }) => {
274312

275313
return () => {
276314
cancelled = true;
315+
if (refreshTimeoutId) {
316+
clearTimeout(refreshTimeoutId);
317+
}
277318
};
278319
}, [uid, documentId, post]);
279320

dist/admin/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"use strict";
2-
const index = require("../_chunks/index--2NeIKGR.js");
2+
const index = require("../_chunks/index-BEZDDgvZ.js");
33
module.exports = index.index;

dist/admin/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { i } from "../_chunks/index-CzvX8YTe.mjs";
1+
import { i } from "../_chunks/index-Dof_eA3e.mjs";
22
export {
33
i as default
44
};

0 commit comments

Comments
 (0)