Skip to content

WebRTC - Security

WebRTC itself is designed with security as a foremost concern. All WebRTC components, including media streams and data channels, are mandatorily encrypted using SRTP (Secure Real-time Transport Protocol) for media and DTLS (Datagram Transport Layer Security) for data channels and key exchange. This chapter focuses on enhancing the security of the signaling server and the application environment, which are crucial adjuncts to a WebRTC deployment.

We’ll explore two main enhancements:

  • User authentication for the signaling server, for example, using a Redis database.
  • Enabling secure WebSocket connections (WSS) for signaling.

Signaling Server User Authentication with Redis

Section titled “Signaling Server User Authentication with Redis”

Authenticating users before they can use the signaling server helps prevent unauthorized access and abuse. While various databases can be used, this example uses Redis, a fast in-memory key-value store.

To set up Redis for development:

  • Install Redis: You can download it from redis.io and compile from source, or use a package manager (e.g., sudo apt install redis-server on Debian/Ubuntu, brew install redis on macOS). For quick local development, Docker is also an excellent option: docker run -d -p 6379:6379 --name my-redis redis.
  • Start Redis Server: If not started automatically by the installer, run redis-server.
  • Interact with Redis: Use redis-cli to connect to your Redis instance. You can set usernames and (hashed) passwords. For this demo, we’ll store plaintext passwords, which is NOT recommended for production.

In redis-cli, let’s add some sample users (replace with your actual user management logic in a real app):

SET user1:password mysecretpassword1
SET user2:password mysecretpassword2

The original tutorial showed an image of redis-cli with SET user1 password123 and GET user1 commands. The above commands achieve a similar outcome but use a slightly better key naming convention.

Now, modify your Node.js signaling server (from previous demos) to include authentication. First, install the Redis client library: npm install redis.

// server.js (partial - Redis integration)
const WebSocket = require('ws');
const redis = require('redis'); // npm install redis
// ... (WebSocket server setup as before) ...
// Create Redis client (v4+ of the library)
const redisClient = redis.createClient(); // Connects to redis://localhost:6379 by default
(async () => {
redisClient.on('error', (err) => console.error('Redis Client Error', err));
try {
await redisClient.connect();
console.log('Connected to Redis successfully!');
} catch (err) {
console.error('Could not connect to Redis:', err);
process.exit(1); // Exit if Redis connection fails, as it's critical for auth
}
})();
// ... (users object) ...
wss.on('connection', (connection) => {
console.log('User connected, awaiting authentication');
connection.isAuthenticated = false; // Custom flag
connection.on('message', async (message) => { // Make handler async for Redis
let data;
try {
data = JSON.parse(message);
} catch (e) { /* ... */ return; }
// All messages except 'login' require authentication
if (data.type !== 'login' && !connection.isAuthenticated) {
sendTo(connection, { type: 'error', message: 'Not authenticated' });
return;
}
switch (data.type) {
case 'login':
// console.log(`Login attempt by: ${data.name}`);
if (!data.name || !data.password) {
sendTo(connection, { type: 'login', success: false, message: 'Username and password required.' });
return;
}
try {
const storedPassword = await redisClient.get(`${data.name}:password`);
if (storedPassword === data.password) {
if (users[data.name]) {
sendTo(connection, { type: 'login', success: false, message: 'User already logged in elsewhere.' });
} else {
users[data.name] = connection;
connection.name = data.name;
connection.isAuthenticated = true;
sendTo(connection, { type: 'login', success: true });
console.log(`User ${data.name} authenticated and logged in.`);
}
} else {
sendTo(connection, { type: 'login', success: false, message: 'Invalid username or password.' });
}
} catch (err) {
console.error('Redis error during login:', err);
sendTo(connection, { type: 'login', success: false, message: 'Server error during login.' });
}
break;
// ... other cases (offer, answer, candidate, leave) remain largely the same ...
// Ensure they check connection.isAuthenticated if needed, though the top check handles it.
default:
// ...
break;
}
});
connection.on('close', () => {
// ... (handleDisconnect logic as before, ensuring isAuthenticated state is considered if needed)
if (connection.name && connection.isAuthenticated) {
console.log(`User ${connection.name} disconnected.`);
delete users[connection.name];
// Notify otherName if in a call
}
});
// ... (error handler) ...
});
function sendTo(conn, message) { /* ... as before ... */ }
// ... (other handlers: handleOffer, handleAnswer, etc.) ...

Key changes for authentication:

  • The redis library is imported, and a client is created and connected (using modern async/await).
  • A connection.isAuthenticated flag is added.
  • The login case now queries Redis for the password associated with the username and compares it.
  • Important: Storing and comparing plaintext passwords is a major security risk. In a production system, you must hash passwords securely (e.g., using bcrypt or Argon2) and store the hashes.

Enabling Secure WebSocket Connections (WSS)

Section titled “Enabling Secure WebSocket Connections (WSS)”

