Skip to content

Signed, expiring links ​

Signed links let your own application decide who may listen. Your app creates a link that works for a limited time; Quikcast only checks that the link is genuine and not expired. There are no accounts, sessions or callbacks in Quikcast, so it stays simple and fast.

Typical uses: members-only streams, paid access, or a "listen now" button that only works for an hour.

1. Share a key ​

Create a secret key that only your application and Quikcast know:

sh
openssl rand -hex 32 > secrets/members-key

Protect the mount with it:

toml
[[mounts]]
path = "/members.mp3"
secret_file = "secrets/members-source"   # the encoder password, a different secret

[mounts.listener_auth]
type = "signed_url"
secret_file = "secrets/members-key"
max_ttl_seconds = 3600
clock_skew_seconds = 0
SettingMeaning
max_ttl_secondsHow far into the future a link may expire. Links asking for longer are refused. Default 86400 (a day), maximum one week.
clock_skew_secondsExtra time after expiry that is still accepted, to forgive clocks that disagree. 0–60, default 0.

A link needs two query parameters: expires (a Unix timestamp in seconds) and sig (a signature). The signature is an HMAC-SHA-256 of this text, using your key:

text
<path>\n<expires>

That's the mount path, a single newline, then the expiry number, with no trailing newline. The result is encoded as base64url without padding.

js
import { createHmac } from 'node:crypto'

export function signedUrl(base, path, key, ttlSeconds = 3600) {
  const expires = Math.floor(Date.now() / 1000) + ttlSeconds
  const sig = createHmac('sha256', key).update(`${path}\n${expires}`).digest('base64url')
  return `${base}${path}?expires=${expires}&sig=${sig}`
}
python
import base64, hashlib, hmac, time

def signed_url(base, path, key, ttl=3600):
    expires = int(time.time()) + ttl
    mac = hmac.new(key, f"{path}\n{expires}".encode(), hashlib.sha256).digest()
    sig = base64.urlsafe_b64encode(mac).rstrip(b"=").decode()
    return f"{base}{path}?expires={expires}&sig={sig}"
php
function signedUrl(string $base, string $path, string $key, int $ttl = 3600): string {
    $expires = time() + $ttl;
    $mac = hash_hmac('sha256', "$path\n$expires", $key, true);
    $sig = rtrim(strtr(base64_encode($mac), '+/', '-_'), '=');
    return "$base$path?expires=$expires&sig=$sig";
}

The key is the file's contents as text. If you made it with openssl rand -hex 32, the key is those 64 hex characters, not their decoded bytes.

3. Test your signer ​

Use this known-good example to check your code. With this key:

text
0123456789abcdef0123456789abcdef

the path /private-radio and expiry 1791504000 must produce:

text
VFu-3J9JLyvyAbZwMz_upmhVy0BouocqUWFd4ADtedY

Rules to remember ​

  • expires is a plain number: no sign, spaces or leading zeros. The parameter names are exactly expires and sig.
  • Any problem (bad signature, expired, too far in the future, duplicate parameters) gives the same 403 Forbidden.
  • Other query parameters are ignored and are not signed.
  • Your server's clock must be right. Quikcast compares against its own clock.
  • Don't log full URLs with their query strings in proxies or CDNs.

HLS streams ​

HLS streams can be protected the same way. The identity you sign is the stream's public path, for example /hls/members, no matter which playlist or segment is requested. One signature covers the master playlist, every rendition and all segments of that stream.

toml
[hls.listener_auth."/hls/members"]
type = "signed_url"
secret_file = "secrets/members-key"

Sign /hls/members and put expires and sig on the master playlist URL. Quikcast copies them into the playlist entries it generates, so players follow signed links all the way down.

Documentation for Quikcast.