Tema

Guía de firmas HMAC

Esta guía de firmas HMAC te explica cómo construir firmas HMAC-SHA256 para FastOG, de modo que puedas generar imágenes Open Graph dinámicas sobre la marcha — con suscripción, cuenta gratuita y un plan de Dynamic API (los renders de pago salen limpios). Puedes implementar la firma manualmente en cualquier lenguaje aquí, o usar el Probador de Imágenes OG Gratuito para ver primero el formato de URL en acción.

Resumen

Todas las solicitudes a /api/v1/og deben incluir una firma HMAC-SHA256 para demostrar que se originaron desde el sitio web legítimo. Esto evita que terceros no autorizados utilicen tu cuota de API.

Dos capas de autenticación:

  1. Clave de API (en la consulta: key=sk_quick_123abc) — identifica la cuenta de usuario (para el control de la cuota de renderizado)
  2. Firma HMAC (en la consulta: s=<signature>) — demuestra que la solicitud proviene de tu sitio web

Ambas deben ser válidas. La clave de API por sí sola NO es suficiente.

¿Por qué parámetros de consulta? Las imágenes OG son obtenidas por los rastreadores de redes sociales (LinkedIn, Twitter, Facebook) que no envían cabeceras personalizadas. La firma DEBE estar en la URL.

Generación de la firma

Algoritmo

method = "GET"
path = "/api/v1/og"
canonicalQuery = for every query param EXCEPT `s` (including `key`):
  - array values are sorted too
  - each pair: rfc3986Encode(key)=rfc3986Encode(value)
  - pairs sorted lexicographically and joined with "&"

message = method + path + "\n" + canonicalQuery
signature = hex(HMAC-SHA256(message, hmac_secret))

Query parameters:
  key=<apiKey>
  s=<signature>

Requisitos

  • Formato de la firma: cadena hexadecimal de 64 caracteres (SHA256)
  • Usa hash_equals() para la comparación en tiempo constante (previene ataques de temporización)
  • Secretos por usuario: cada clave de API tiene su propio campo hmac_secret

Implementaciones de ejemplo

Para tutoriales completos con múltiples frameworks y patrones, consulta:

JavaScript (Node.js / Frontend)

javascript
const crypto = require("crypto");

// rfc3986-encode: everything except A-Za-z0-9-._~
const enc = (s) =>
    encodeURIComponent(s).replace(
        /[!'()*]/g,
        (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
    );

function generateOgImageUrl(baseUrl, params, apiKey, hmacSecret) {
    // every query param except `s`; key included; arrays expand to one pair each
    const all = { ...params, key: apiKey };
    const pairs = [];
    for (const [k, v] of Object.entries(all)) {
        for (const vv of Array.isArray(v) ? [...v].sort() : [v]) {
            pairs.push(`${enc(k)}=${enc(vv)}`);
        }
    }
    pairs.sort();
    const canonical = pairs.join("&");

    const signature = crypto
        .createHmac("sha256", hmacSecret)
        .update(`GET/api/v1/og\n${canonical}`)
        .digest("hex");

    const url = new URL(baseUrl);
    for (const [k, v] of Object.entries(all)) {
        const isArray = Array.isArray(v);
        for (const vv of isArray ? v : [v]) {
            url.searchParams.append(isArray ? `${k}[]` : k, vv);
        }
    }
    url.searchParams.set("s", signature);
    return url.toString();
}

// Usage in meta tag:
const ogUrl = generateOgImageUrl(
    "https://fastog.com/api/v1/og",
    { title: "Como criar um SaaS", template: "default" },
    "sk_quick_123abc",
    process.env.HMAC_SECRET,
);

// <meta property="og:image" content="{ogUrl}" />

PHP (Laravel)

php
function generateOgImageUrl(string $baseUrl, array $params, string $apiKey, string $hmacSecret): string
{
    // every query param except `s`; key included; array values sorted too
    $all = array_merge($params, ['key' => $apiKey]);

    $pairs = [];
    foreach ($all as $k => $v) {
        $values = is_array($v) ? $v : [$v];
        sort($values, SORT_STRING);
        foreach ($values as $vv) {
            $pairs[] = rawurlencode((string)$k) . '=' . rawurlencode((string)$vv);
        }
    }
    sort($pairs, SORT_STRING);
    $canonical = implode('&', $pairs);

    $signature = hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret);

    // URL uses bracket keys for arrays (features[]=a&features[]=b); canonical signs plain keys
    $urlPairs = [];
    foreach ($all as $k => $v) {
        $isArray = is_array($v);
        $values = $isArray ? $v : [$v];
        $urlKey = $isArray ? $k . '[]' : $k;
        foreach ($values as $vv) {
            $urlPairs[] = rawurlencode($urlKey) . '=' . rawurlencode((string)$vv);
        }
    }

    return $baseUrl . '?' . implode('&', $urlPairs) . '&s=' . $signature;
}

// Usage in Blade template:
$ogUrl = generateOgImageUrl(
    'https://fastog.com/api/v1/og',
    ['title' => 'Como criar um SaaS'],
    'sk_quick_123abc',
    env('HMAC_SECRET')
);

// <meta property="og:image" content="{{ $ogUrl }}" />

Python

python
import hmac
import hashlib
from urllib.parse import quote

def _enc(s):
    return quote(str(s), safe='-._~')

def generate_og_image_url(base_url, params, api_key, hmac_secret):
    # every query param except `s`; key included; array values sorted too
    all_params = {**params, 'key': api_key}

    pairs = []
    for k, v in all_params.items():
        for vv in sorted(v if isinstance(v, list) else [v]):
            pairs.append(f"{_enc(k)}={_enc(vv)}")
    canonical = '&'.join(sorted(pairs))

    signature = hmac.new(
        hmac_secret.encode(),
        f"GET/api/v1/og\n{canonical}".encode(),
        hashlib.sha256
    ).hexdigest()

    # URL uses bracket keys for arrays (features[]=a&features[]=b); canonical signs plain keys
    url_pairs = []
    for k, v in all_params.items():
        if isinstance(v, list):
            for vv in v:
                url_pairs.append(f"{_enc(f'{k}[]')}={_enc(vv)}")
        else:
            url_pairs.append(f"{_enc(k)}={_enc(v)}")

    return f"{base_url}?{'&'.join(url_pairs)}&s={signature}"

# Usage:
og_url = generate_og_image_url(
    'https://fastog.com/api/v1/og',
    {'title': 'Como criar um SaaS', 'template': 'default'},
    'sk_quick_123abc',
    os.environ['HMAC_SECRET']
)

# <meta property="og:image" content="{{ og_url }}" />

Ejemplo completo: plantilla de Laravel Blade

blade
{{-- In your blog post view --}}
@php
    $apiKey = 'sk_quick_123abc'; // Your full API key
    $hmacSecret = env('OG_RENDER_HMAC_SECRET');

    $params = [
        'template' => 'blog',
        'title' => $post->title,
        'subtitle' => $post->excerpt,
        'key' => $apiKey,
    ];

    // canonical query: sorted "k=v" pairs, rfc3986-encoded, every param except `s`
    $pairs = [];
    foreach ($params as $k => $v) {
        $pairs[] = rawurlencode((string)$k) . '=' . rawurlencode((string)$v);
    }
    sort($pairs, SORT_STRING);
    $canonical = implode('&', $pairs);
    $signature = hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret);

    $ogImageUrl = 'https://fastog.com/api/v1/og?' . $canonical . '&s=' . $signature;
