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:
- Clave de API (en la consulta:
key=sk_quick_123abc) — identifica la cuenta de usuario (para el control de la cuota de renderizado) - 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:
- Tutorial de JavaScript/Node.js - Next.js, Express, Node.js puro
- Next.js App Router
- Next.js Pages Router
- React
- SvelteKit
- Vue/Nuxt
- Astro
- Express
- Node.js puro
- PHP/Laravel
- Python
- Ruby/Rails
JavaScript (Node.js / Frontend)
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)
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
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
{{-- 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
- Almacena los secretos de forma segura - Usa variables de entorno o un gestor de secretos
- Nunca expongas los secretos en código de cliente - Firma las URLs solo en el servidor
- Usa HTTPS - Evita ataques de intermediario (MITM)
- Rota los secretos periódicamente - Especialmente si se han visto comprometidos
- Supervisa las firmas fallidas - Los fallos repetidos pueden indicar ataques
- Aislamiento por usuario - Cada clave de API tiene su propio secreto HMAC
Respuestas de error
Firma ausente
{
"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
{
"error": "Invalid signature",
"code": "invalid_signature"
}
Pruebas
Genera una URL de prueba:
# 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
- Verifica que estás usando el secreto HMAC correcto (revisa la clave de API en el panel de control)
- Comprueba la consulta canónica: todos los parámetros de consulta excepto
s(incluidokey), valores codificados según rfc3986, claves ordenadas lexicográficamente - Verifica que la firma sea hexadecimal en minúsculas (64 caracteres)
Cómo obtener tu secreto HMAC
- Ve a Panel de control → Claves de API
- Crea una nueva clave de API (o usa una existente)
- Copia el secreto HMAC que se muestra junto a la clave de API (¡solo se muestra una vez!)
- Guárdalo de forma segura en tus variables de entorno
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>
Pruebas con curl
# 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).