Generating Time-Limited TURN Credentials with HMAC

This page covers the exact mechanics of minting ephemeral TURN credentials with the REST-API scheme: how to compose the timestamped username, sign it with HMAC-SHA1 over the relay’s shared secret, pick a TTL, and verify the result against a running coturn. It is part of the TURN Server Configuration & Auth section within the WebRTC Protocol Stack & Signaling Servers guide. The precise decision here is how to hand a browser a credential that the relay will accept for a bounded window without ever storing per-user state β€” and without letting the signing secret leave your backend.

Context & Trade-offs

Static long-term TURN usernames and passwords are a liability: embedded in client JavaScript, they are scraped within hours and replayed to mine free relay bandwidth. The REST-API model (draft-uberti-behave-turn-rest) removes per-user secrets entirely. coturn holds a single static-auth-secret; your backend derives a credential by signing a username that encodes an expiry timestamp. At allocation time coturn recomputes the same HMAC-SHA1 and compares β€” if the timestamp is in the past or the signature does not match, the Allocate is rejected. No database lookup, no shared state across relay nodes.

The username format is ${expiryUnixTimestamp}:${userId} and the credential is base64( HMAC_SHA1( username, static-auth-secret ) ). The TTL is the only real tuning knob and it is a direct security-versus-ergonomics trade. A 1-hour TTL shrinks the window in which a leaked credential is useful but forces re-fetches on long calls and after every ICE restart. A 24-hour TTL eliminates that churn but widens exposure. Most production deployments land at 1–12 hours and cache the credential client-side for its lifetime so restartIce() does not trigger a fresh signalling round-trip. The userId half is informational β€” coturn does not validate it against any user store β€” so use it for log correlation, not authorization.

Minimal Runnable Implementation

The signing must happen server-side; the static secret must never reach the browser. This Express endpoint mints a credential and returns only the public fields.

const crypto  = require('crypto');
const express = require('express');
const app     = express();

// Compose username = expiry:userId, credential = base64 HMAC-SHA1(username, secret)
function generateTurnCredentials(userId, secret, ttlSeconds = 3600) {
  const expiry   = Math.floor(Date.now() / 1000) + ttlSeconds;  // absolute UNIX expiry
  const username = `${expiry}:${userId}`;                       // coturn parses the prefix
  const credential = crypto
    .createHmac('sha1', secret)        // key = the relay's static-auth-secret
    .update(username)                  // sign the FULL username string, exactly
    .digest('base64');                 // base64, NOT hex β€” coturn expects base64
  return { username, credential, ttl: ttlSeconds };
}

app.get('/api/turn-credentials', (req, res) => {
  const userId = req.session?.userId || 'anon';   // identify for log correlation only
  const creds  = generateTurnCredentials(userId, process.env.TURN_SECRET, 3600);
  res.set('Cache-Control', 'no-store');           // never let a proxy cache a credential
  res.json({
    username:   creds.username,
    credential: creds.credential,
    ttl:        creds.ttl,
    urls: [                                        // hand the client both transports
      'turn:turn.example.com:3478?transport=udp',
      'turns:turn.example.com:5349?transport=tcp'
    ]
  });
});

app.listen(8081);

The relay side needs only the matching directives β€” lt-cred-mech, use-auth-secret, and the same static-auth-secret β€” covered in full in Configuring Coturn for Production TURN Relay. The client then drops username and credential straight into its iceServers entries; delivery happens over the encrypted channel described in WebSocket Signaling Implementation, and the relay itself is reached only after STUN Server Deployment Strategies have exhausted the cheaper srflx path.

Reproduction Steps & Debugging Log Patterns

  1. Reproduce the signature on the command line so you can diff it against the Node output. The two must be byte-identical:
# Independently recompute the credential for a fixed username
printf '%s' '1780000000:alice' \
  | openssl dgst -sha1 -hmac "$TURN_SECRET" -binary | base64
  1. Drive a real allocation with that pair using coturn’s test client:
turnutils_uclient -u "1780000000:alice" -w "<base64-credential>" \
  -y -m 5 turn.example.com    # -y verbose, -m 5 relay five messages
  1. On success the log shows INFO: session <id>: realm <…> user <1780000000:alice>: incoming packet ALLOCATE processed, success followed by relayed address … allocated.
  2. On an expired or wrong credential the log shows 401 then error 401 (Unauthorized) β€” confirm the username prefix is a future UNIX timestamp and that the secret matches exactly. A 438 (Stale Nonce) is normal mid-handshake; the client retries with the fresh nonce automatically.
  3. A common mismatch is hex-vs-base64: if the credential validates with openssl but coturn rejects it, confirm you passed .digest('base64') and not 'hex'.

Common Implementation Mistakes

FAQ

What TTL should I choose? Between 1 and 12 hours for most apps. Shorter TTLs reduce the value of a leaked credential but force re-fetches on long calls and ICE restarts; cache the credential client-side for its lifetime to avoid redundant signalling round-trips.

Does the userId in the username need to match a real account? No. coturn does not validate the user portion against any store β€” it only checks the HMAC and the expiry. Use userId for log correlation and rate-limiting on your own backend, not as an authorization gate.

Can I rotate the secret without breaking outstanding credentials? Yes β€” coturn accepts multiple static-auth-secret lines. Add the new secret, reload, and let existing credentials expire by their TTL before removing the old one; both validate during the overlap window.

Related: this builds on TURN Server Configuration & Auth and pairs with Configuring Coturn for Production TURN Relay and STUN Server Deployment Strategies.