How TURN Credentials Are Configured and Rotated for WebRTC Calls in Dograh

Dograh implements a time-limited TURN REST API that generates ephemeral credentials using HMAC-SHA1 signatures, where each WebRTC session receives unique username-password pairs encoded with expiration timestamps to enable automatic rotation without server-side state.

WebRTC applications require secure TURN (Traversal Using Relays around NAT) servers to relay media when direct peer-to-peer connections fail. In the Dograh open-source platform, TURN credentials are configured and rotated for WebRTC calls through a cryptographic REST API pattern embedded in the Python backend, eliminating the need for long-lived secrets by encoding expiry timestamps directly into credential usernames.

Environment-Based TURN Configuration

All TURN server parameters are externalized into environment variables and loaded at runtime in api/constants.py (lines 31-41). The backend validates the presence of these variables once at import time to ensure the credential generation subsystem can function immediately.

The following environment variables control the TURN infrastructure:

  • TURN_HOST — DNS name or IP address of the TURN server (default: localhost)
  • TURN_PORT — UDP/TCP port for non-TLS TURN traffic (default: 3478)
  • TURN_TLS_PORT — Port for TURN over TLS (default: 5349)
  • TURN_SECRET — Shared secret used for HMAC-SHA1 signing (no default; required)
  • TURN_CREDENTIAL_TTL — Credential lifetime in seconds (default: 86400, i.e., 24 hours)
  • FORCE_TURN_RELAY — Boolean flag that forces clients to use only relay candidates (default: false)

These values propagate to api/routes/turn_credentials.py, where the generate_turn_credentials() function consumes them to construct time-bound access tokens.

HMAC-SHA1 Credential Generation

When an authenticated client requests TURN access, the backend in api/routes/turn_credentials.py (lines 57-90) executes generate_turn_credentials() to produce a cryptographically signed credential pair. The implementation follows the TURN REST API specification:

def generate_turn_credentials(user_id: str, ttl: int = TURN_CREDENTIAL_TTL) -> dict:
    # 1. Calculate expiration timestamp

    expiration = int(time.time()) + ttl
    
    # 2. Construct username as "<expiration>:<user_id>"

    username = f"{expiration}:{user_id}"
    
    # 3. Generate password as base64(HMAC-SHA1(TURN_SECRET, username))

    password = base64.b64encode(
        hmac.new(
            TURN_SECRET.encode("utf-8"),
            username.encode("utf-8"),
            hashlib.sha1,
        ).digest()
    ).decode("utf-8")
    
    return {
        "username": username,
        "password": password,
        "ttl": ttl,
        "uris": build_turn_uris()  # Prioritizes UDP in production, TCP for local dev

    }

The function builds the TURN URIs list dynamically based on the environment. For local development (notably macOS Docker configurations), it prefers TCP transports, while production deployments prioritize UDP for lower latency. When TURN_TLS_PORT is defined, the function appends secure turns:// URIs to the array (see lines 92-124 in the same file).

Automatic Rotation via Ephemeral Credentials

Credential rotation occurs implicitly because every GET request to /turn/credentials invokes generate_turn_credentials() fresh, producing a distinct username and password pair. The endpoint implementation in api/routes/turn_credentials.py (lines 67-124) is protected by the standard authentication dependency get_user:

@router.get("/credentials", response_model=TurnCredentialsResponse)
async def get_turn_credentials(user: UserModel = Depends(get_user)):
    credentials = generate_turn_credentials(str(user.id))
    return TurnCredentialsResponse(**credentials)

The username embeds the Unix expiration timestamp, allowing the TURN server (e.g., coturn configured with use-auth-secret) to validate credentials statelessly. When the TURN server receives a request, it recomputes the HMAC-SHA1 signature using its copy of TURN_SECRET and the provided username. If the current time exceeds the timestamp embedded in the username, the server rejects the connection, forcing the client to request a fresh credential pair from Dograh's API.

Client-Side Integration

The Dograh React frontend consumes these credentials through the useWebSocketRTC hook located at ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx (lines 67-124). The hook fetches credentials via the generated SDK method and injects them into the RTCPeerConnection:

const turnResponse = await client.turn.getCredentials();
const iceServers = [
  {
    urls: turnResponse.data.uris,
    username: turnResponse.data.username,
    credential: turnResponse.data.password
  }
];
const pc = new RTCPeerConnection({ iceServers });

The embedded widget (ui/public/embed/dograh-widget.js, lines 695-724) performs an equivalent fetch using session tokens, falling back to STUN-only configurations when the backend indicates TURN is disabled. The TypeScript SDK wrapper in ui/src/client/sdk.gen.ts (around line 1240) provides the typed getCredentials() abstraction for safer client consumption.

Rotating Secrets and Operational Controls

Dograh provides three mechanisms to control credential behavior without code changes:

Adjusting TTL — Modify TURN_CREDENTIAL_TTL in the environment variables. The backend honors the new value immediately for all subsequent credential generations because the constant is evaluated at import time.

Secret Rotation — Changing TURN_SECRET forces all future credentials to be signed with the new cryptographic key. Existing credentials remain valid until their embedded expiration timestamps pass, ensuring zero-downtime rotation. A process reload ensures the new secret resides in memory.

Force Relay Mode — Setting FORCE_TURN_RELAY=true (read in api/constants.py, lines 36-40) modifies the returned ICE configuration to contain only TURN URIs. This diagnostic flag exposes connectivity issues immediately by preventing direct peer-to-peer or STUN candidates.

Summary

  • Configuration occurs via environment variables in api/constants.py, including the critical TURN_SECRET and TURN_CREDENTIAL_TTL.
  • Generation uses generate_turn_credentials() in api/routes/turn_credentials.py to create HMAC-SHA1 signed passwords with usernames formatted as <expiration>:<user_id>.
  • Rotation is automatic and stateless; every API request produces new credentials, and the TURN server rejects expired usernames based on embedded timestamps.
  • Client integration spans the React hook useWebSocketRTC, the embedded widget, and the auto-generated TypeScript SDK.
  • Operational levers include the FORCE_TURN_RELAY flag for debugging and environment-driven TTL adjustments.

Frequently Asked Questions

How does the TURN server validate Dograh's ephemeral credentials?

The TURN server—configured with use-auth-secret—receives the username and password from the WebRTC client. It extracts the expiration timestamp from the username, validates that it has not passed, and recomputes the base64(HMAC-SHA1(TURN_SECRET, username)) using its local copy of the shared secret. If the computed signature matches the provided password, the credential is valid.

What happens when TURN_SECRET is rotated in a running Dograh deployment?

Existing credentials remain functional until their embedded expiration timestamps expire because the TURN server validates against the current secret. New credential requests immediately use the updated TURN_SECRET from the environment. To apply the change, reload the Python process so api/constants.py re-imports the new value.

How can I force WebRTC traffic through TURN relays for connectivity testing?

Set the environment variable FORCE_TURN_RELAY=true before starting the Dograh API. This flag, consumed in api/constants.py (lines 36-40), causes the /turn/credentials endpoint to return ICE configurations containing only TURN server URIs, blocking direct peer-to-peer and STUN candidates and ensuring all media flows through the relay path.

What is the default credential lifetime and how do I change it?

The default TTL is 86400 seconds (24 hours), defined by TURN_CREDENTIAL_TTL in api/constants.py. To change it, update the environment variable and restart the API process. Clients must fetch new credentials before the Unix timestamp embedded in their current username elapses, or the TURN server will reject the connection.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →