Guida alla firma HMAC
Questa guida alla firma HMAC ti accompagna nella creazione di firme HMAC-SHA256 per FastOG, così puoi generare immagini Open Graph dinamiche al volo — con abbonamento, account gratuito e piano Dynamic API (i render a pagamento sono puliti). Puoi implementare la firma manualmente in qualsiasi linguaggio qui, oppure usare il Tester gratuito di immagini OG per vedere prima il formato dell'URL in azione.
Panoramica
Tutte le richieste a /api/v1/og devono includere una firma HMAC-SHA256 per dimostrare che provengono dal sito web legittimo. Questo impedisce a soggetti non autorizzati di utilizzare la tua quota API.
Due livelli di autenticazione:
- Chiave API (nella query:
key=sk_quick_123abc) — identifica l'account utente (per la contabilità della quota di rendering) - Firma HMAC (nella query:
s=<signature>) — dimostra che la richiesta proviene dal tuo sito web
Entrambi devono essere validi. La sola chiave API NON è sufficiente.
Perché parametri di query? Le immagini OG vengono recuperate dai crawler dei social network (LinkedIn, Twitter, Facebook) che non inviano header personalizzati. La firma DEVE essere nell'URL.
Generazione della 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>
Requisiti
- Formato della firma: stringa esadecimale di 64 caratteri (SHA256)
- Usa
hash_equals()per il confronto a tempo costante (previene gli attacchi di temporizzazione) - Segreti per utente: ogni chiave API ha il proprio campo
hmac_secret
Implementazioni di esempio
Per tutorial completi con più framework e pattern, consulta:
- Tutorial JavaScript/Node.js - Next.js, Express, Node.js vanilla
- Next.js App Router
- Next.js Pages Router
- React
- SvelteKit
- Vue/Nuxt
- Astro
- Express
- Node.js vanilla
- 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 }}" />
Esempio Completo: Template 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>
Best practice di sicurezza
- Conserva le chiavi segrete in modo sicuro - Usa variabili d'ambiente o un gestore di segreti
- Non esporre mai le chiavi segrete nel codice lato client - Firma gli URL solo sul server
- Usa HTTPS - Previene gli attacchi MITM
- Ruota le chiavi segrete periodicamente - Soprattutto se compromesse
- Monitora le firme fallite - Fallimenti ripetuti possono indicare attacchi
- Isolamento per utente - Ogni chiave API ha il proprio segreto HMAC
Risposte di errore
Firma mancante
{
"error": "Missing signature parameters",
"code": "missing_signature",
"required_params": ["s", "key"],
"example": "/api/v1/og?title=Hello&key=sk_quick_123abc&s=<signature>"
}
Firma non valida
{
"error": "Invalid signature",
"code": "invalid_signature"
}
Test
Genera un URL di test:
# 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>"
Risposte attese:
- ✅ Firma valida → Restituisce l'immagine OG (o un errore di validazione se mancano i parametri del template)
- ❌ Firma non valida →
{"error": "...", "code": "invalid_signature"} - ❌ Parametri mancanti →
{"error": "...", "code": "missing_signature"}
Risoluzione dei problemi
La firma non corrisponde
- Verifica di utilizzare il segreto HMAC corretto (controlla la chiave API nella dashboard)
- Controlla la query canonica: ogni parametro della query tranne
s(inclusokey), valori codificati secondo rfc3986, chiavi ordinate lessicograficamente - Verifica che la firma sia in esadecimale minuscolo (64 caratteri)
Ottenere il tuo segreto HMAC
- Vai su Dashboard → API Keys
- Crea una nuova chiave API (oppure usane una esistente)
- Copia il segreto HMAC mostrato accanto alla chiave API (viene mostrato solo una volta!)
- Salvalo in modo sicuro nelle variabili d'ambiente
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>
Test 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}"
Risultato atteso: 200 OK con i dati dell'immagine (oppure 422 se i parametri del template non sono validi).