Skip to content

Security best practices

Security is crucial when building real-time applications. This guide covers best practices for securing your Jetsocket.io applications.

Never allow unauthorized access to private or presence channels:

// Good: Always verify user identity
app.post("/jetsocket/auth", authenticateUser, (req, res) => {
const user = req.user;
const channel = req.body.channel_name;
if (canAccessChannel(user, channel)) {
const authResponse = jetsocket.authorizeChannel(req.body.socket_id, channel);
res.send(authResponse);
} else {
res.status(403).send("Forbidden");
}
});
// Bad: No authentication
app.post("/jetsocket/auth", (req, res) => {
const authResponse = jetsocket.authorizeChannel(req.body.socket_id, req.body.channel_name);
res.send(authResponse);
});

Implement proper channel access control:

function canAccessChannel(user, channel) {
// Validate channel name format
if (!channel || typeof channel !== 'string') {
return false;
}
// Check for private channel prefix
if (channel.startsWith('private-')) {
// Extract user ID from channel name
if (channel.startsWith('private-user-')) {
const userId = channel.replace('private-user-', '');
return user.id === userId;
}
// Check role-based access
if (channel === 'private-admin-dashboard') {
return user.role === 'admin';
}
// Check group-based access
if (channel.startsWith('private-group-')) {
const groupId = channel.replace('private-group-', '');
return user.groups.includes(groupId);
}
}
return false;
}

Always use HTTPS for your authorization endpoints:

// Good: HTTPS endpoint
const jetsocket = new Jetsocket("APP_KEY", {
cluster: "APP_CLUSTER",
channelAuthorization: {
endpoint: "https://yourdomain.com/jetsocket/auth"
}
});
// Bad: HTTP endpoint (insecure)
const jetsocket = new Jetsocket("APP_KEY", {
cluster: "APP_CLUSTER",
channelAuthorization: {
endpoint: "http://yourdomain.com/jetsocket/auth"
}
});

Use descriptive and secure channel names:

// Good channel names
"private-user-123"
"private-group-456"
"presence-chat-room"
"private-encrypted-financial-data"
// Bad channel names
"private-secret"
"private-12345"
"private-admin" // Too generic

Always validate channel names on the server:

function validateChannelName(channel) {
// Check for required prefixes
if (channel.startsWith('private-') || channel.startsWith('presence-')) {
// Validate format
const pattern = /^[a-zA-Z0-9_-]+$/;
return pattern.test(channel.replace(/^(private-|presence-)/, ''));
}
// Public channels
return /^[a-zA-Z0-9_-]+$/.test(channel);
}
app.post("/jetsocket/auth", (req, res) => {
const channel = req.body.channel_name;
if (!validateChannelName(channel)) {
return res.status(400).send("Invalid channel name");
}
// Continue with authorization...
});

Implement rate limiting on your authorization endpoints:

const rateLimit = require("express-rate-limit");
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: "Too many authorization requests"
});
app.post("/jetsocket/auth", authLimiter, authenticateUser, (req, res) => {
// Authorization logic
});

Don’t send sensitive data on public channels

Section titled “Don’t send sensitive data on public channels”
// Good: Use private channels for sensitive data
jetsocket.trigger("private-user-123", "personal-notification", {
message: "Your account has been updated"
});
// Bad: Sending sensitive data on public channels
jetsocket.trigger("notifications", "user-update", {
user_id: "123",
email: "user@example.com",
password_hash: "abc123" // Never do this!
});

Always validate data on the server side:

app.post("/send-message", authenticateUser, (req, res) => {
const { message, channel } = req.body;
// Validate message
if (!message || typeof message !== 'string' || message.length > 1000) {
return res.status(400).json({ error: "Invalid message" });
}
// Validate channel
if (!validateChannelName(channel)) {
return res.status(400).json({ error: "Invalid channel" });
}
// Sanitize message
const sanitizedMessage = sanitizeInput(message);
// Send message
jetsocket.trigger(channel, "new-message", {
message: sanitizedMessage,
user_id: req.user.id
});
res.json({ success: true });
});
function sanitizeInput(input) {
// Remove potentially dangerous content
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.trim();
}
// For financial, medical, or legal data
const encryptedChannel = jetsocket.subscribe("private-encrypted-financial-data");
// Server-side
const jetsocket = new Jetsocket({
appId: "APP_ID",
key: "APP_KEY",
secret: "APP_SECRET",
cluster: "APP_CLUSTER",
encryptionMasterKey: process.env.ENCRYPTION_MASTER_KEY
});

Never expose your API keys in client-side code:

// Good: Keys only on server
const jetsocket = new Jetsocket({
appId: process.env.JETSOCKET_APP_ID,
key: process.env.JETSOCKET_KEY,
secret: process.env.JETSOCKET_SECRET,
cluster: process.env.JETSOCKET_CLUSTER,
});
// Bad: Keys in client code
const jetsocket = new Jetsocket("APP_KEY", {
cluster: "APP_CLUSTER",
secret: "APP_SECRET" // Never expose secret on client!
});

Store sensitive configuration in environment variables:

