Appearance
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-keyProtect 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| Setting | Meaning |
|---|---|
max_ttl_seconds | How far into the future a link may expire. Links asking for longer are refused. Default 86400 (a day), maximum one week. |
clock_skew_seconds | Extra time after expiry that is still accepted, to forgive clocks that disagree. 0–60, default 0. |
2. Sign links in your application
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
0123456789abcdef0123456789abcdefthe path /private-radio and expiry 1791504000 must produce:
text
VFu-3J9JLyvyAbZwMz_upmhVy0BouocqUWFd4ADtedYRules to remember
expiresis a plain number: no sign, spaces or leading zeros. The parameter names are exactlyexpiresandsig.- 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.