Tokens
A token is a signed, expiring pass for one user in one room. The format
matches bonfire's GatherWs tokens, so a Soli app mints them
with builtins it already has, and the SFU never calls back into the app.
Format
sfu1.<base64(user_id)>:<base64(room)>:<unix_exp>.<hmac>
hmac = HMAC-SHA256("sfu1." + payload, secret), lowercase hexMinting from Soli
static def mint_sfu_token(user_id, room)
secret = getenv("SOLI_SFU_SECRET") ?? getenv("SOLI_WEBHOOK_SECRET") ?? ""
return "" if secret.blank?
exp = DateTime.utc().to_unix() + 3600
payload = Base64.encode(user_id) + ":" + Base64.encode(room) + ":" + str(exp)
signed = "sfu1." + payload
signed + "." + Crypto.hmac(signed, secret)
end
Crypto.hmac is HMAC-SHA256 with lowercase hex. The Rust side
pins that exact construction in a unit test, so drift on either side
fails CI. Keep lifetimes short: an hour is plenty, since a token is only
needed to join and to change groups.
Where the secret comes from
| Order | Source |
|---|---|
| 1 | SOLI_SFU_SECRET environment variable. A dedicated secret is best. |
| 2 | secret in the TOML config. |
| 3 | SOLI_WEBHOOK_SECRET, to share bonfire's existing secret. |
The server refuses to start with no secret unless dev tokens are enabled.
Minting from the command line
SOLI_SFU_SECRET=... soli-sfu mint-token user-42 spatial:acme 3600
# sfu1.dXNlci00Mg==:c3BhdGlhbDphY21l:1781240000.9f3a…Dev tokens
With allow_unauthenticated = true, as in
dev.toml, the SFU also accepts unsigned
dev.<user_id>.<room> tokens. The test client sends
one when its token field is empty.
Never turn this on in production. A dev token has no signature and no expiry, so anyone who can reach the control API can join any room. The server logs a warning at startup when it is on.
What a token does not cover
- It does not encrypt media: DTLS-SRTP does that for every session.
- It does not decide who hears whom inside a room. Whoever holds the token sets
peers; read the security model before relying on it for privacy.