Terminal window
# .env file
JETSOCKET_APP_ID=your_app_id
JETSOCKET_KEY=your_app_key
JETSOCKET_SECRET=your_app_secret
JETSOCKET_CLUSTER=your_cluster
ENCRYPTION_MASTER_KEY=your_encryption_key
JWT_SECRET=your_jwt_secret

Always verify webhook signatures:

app.post("/jetsocket/webhook", (req, res) => {
const webhook = jetsocket.webhook(req, {
webhookSecret: process.env.WEBHOOK_SECRET
});
if (webhook.isValid()) {
// Process webhook events
webhook.getEvents().forEach(event => {
console.log("Webhook event:", event.name);
});
res.status(200).send("OK");
} else {
res.status(400).send("Invalid webhook");
}
});
// Good: Generic error messages
app.post("/jetsocket/auth", (req, res) => {
try {
// Authorization logic
} catch (error) {
console.error("Auth error:", error);
res.status(500).send("Internal server error");
}
});
// Bad: Exposing internal details
app.post("/jetsocket/auth", (req, res) => {
try {
// Authorization logic
} catch (error) {
res.status(500).json({
error: "Database connection failed",
details: error.message,
stack: error.stack
});
}
});
app.post("/jetsocket/auth", (req, res) => {
const user = req.user;
const channel = req.body.channel_name;
const socketId = req.body.socket_id;
console.log(`Auth attempt: User ${user.id} requesting access to ${channel}`);
if (canAccessChannel(user, channel)) {
console.log(`Auth success: User ${user.id} granted access to ${channel}`);
const authResponse = jetsocket.authorizeChannel(socketId, channel);
res.send(authResponse);
} else {
console.log(`Auth failed: User ${user.id} denied access to ${channel}`);
res.status(403).send("Forbidden");
}
});
jetsocket.connection.bind("error", (error) => {
console.error("Connection error:", error);
// Don't show sensitive error details to users
showUserMessage("Connection failed. Please try again.");
});
const channel = jetsocket.subscribe("private-my-channel");
channel.bind("jetsocket:subscription_error", (error) => {
console.error("Subscription error:", error);
if (error.status === 403) {
showUserMessage("Access denied");
} else if (error.status === 401) {
redirectToLogin();
} else {
showUserMessage("Connection failed");
}
});
// Only allow client events on private/presence channels
const channel = jetsocket.subscribe("private-my-channel");
channel.bind("client-message", (data, metadata) => {
// Validate the event data
if (!data.message || typeof data.message !== 'string') {
console.error("Invalid client event data");
return;
}
// Process the message
handleMessage(data, metadata.user_id);
});
// Track authorization attempts
const authAttempts = new Map();
app.post("/jetsocket/auth", (req, res) => {
const ip = req.ip;
const now = Date.now();
// Track attempts per IP
if (!authAttempts.has(ip)) {
authAttempts.set(ip, []);
}
const attempts = authAttempts.get(ip);
attempts.push(now);
// Remove old attempts (older than 1 hour)
const oneHourAgo = now - 60 * 60 * 1000;
const recentAttempts = attempts.filter(time => time > oneHourAgo);
authAttempts.set(ip, recentAttempts);
// Check for suspicious activity
if (recentAttempts.length > 100) {
console.warn(`Suspicious activity from IP: ${ip}`);
return res.status(429).send("Too many requests");
}
// Continue with authorization...
});
function logSecurityEvent(event, details) {
const logEntry = {
timestamp: new Date().toISOString(),
event: event,
details: details,
ip: req.ip,
userAgent: req.get('User-Agent')
};
console.log("Security event:", JSON.stringify(logEntry));
// Send to security monitoring service
sendToSecurityMonitoring(logEntry);
}
// Handle user data deletion
app.delete("/user/:userId", authenticateUser, async (req, res) => {
const userId = req.params.userId;
// Verify user can delete this data
if (req.user.id !== userId && req.user.role !== 'admin') {
return res.status(403).send("Forbidden");
}
// Delete user data
await deleteUserData(userId);
// Terminate user connections
await jetsocket.terminateUserConnections(userId);
res.json({ success: true });
});
// Use encrypted channels for medical data
const medicalChannel = jetsocket.subscribe("private-encrypted-medical-records");
// Log access to medical data
function logMedicalDataAccess(userId, recordId) {
console.log(`Medical data access: User ${userId} accessed record ${recordId}`);
// Send to audit log
}
describe("Channel Authorization", () => {
test("should deny access to unauthorized users", async () => {
const response = await request(app)
.post("/jetsocket/auth")
.send({
socket_id: "123.456",
channel_name: "private-admin-dashboard"
})
.set("Authorization", "Bearer user-token");
expect(response.status).toBe(403);
});
test("should allow access to authorized users", async () => {
const response = await request(app)
.post("/jetsocket/auth")
.send({
socket_id: "123.456",
channel_name: "private-user-123"
})
.set("Authorization", "Bearer valid-user-token");
expect(response.status).toBe(200);
});
});
describe("Input Validation", () => {
test("should reject invalid channel names", async () => {
const response = await request(app)
.post("/jetsocket/auth")
.send({
socket_id: "123.456",
channel_name: "invalid-channel-name!"
});
expect(response.status).toBe(400);
});
});