Token Auth en el CDN — Protege tu Contenido con URLs Firmadas

Token Auth te permite servir contenido desde tu zona CDN únicamente a las personas que tú autorices. Cada enlace que publicas lleva una firma criptográfica con fecha de caducidad — cualquiera que intente acceder sin una URL firmada válida recibe un 403 Forbidden.

Es el patrón estándar para video bajo demanda de pago, descargas con permiso, galerías privadas, archivos compartidos con caducidad, y cualquier caso donde quieras mantener tu contenido CDN fuera de buscadores y hotlinkers.

Cómo funciona (en un párrafo)

Cuando activas Token Auth en una zona, CubePath genera un secret compartido para ella. Para publicar un enlace, tu backend calcula un pequeño HMAC-SHA256 sobre el path que quieres compartir, una marca de caducidad, y el secret. El resultado se codifica en base64-url y se añade a la URL como ?token=...&expires=.... Cuando la petición llega al CDN, recalculamos el mismo HMAC con el secret que tenemos para esa zona y los comparamos — coincidencia → sirve, mismatch → 403. El secret se queda en tu backend y en nuestra infraestructura; no viaja en la URL.

Activarlo

En el panel CubePath: tu zona CDN → pestaña Settings → tarjeta URLs firmadas (Token Auth) → activa Habilitar Token Auth. La primera vez que lo activas generamos un secret y lo mostramos una sola vez en una alerta verde. Cópialo inmediatamente a un sitio seguro (variable de entorno, gestor de secretos). No lo volvemos a mostrar.

Si en algún momento necesitas empezar de cero con un secret nuevo, pulsa Rotar secret. La rotación invalida instantáneamente todas las URLs firmadas con el secret antiguo — útil si el secret se filtra.

Importante: asegúrate de guardar el secret en la primera muestra. No lo almacenamos en texto plano en ningún sitio que puedas recuperar después — solo cifrado en disco para que el CDN pueda validar las firmas. Si lo pierdes, tienes que rotar.

Firmar URLs desde tu backend

La fórmula: HMAC-SHA256(secret, path + str(expires)) → base64url sin padding.

Python

import hmac, hashlib, base64, time

SECRET = "<tu_secret_de_zona>"

def firmar(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://tu-zona.cubecdn.io{path}?token={token}&expires={expires}"

print(firmar("/videos/clip.mp4", ttl_seconds=3600))

Node.js

const crypto = require('crypto');
const SECRET = process.env.CDN_SECRET;

function firmar(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://tu-zona.cubecdn.io${path}?token=${token}&expires=${expires}`;
}

PHP

function firmar($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://tu-zona.cubecdn.io{$path}?token={$token}&expires={$expires}";
}

Desde el panel (testing)

La tarjeta de Settings tiene un mini-formulario Generar URL firmada para probar — pega un path, ajusta la validez, pulsa Generar, y te da una URL lista para pegar. Úsalo para enlaces puntuales o para depurar; para tráfico en producción firma en tu backend con los snippets de arriba (sin tener que llamar a nuestra API por cada URL).

Eligiendo el expires_in adecuado

No hay un valor perfecto, solo trade-offs:

ValidezCaso de usoCompromiso
5 minDescargas puntuales, respuestas API protegidasRefrescar la página después del plazo requiere re-firmar
1 horaSesiones de reproducción de vídeoVídeos largos pueden superar la ventana — gestiona re-firmado en el cliente
24 horasRotación diaria de contenido, sesión de login atada a URLUna URL filtrada es usable un día entero
7 díasMedia estático cacheable, app bundlesExposición larga si se filtra — considera IP binding

La API limita expires_in a un máximo de 7 días (604800 s) para limitar el daño en caso de fuga.

Opcional: Vincular tokens a la IP del cliente

La misma tarjeta de Settings tiene un toggle Vincular tokens a la IP del cliente. Con esto activo, la firma incluye también la IP origen del request — significa que una URL filtrada solo funciona desde la IP que la recibió. Defensa fuerte contra el reparto de enlaces en chats y foros.

El coste: clientes cuya IP cambia durante la sesión (móviles en CGNAT, WiFi de hotel, VPN, roaming entre 4G y WiFi de casa) pierden el acceso a mitad de la reproducción. Para video consumer este suele ser un mal trade-off — un expires_in corto suele bastar.

Si lo activas, pasa la IP del cliente al firmar:

def firmar(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()
    ...

La IP tiene que ser la IP pública del cliente (típicamente request.headers['X-Forwarded-For'].split(',')[0].strip() si estás detrás de un proxy, o request.client.host si no). IPv6 debe ir en forma comprimida canónica (2001:db8::1, no [2001:db8::1]).

Manual de rotación

  1. Genera un secret nuevo → botón Rotar secret en el panel.
  2. Copia el nuevo secret inmediatamente — misma regla de revelación única.
  3. Actualiza la variable CDN_SECRET de tu backend con el valor nuevo.
  4. Reinicia / redespliega tu aplicación para que coja el nuevo secret.
  5. Desde ese momento todas las URLs nuevas que firmes usan el nuevo secret. Las URLs antiguas (firmadas con el secret anterior) empiezan a devolver 403 al instante.

Si tienes contenido reproduciéndose cuando rotas, esas sesiones se cortan. Planifica rotaciones en ventanas de bajo tráfico o firma con expires_in corto para minimizar los cortes durante la rotación.

Lo que Token Auth NO hace

  • No es un Web Application Firewall: no filtra bots, no limita el rate, no bloquea por país. Para eso usa las reglas WAF del CDN.
  • No autentica usuarios: autoriza una URL contra un secret compartido. Cualquiera con la URL firmada puede usarla (a menos que tengas IP binding).
  • No cifra el contenido: el tráfico es HTTPS, pero el contenido en sí son los mismos bytes que tienes en tu origin. No uses Token Auth como sustituto de cifrado en reposo de datos verdaderamente sensibles.

Resolución de problemas

SíntomaCausa probable
403 en todas las URLsToken Auth está activo pero pides sin ?token= o con un token calculado con el secret equivocado
403 solo en algunas URLsMismatch entre el path que firmaste y el path que pide el cliente (trailing slash, mayúsculas, percent-encoding) — firma y pide exactamente el mismo string
403 tras rotarURLs cacheadas de antes de rotar — vuelve a firmar con el nuevo secret
Funciona en navegador, falla en móvilIP binding activo y el cliente cambió de red — desactiva IP binding o baja el TTL
Los clientes ven 403 aleatorios a mitad de reproducciónURL expiró a media reproducción — sube expires_in, o haz que el player pida una URL nueva cerca de la expiración

Referencia de API

PATCH /cdn/zones/{uuid}      { "token_auth_enabled": true|false,
                               "token_auth_ip_binding": true|false }
POST  /cdn/zones/{uuid}/token-auth/rotate-secret  → devuelve el nuevo secret
POST  /cdn/zones/{uuid}/token-auth/sign-url       { "path": "...", "expires_in": N,
                                                    "client_ip": "..."? }

El secret se devuelve exactamente una vez — al activarlo por primera vez, y en cada rotación.