Docs

Secure API Calls

Updated

On this page

Your Xirsys identifier and secret authorize privileged API operations. Keep them in a trusted backend and expose only narrow application endpoints that return temporary TURN or signaling values.

ClientBrowser or appNo Xirsys secret
Authenticated HTTPS request
TrustedYour backendHolds XIRSYS_SECRET
HTTPS + Xirsys Basic auth
XirsysXirsys APIglobal.xirsys.net

Do not build a generic pass-through proxy that accepts an upstream path from the client. Define fixed routes, authenticate your own user, authorize the requested channel, validate inputs, rate-limit issuance, and redact credentials from logs.

Store Secrets on the Server

Use a deployment secret manager or protected environment variables:

.env (server only)Shell
XIRSYS_IDENT=your-account-ident
XIRSYS_SECRET=your-api-secret
XIRSYS_CHANNEL=your-channel

Do not commit an .env file, print the Basic authorization header, include credentials in a URL, or return the Xirsys secret to a client. Rotate the secret immediately if it is exposed.

Node.js 18+ Backend Pattern

The following excerpts use Express and the built-in Node.js fetch. The application-specific authentication middleware must populate request.user. If geo routing is enabled, trusted proxy middleware must separately populate request.authenticatedClientIp from a verified network boundary.

server.js — setupJS
const express = require('express');
const net = require('node:net');

const app = express();
const baseUrl = 'https://global.xirsys.net';
const { XIRSYS_IDENT, XIRSYS_SECRET, XIRSYS_CHANNEL } = process.env;

if (!XIRSYS_IDENT || !XIRSYS_SECRET || !XIRSYS_CHANNEL) {
  throw new Error('Missing Xirsys server configuration');
}

const authorization = `Basic ${Buffer.from(
  `${XIRSYS_IDENT}:${XIRSYS_SECRET}`
).toString('base64')}`;
const encodedChannel = encodeURIComponent(XIRSYS_CHANNEL);

async function xirsysValue(response, operation) {
  const data = await response.json();
  if (!response.ok || data.s !== 'ok') {
    throw new Error(`${operation} failed (${response.status})`);
  }
  return data.v;
}

function trustedPublicIp(request) {
  const candidate = request.authenticatedClientIp;
  return typeof candidate === 'string' && net.isIP(candidate)
    ? candidate
    : null;
}

function requireAppUser(request, response, next) {
  // Replace with your session/JWT authentication and authorization.
  if (!request.user) return response.sendStatus(401);
  next();
}

app.use(requireAppUser);

Do not trust X-Forwarded-For by default. Configure your framework with the exact proxies you operate, discard untrusted forwarded headers at the network edge, and normalize IPv4/IPv6 before treating an address as authenticated context.

Fixed TURN Route

server.js — TURN routeJS
app.get('/api/ice', async (request, response) => {
  try {
    const userIp = trustedPublicIp(request);
    const query = new URLSearchParams({ expire: '60', webrtc: '1' });
    const body = {};

    if (userIp) {
      query.set('geo', '1');
      body.user_ip = userIp;
    }

    const upstream = await fetch(
      `${baseUrl}/_turn/${encodedChannel}?${query}`,
      {
        method: 'PUT',
        headers: {
          Authorization: authorization,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(body)
      }
    );
    const value = await xirsysValue(upstream, 'TURN request');

    response.set('Cache-Control', 'no-store');
    response.json({ iceServers: value.iceServers });
  } catch (error) {
    console.error(error.message);
    response.status(502).json({ error: 'Unable to obtain ICE servers' });
  }
});

The webrtc=1 flag returns v.iceServers in the standard WebRTC array schema, so it can be passed directly to RTCPeerConnection without conversion.

When no trusted public IP is available, this route makes a normal TURN request. It does not accept user_ip from a query parameter or request body.

End-user IP privacy: The user_ip value is strictly transient. Xirsys uses it only in memory to select a TURN region for the current request, then discards it. It is not persisted, stored, cached, or logged anywhere within the Xirsys service, and it is not included in forwarded request bodies.

Fixed Signaling Route

Assign the peer ID from authenticated application state instead of accepting an arbitrary identity from the client:

server.js — signaling routeJS
app.get('/api/signal-config', async (request, response) => {
  const peerId = String(request.user.id);
  if (!/^[A-Za-z0-9-]{1,64}$/.test(peerId)) {
    return response.status(400).json({ error: 'Invalid peer identity' });
  }

  try {
    const headers = { Authorization: authorization };
    const [hostResponse, tokenResponse] = await Promise.all([
      fetch(`${baseUrl}/_host?type=signal`, { headers }),
      fetch(
        `${baseUrl}/_token/${encodedChannel}` +
          `?k=${encodeURIComponent(peerId)}&expire=120`,
        { method: 'PUT', headers }
      )
    ]);

    const [host, token] = await Promise.all([
      xirsysValue(hostResponse, 'Host request'),
      xirsysValue(tokenResponse, 'Token request')
    ]);

    response.set('Cache-Control', 'no-store');
    response.json({ host, token });
  } catch (error) {
    console.error(error.message);
    response.status(502).json({ error: 'Unable to configure signaling' });
  }
});

The browser connects to ${host}/v2/${encodeURIComponent(token)}. On reconnect, it should call this backend route again rather than reuse stale configuration.

Production Checklist

  • Serve the application and API over HTTPS with a certificate trusted by clients.
  • Authenticate users before issuing ICE credentials or signaling tokens.
  • Authorize channel membership and assign peer IDs on the server.
  • Apply per-user and per-IP rate limits and monitor issuance failures.
  • Set Cache-Control: no-store on responses containing temporary credentials.
  • Use short expiration periods and retrieve fresh values for new connections.
  • Redact Basic auth, TURN credentials, signaling tokens, and SDP from routine logs.
  • Configure trusted proxy handling explicitly before using a forwarded client IP.
  • Keep development, staging, and production channels and secrets separate.

Continue with the Quick Start for a runnable local example, then review the detailed TURN and Signaling references.

Questions about this page? Email experts@xirsys.com