Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 8 min read

Postgres Presence Ledger: Room Tokens Against Shared Links for Media Devices

Issue short-lived, room-scoped tokens when device presence on a media operations dashboard must be accurate; keep a shared join link only as an invitation that is exchanged for a scoped credential. The deciding constrain

Issue short-lived, room-scoped tokens when device presence on a media operations dashboard must be accurate; keep a shared join link only as an invitation that is exchanged for a scoped credential. The deciding constraint is identity continuity: a reusable URL can admit someone to a video call, but it cannot by itself prove which device is still connected, which room it belongs to, or whether a later reconnect is the same authorized session.

TL;DR: Put authorization before the WebRTC session, give each admitted device a unique session identifier, and record its lease in Postgres. Presence then comes from authenticated session events plus an expiry deadline, not from possession of a copied URL. This adds an issuer and a small state machine, but it makes revocation, audit, and stale-device cleanup explicit. Shared links remain reasonable for low-risk, short-lived calls where approximate attendance is acceptable.

What must remain true when a device reconnects?

This decision starts with invariants, because β€œthe link worked” is not an authorization model. For a media control room streaming encoder, camera, and monitor status to a live dashboard, I would require five properties:

  1. A credential names one room, one device or participant, and one authorization session.
  2. Admission expires, while a reconnect can be distinguished from a new admission.
  3. Revoking one device does not rotate the invitation for every operator.
  4. The dashboard never treats an expired lease as online merely because a disconnect event was lost.
  5. Signaling authorization and media transport remain separate concerns. WebRTC defines peer connections and media/data transport behavior; the application still owns identity and access policy.

The last point is easy to blur. An ICE connection state describes transport progress, not business authorization, and browser shutdown, network loss, and process termination do not guarantee that a final β€œoffline” message reaches the server. Silence is ambiguous.

So the presence record needs a deadline. A device renews a lease while its authenticated signaling session is healthy; the dashboard derives online from lease_expires_at > now(). A disconnect can shorten that deadline immediately, but correctness cannot depend on receiving it.

Should RTC room tokens replace a shared join link?

For this dashboard, yes, but the comparison is narrower than it first appears: RTC room tokens should replace direct authorisation by a shared join link, while the link can remain the invitation. The chosen design uses a shared URL only to carry an opaque, revocable invitation reference. A server exchanges that reference, after any required user or device check, for a short-lived room token. The token contains an audience, issuer, subject, room identifier, session identifier, issued-at time, and expiry; the verifier fixes the accepted algorithm and validates every relevant claim. RFC 8725 specifically warns against accepting whatever algorithm an untrusted JWT header requests and against confusing tokens meant for different contexts. This extra complexity buys a specific property, not abstract β€œsecurity”: the presence ledger can associate every renewal and revocation with one admitted device session.

Postgres is the authority for session status and lease expiry. It is not asked to carry media packets. A uniqueness constraint on session_id, and an update conditioned on the same device and room, prevents a heartbeat from quietly moving a session between rooms. Store token digests or opaque identifiers when server-side lookup is required; do not store bearer credentials in plaintext logs.

Decision factor Room-scoped token Shared join link used directly
Identity granularity Distinct subject and session claims Usually proves possession of the same secret
Single-device revocation Revoke one session or deny renewal Often requires replacing the shared secret
Presence attribution Heartbeats bind to a stable session Concurrent users can be indistinguishable
Replay boundary Limited by expiry, audience, room, and server state Persists until the link is rotated or disabled
Operational load Issuance, verification, key rotation, and session cleanup Link creation and coarse rotation
Best fit Managed devices and auditable rooms Low-risk gatherings with approximate attendance

Neither option makes the URL confidential after it has been copied. A room token is also a bearer credential unless it is bound to stronger proof, so TLS, careful log redaction, brief validity, and replay handling still matter. Short expiry narrows exposure, but making it extremely short increases renewal traffic and turns clock skew or issuer unavailability into an admission failure. Choose the lifetime from the tolerated revocation delay and test that boundary; there is no universal number supported by the protocol.

It can still fail.

Name the failures plainly: invitation leakage, token replay, wrong-room acceptance, expired-token acceptance, signing-key rollover, duplicate reconnects, heartbeat loss, delayed disconnects, database contention, and verifier clock skew. A design review that covers only the happy-path join has not covered presence.

Critical path in Python

The critical path is deliberately small. This example assumes authentication and signature verification happened before authorize_claims is called; claim validation is shown because that is where room confusion and accidental token reuse become data corruption. Times are server-derived UTC values, and parameters are bound rather than interpolated. The omitted token decoder must reject an unexpected signing algorithm before claims reach this function; treating decoding as proof without that verifier configuration would leave the most important check outside the example.

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone


@dataclass(frozen=True)
class Admission:
    issuer: str
    audience: str
    subject: str
    room_id: str
    session_id: str
    expires_at: datetime