Signaling messages, even if not containing media, can carry sensitive information (like IP addresses in ICE candidates if not using mDNS, or user identifiers). Encrypting the signaling channel using WSS (WebSocket Secure, which is WebSocket over TLS/SSL) is highly recommended. This requires an HTTPS server.

For development, you can use a self-signed SSL certificate. For production, obtain a certificate from a trusted Certificate Authority (CA) (e.g., Let’s Encrypt provides free certificates).

To generate a self-signed certificate using OpenSSL (often pre-installed on Linux/macOS, available for Windows):

  1. Generate a private key: openssl genrsa -out server.key 2048
  2. Generate a Certificate Signing Request (CSR): openssl req -new -key server.key -out server.csr (You’ll be prompted for information; for local dev, defaults are fine, ensure Common Name is localhost if testing locally).
  3. Generate the self-signed certificate: openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

The original tutorial detailed more intermediate steps for key generation (e.g., with passphrase). The above is a more direct approach for a basic self-signed cert. These commands will create server.key (private key) and server.crt (certificate). Place them in your server’s root folder.

Modify the signaling server to use HTTPS and WSS:

// server.js (partial - WSS integration)
const https = require('https');
const fs = require('fs');
const WebSocket = require('ws');
// const redis = require('redis'); // and Redis client setup if using authentication
// SSL Configuration
const privateKey = fs.readFileSync('server.key', 'utf8');
const certificate = fs.readFileSync('server.crt', 'utf8');
const credentials = { key: privateKey, cert: certificate };
// Create HTTPS server
const httpsServer = https.createServer(credentials, (req, res) => {
// Basic HTTP handler (e.g., for health checks or serving client files)
res.writeHead(200);
res.end('HTTPS Server for WebRTC Signaling\n');
});
// Create WebSocket server and attach it to the HTTPS server
const wss = new WebSocket.Server({ server: httpsServer });
const port = 9090;
httpsServer.listen(port, () => {
console.log(`Secure Signaling server started on wss://localhost:${port}`);
});
// ... (rest of the WebSocket and Redis logic: wss.on('connection', ...), etc.) ...
// Note: For development with self-signed certificates, browsers will show a warning.
// You'll need to accept the risk to proceed. Some clients might require specific flags
// to allow self-signed certs (e.g., Node.js `NODE_TLS_REJECT_UNAUTHORIZED=0` for a Node client,
// but this is NOT for browser clients). Browsers usually provide an option to bypass the warning.

Key changes for WSS:

  • The https and fs modules are used.
  • SSL key and certificate files are read.
  • An https.createServer is instantiated with these credentials.
  • The WebSocket.Server is then configured to use this httpsServer instead of directly binding to a port.
  • The server now listens on wss://localhost:9090.

If you’ve secured your signaling server, the client application (e.g., from the Text Demo or Voice Demo) needs a few changes:

  1. Connect to WSS: Change the WebSocket URL from ws:// to wss://:
client.js
// const wsURL = 'ws://localhost:9090'; // Old
const wsURL = 'wss://localhost:9090'; // New for secure connection
const connection = new WebSocket(wsURL);
  1. Send Password on Login: If authentication is enabled, modify the login part to send the password. The index.html would need a password input field:
<!-- index.html (add password field) -->
<div class="form-group">
<input type="password" id="passwordInput" class="form-control" placeholder="Enter your password" required>
</div>
// client.js (updated login logic)
const passwordInput = document.querySelector('#passwordInput'); // Add selector
loginBtn.addEventListener('click', () => {
name = usernameInput.value;
const password = passwordInput.value; // Get password
if (name.length > 0 && password.length > 0) {
send({
type: 'login',
name: name,
password: password // Send password
});
} else {
alert('Please enter both username and password.');
}
});

The original tutorial provided an example of a full client.js with these changes, but it incorrectly wrapped JavaScript code within <html><body><p> tags inside the <pre> block. The correct approach is to show only the JavaScript code, as demonstrated in the snippets above.

When running a client against a server with a self-signed certificate, your browser will display a security warning (e.g., ‘Your connection is not private’). You’ll need to proceed (e.g., ‘Advanced’ -> ‘Proceed to localhost (unsafe)’) to test. This is expected for self-signed certs.

An image in the original tutorial showed this browser warning for an invalid certificate, and then the ‘OK’ message after proceeding, which was the response from a basic HTTPS server. With our full WSS server, the client application should connect and function after bypassing the warning.

  • Application Layer: Authenticate users for signaling to control access.
  • Signaling Transport Layer: Use WSS (HTTPS for WebSockets) to encrypt signaling messages between client and server.
  • WebRTC Media/Data Layer: This is inherently secure. Media is encrypted with SRTP, data channels with DTLS. Keys are exchanged securely using DTLS-SRTP. Browsers enforce this.

By implementing authentication for your signaling server and using secure WebSockets (WSS), you significantly enhance the overall security of your WebRTC application. Always remember to use strong password hashing and obtain valid SSL certificates from a CA for production deployments.