CDN Token Auth — Protect Your Content with Signed URLs
Token Auth lets you serve content from your CDN zone only to people you authorize. Each link you publish carries a cryptographic signature with an expiry date — anyone trying to access without a valid signed URL gets a 403 Forbidden.
It's the standard pattern for paid video on demand, gated downloads, private media galleries, time-limited file shares, and any case where you want to keep your CDN content out of search engines and hotlinkers.
How it works (in one paragraph)
When you enable Token Auth on a zone, CubePath generates a shared secret for it. To publish a link, your backend computes a small HMAC-SHA256 over the path you want to share, an expiry timestamp, and the secret. The result is base64-url-encoded and added to the URL as ?token=...&expires=.... When the request reaches the CDN, we recompute the same HMAC using the secret we have for that zone and compare — match means we serve, mismatch means 403. The secret stays in your backend and in our infrastructure; it never travels in the URL.
Enabling it
In the CubePath dashboard, open your CDN zone → Settings tab → Signed URLs (Token Auth) card → toggle Enable Token Auth. The first time you enable it we generate a secret and show it once in a green alert. Copy it immediately to a secure place (env var, secrets manager). We never show it again.
If you ever need to start over with a new secret, click Rotate secret. Rotation invalidates every URL signed with the old secret instantly — useful if the secret leaks.
Important: make sure you save the secret on first display. We don't store it in plaintext anywhere you can retrieve it later — only encrypted at rest so the CDN can validate signatures. If you lose it, you must rotate.
Signing URLs from your backend
The math: HMAC-SHA256(secret, path + str(expires)) → base64url without padding.
Python
import hmac, hashlib, base64, time
SECRET = "<your_zone_secret>"
def sign(path: str, ttl_seconds: int = 3600) -> str:
expires = int(time.time()) + ttl_seconds
msg = f"{path}{expires}".encode()
mac = hmac.new(SECRET.encode(), msg, hashlib.sha256).digest()
token = base64.urlsafe_b64encode(mac).rstrip(b"=").decode()
return f"https://your-zone.cubecdn.io{path}?token={token}&expires={expires}"
print(sign("/videos/clip.mp4", ttl_seconds=3600))
Node.js
const crypto = require('crypto');
const SECRET = process.env.CDN_SECRET;
function sign(path, ttlSeconds = 3600) {
const expires = Math.floor(Date.now() / 1000) + ttlSeconds;
const mac = crypto.createHmac('sha256', SECRET).update(`${path}${expires}`).digest();
const token = mac.toString('base64url').replace(/=+$/, '');
return `https://your-zone.cubecdn.io${path}?token=${token}&expires=${expires}`;
}
PHP
function sign($path, $ttlSeconds = 3600) {
$secret = getenv('CDN_SECRET');
$expires = time() + $ttlSeconds;
$mac = hash_hmac('sha256', $path . $expires, $secret, true);
$token = rtrim(strtr(base64_encode($mac), '+/', '-_'), '=');
return "https://your-zone.cubecdn.io{$path}?token={$token}&expires={$expires}";
}
From the dashboard (testing)
The Settings card has a small Generate signed URL subform for testing — paste a path, set a validity window, click Generate, and you get a ready-to-paste URL. Use this for one-off shares or to debug; for production traffic at scale sign in your backend with the snippets above (no API round-trip per URL).
Choosing the right expires_in
There's no perfect value, just trade-offs:
| Validity | Use case | Trade-off |
|---|---|---|
| 5 min | One-off downloads, secured API responses | Refreshing the page after the window means re-signing |
| 1 hour | Video playback sessions | Long videos can outlive the window — handle re-signing on the client |
| 24 hours | Daily content rotation, login session tied to URL | Leaked URL is usable for a day |
| 7 days | Static cacheable media, app bundles | Long exposure if leaked — consider IP binding |
The API caps expires_in at 7 days (604800 s) to limit blast radius from leaks.
Optional: Bind tokens to the client IP
Same Settings card has a Bind tokens to client IP toggle. With it on, the signature also includes the request's source IP — meaning a leaked URL only works from the IP that received it. Strong defense against link-sharing in chats and forums.
The cost: clients whose IP changes during the session (mobile networks on CGNAT, hotel WiFi, VPN, roaming between 4G and home WiFi) lose access mid-stream. For consumer video streaming this is usually a bad trade-off — short expires_in is usually enough.
If you do enable it, pass the client IP when signing:
def sign(path, ttl_seconds=3600, client_ip=None):
expires = int(time.time()) + ttl_seconds
msg = f"{path}{expires}".encode()
if client_ip:
msg += client_ip.encode()
mac = hmac.new(SECRET.encode(), msg, hashlib.sha256).digest()
...
The IP must be the client's public IP (typically request.headers['X-Forwarded-For'].split(',')[0].strip() if you're behind a proxy, or request.client.host otherwise). IPv6 addresses should be in their canonical compressed form (2001:db8::1, not [2001:db8::1]).
Rotation playbook
- Generate a new secret → Rotate secret button in the dashboard.
- Copy the new secret immediately — same one-shot reveal rule.
- Update your backend's
CDN_SECRETenv var with the new value. - Restart / redeploy your app so it picks up the new secret.
- From that moment on every new URL you sign uses the new secret. Older URLs (signed with the old secret) start returning 403 immediately.
If you have content currently being streamed when you rotate, those sessions break. Plan rotations for low-traffic windows or sign with short expires_in so cuts during rotation are minimal.
What Token Auth doesn't do
- It's not a Web Application Firewall: it doesn't filter bots, doesn't rate-limit, doesn't block by country. Use CDN WAF rules for that.
- It's not authentication of users: it authorizes one URL against a shared secret. Anyone with the signed URL can use it (unless IP binding is on).
- It doesn't encrypt content: traffic is HTTPS, but the content itself is the same bytes you have in your origin. Don't use Token Auth as a substitute for at-rest encryption of truly sensitive data.
Troubleshooting
| Symptom | Likely cause |
|---|---|
403 on every URL | Token Auth is on but you're requesting without ?token= or with a token computed with the wrong secret |
403 only on some URLs | Mismatch between path you signed and path the client requests (trailing slash, case, percent-encoding) — sign and request the exact same string |
403 after rotation | Cached URLs from before rotation — re-sign with the new secret |
| Works in browser, fails on mobile | IP binding is on and the client roamed between networks — turn off IP binding or shorten TTL |
Customers report random 403s during playback | URL expired mid-stream — increase expires_in, or have the player re-fetch a new signed URL near expiry |
API reference
PATCH /cdn/zones/{uuid} { "token_auth_enabled": true|false,
"token_auth_ip_binding": true|false }
POST /cdn/zones/{uuid}/token-auth/rotate-secret → returns the new secret
POST /cdn/zones/{uuid}/token-auth/sign-url { "path": "...", "expires_in": N,
"client_ip": "..."? }
The secret is returned exactly once — on first enable, and on every rotate.