def authorize_claims(claims: dict, expected_room: str, now: datetime) -> Admission:
    required = {"iss", "aud", "sub", "room_id", "session_id", "exp"}
    if not required.issubset(claims):
        raise PermissionError("missing required claim")
    if claims["iss"] != "https://auth.example.invalid":
        raise PermissionError("unexpected issuer")
    if claims["aud"] != "media-presence":
        raise PermissionError("unexpected audience")
    if claims["room_id"] != expected_room:
        raise PermissionError("wrong room")

    expires_at = datetime.fromtimestamp(claims["exp"], tz=timezone.utc)
    if expires_at <= now:
        raise PermissionError("expired credential")

    return Admission(
        issuer=claims["iss"],
        audience=claims["aud"],
        subject=claims["sub"],
        room_id=claims["room_id"],
        session_id=claims["session_id"],
        expires_at=expires_at,
    )


def renew_presence(connection, admission: Admission, now: datetime) -> None:
    lease_until = min(now + timedelta(seconds=30), admission.expires_at)
    with connection.transaction():
        updated = connection.execute(
            """
            UPDATE presence_sessions
               SET last_seen_at = %s, lease_expires_at = %s
             WHERE session_id = %s
               AND device_id = %s
               AND room_id = %s
               AND revoked_at IS NULL
            """,
            (
                now,
                lease_until,
                admission.session_id,
                admission.subject,
                admission.room_id,
            ),
        )
        if updated.rowcount != 1:
            raise PermissionError("session is unknown or revoked")

The illustrative 30-second lease is not a protocol recommendation. It exists to expose the design variable: expiry must be longer than normal heartbeat jitter yet shorter than the maximum stale-online interval the operations team accepts. Measure the delay distribution under packet loss and deployment restarts, then set an alert on lease-renewal failures before arguing about a smaller interval.

There is another race. Two processes may reconnect with the same credential and both renew one session row. If that is unacceptable, issue a fresh session identifier on exchange, reject reuse transactionally, or track a connection generation and allow only the newest generation to renew. Do not infer exclusivity from JWT validation alone.

How should this be tested and operated?

Test authorization separately from WebRTC transport. Unit tests should reject a wrong audience, wrong issuer, wrong room, missing subject, expired credential, revoked session, and replayed single-use exchange. Integration tests should kill the signaling process without a clean close, pause heartbeats, reconnect with the same device identity, and verify that the dashboard moves through the intended state without inventing a second device.

Then test time. Run verifier and issuer clocks near the permitted skew, rotate signing keys while sessions are active, and keep old verification material available only for the overlap your token lifetime requires. Record issuer, key identifier, room, session, decision, and reason in the authorization audit event, but omit the bearer token. OWASP's logging guidance recommends excluding access tokens and other primary secrets from logs.

Deployment needs a failure policy. If the token issuer is unavailable, already admitted sessions can continue until their validated credential or server-side session expires; new admissions fail closed. If Postgres is temporarily unavailable, buffering presence writes may preserve throughput but makes the displayed state less trustworthy. For a control-room dashboard whose primary axis is presence accuracy, show an explicit β€œunknown” state after the freshness threshold rather than claiming β€œonline” or β€œoffline” without current evidence.

Watch four signals: authorization denials by reason, invitation exchanges by room, lease-renewal latency and failures, and the count of sessions that expire without a clean disconnect. Those measures locate policy mistakes and infrastructure trouble without pretending that an ICE state alone answers who is authorized.

The cost difference is mostly engineering surface, not a stable per-minute price. Scoped tokens require an issuer, key management, claim validation, revocation semantics, database writes, monitoring, and on-call ownership. Shared links reduce those components but transfer cost into coarse revocation, weak attribution, and manual incident response. Estimate both with expected rooms, devices, heartbeat rate, retention, and tolerated stale-presence time; vendor unit prices change too often to carry the architecture decision. This is also a team constraint: a service with key rotation and revocation but no clear owner is more dangerous than a modest shared-link system whose coarse boundary is honestly documented, because the former invites operators to trust controls that may quietly expire or stop being observed.

Rejected option, with its valid use case

Direct authorization by shared join link is rejected for the media-device dashboard because possession is shared while presence must be attributed per device. Rotating the link after one encoder is removed can also disrupt every legitimate participant, and a copied link gives the server no reliable basis for deciding which physical device returned after a network interruption.

The selected token design has real limitations. It is not suitable for a team that cannot operate an issuer, protect signing keys, rotate verification material, and clean up session state; it also does not solve compromised endpoints, stolen bearer credentials before expiry, or authorization mistakes encoded by the issuer. Its main trade-off is a larger failure surface in exchange for finer identity and revocation boundaries.

It still has a valid use case. For an informal, short-lived video call with no privileged device controls, no per-participant audit requirement, and an acceptable risk of approximate attendance, a reusable invitation can be the simpler system. Protect it as a secret, support room-wide rotation, avoid indexing the full URL in logs or analytics, and state clearly that the attendee list is observational rather than authoritative.

That boundary matters. Use scoped sessions when a false β€œonline” status could mislead media operations; use a shared invitation when coarse admission is the actual requirement. The architecture should pay for the accuracy the job needs, no more and no less.

References

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.