@endphp

<head>
    <meta property="og:image" content="{{ $ogImageUrl }}" />
    <meta property="og:title" content="{{ $post->title }}" />
    <meta property="og:description" content="{{ $post->excerpt }}" />
</head>

Prácticas recomendadas de seguridad

  1. Almacena los secretos de forma segura - Usa variables de entorno o un gestor de secretos
  2. Nunca expongas los secretos en código de cliente - Firma las URLs solo en el servidor
  3. Usa HTTPS - Evita ataques de intermediario (MITM)
  4. Rota los secretos periódicamente - Especialmente si se han visto comprometidos
  5. Supervisa las firmas fallidas - Los fallos repetidos pueden indicar ataques
  6. Aislamiento por usuario - Cada clave de API tiene su propio secreto HMAC

Respuestas de error

Firma ausente

json
{
    "error": "Missing signature parameters",
    "code": "missing_signature",
    "required_params": ["s", "key"],
    "example": "/api/v1/og?title=Hello&key=sk_quick_123abc&s=<signature>"
}

Firma no válida

json
{
    "error": "Invalid signature",
    "code": "invalid_signature"
}

Pruebas

Genera una URL de prueba:

bash
# Node.js
node -e "
const crypto = require('crypto');
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
const all = { title: 'Test', key: 'sk_quick_123abc' };
const pairs = [];
for (const [k, v] of Object.entries(all)) for (const vv of Array.isArray(v) ? v : [v]) pairs.push(enc(k) + '=' + enc(vv));
pairs.sort();
const canonical = pairs.join('&');
const sig = crypto.createHmac('sha256', 'YOUR_HMAC_SECRET').update('GET/api/v1/og\n' + canonical).digest('hex');
console.log('https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=' + sig);
"

# Test with curl
curl "https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=<signature>"

Respuestas esperadas:

  • ✅ Firma válida → Devuelve la imagen OG (o error de validación si faltan parámetros de la plantilla)
  • ❌ Firma inválida → {"error": "...", "code": "invalid_signature"}
  • ❌ Parámetros faltantes → {"error": "...", "code": "missing_signature"}

Solución de problemas

La firma no coincide

  1. Verifica que estás usando el secreto HMAC correcto (revisa la clave de API en el panel de control)
  2. Comprueba la consulta canónica: todos los parámetros de consulta excepto s (incluido key), valores codificados según rfc3986, claves ordenadas lexicográficamente
  3. Verifica que la firma sea hexadecimal en minúsculas (64 caracteres)

Cómo obtener tu secreto HMAC

  1. Ve a Panel de control → Claves de API
  2. Crea una nueva clave de API (o usa una existente)
  3. Copia el secreto HMAC que se muestra junto a la clave de API (¡solo se muestra una vez!)
  4. Guárdalo de forma segura en tus variables de entorno
bash
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>

Pruebas con curl

bash
# Bash script to generate signed URL
SECRET="your-hmac-secret-here"
API_KEY="sk_quick_123abc"  # Your full API key

# canonical query: sorted "k=v" pairs, every param except `s` (key included)
CANONICAL="key=${API_KEY}&template=default&title=Test"
MESSAGE=$(printf 'GET/api/v1/og\n%s' "$CANONICAL")
SIGNATURE=$(printf '%s' "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)

curl "http://localhost:8000/api/v1/og?template=default&title=Test&key=${API_KEY}&s=${SIGNATURE}"

Resultado esperado: 200 OK con datos de imagen (o 422 si los parámetros de la plantilla no son válidos).

Referencia completa de la API

Pruébalo gratis: no se requiere registro

Previsualiza y prueba imágenes OG dinámicas en segundos.

Abrir el probador gratuito de imágenes OG

A BeCaSimulations site