Guide des signatures HMAC
Ce guide des signatures HMAC vous explique comment construire des signatures HMAC-SHA256 pour FastOG, afin de générer dynamiquement des images Open Graph à la volée — sur abonnement, avec un compte gratuit et un plan Dynamic API (les rendus payants sont propres). Vous pouvez créer la signature manuellement dans n'importe quel langage ici, ou utiliser le Testeur d'images OG gratuit pour voir d'abord le format d'URL en action.
Vue d'ensemble
Toutes les requêtes vers /api/v1/og doivent inclure une signature HMAC-SHA256 pour prouver qu'elles proviennent du site web légitime. Cela empêche des tiers non autorisés d'utiliser votre quota API.
Deux niveaux d'authentification :
- Clé API (dans la requête :
key=sk_quick_123abc) — identifie le compte utilisateur (pour la comptabilisation du quota de rendu) - Signature HMAC (dans la requête :
s=<signature>) — prouve que la requête provient de votre site web
Les deux doivent être valides. La clé API seule ne suffit PAS.
Pourquoi des paramètres de requête ? Les images OG sont récupérées par les robots des réseaux sociaux (LinkedIn, Twitter, Facebook) qui n'envoient pas d'en-têtes personnalisés. La signature DOIT être dans l'URL.
Génération de la signature
Algorithme
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>
Exigences
- Format de la signature : chaîne hexadécimale de 64 caractères (SHA256)
- Utilisez
hash_equals()pour une comparaison en temps constant (prévient les attaques temporelles) - Secrets par utilisateur : chaque clé API possède son propre champ
hmac_secret
Exemples d’implémentations
Pour des tutoriels complets couvrant plusieurs frameworks et modèles, consultez :
- Tutoriel 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 }}" />
Exemple complet : modèle Blade Laravel
{{-- 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>
Bonnes pratiques de sécurité
- Stockez les secrets de manière sécurisée - Utilisez des variables d’environnement ou un gestionnaire de secrets
- N’exposez jamais les secrets dans le code côté client - Signez les URL uniquement côté serveur
- Utilisez HTTPS - Prévient les attaques de type MITM
- Faites pivoter les secrets périodiquement - Surtout en cas de compromission
- Surveillez les signatures échouées - Des échecs répétés peuvent indiquer des attaques
- Isolation par utilisateur - Chaque clé API possède son propre secret HMAC
Réponses d’erreur
Signature manquante
{
"error": "Missing signature parameters",
"code": "missing_signature",
"required_params": ["s", "key"],
"example": "/api/v1/og?title=Hello&key=sk_quick_123abc&s=<signature>"
}
Signature invalide
{
"error": "Invalid signature",
"code": "invalid_signature"
}
Test
Générez une URL de 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>"
Réponses attendues :
- ✅ Signature valide → Renvoie l’image OG (ou une erreur de validation si les paramètres du modèle sont manquants)
- ❌ Signature invalide →
{"error": "...", "code": "invalid_signature"} - ❌ Paramètres manquants →
{"error": "...", "code": "missing_signature"}
Dépannage
La signature ne correspond pas
- Vérifiez que vous utilisez le bon secret HMAC (consultez la clé API dans le tableau de bord)
- Vérifiez la requête canonique : chaque paramètre de requête sauf
s(y compriskey), valeurs encodées en rfc3986, clés triées par ordre lexicographique - Vérifiez que la signature est en hexadécimal minuscule (64 caractères)
Obtention de votre secret HMAC
- Allez dans Tableau de bord → Clés API
- Créez une nouvelle clé API (ou utilisez une clé existante)
- Copiez le secret HMAC affiché à côté de la clé API (affiché une seule fois !)
- Stockez-le de manière sécurisée dans vos variables d’environnement
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>
Test avec 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}"
Résultat attendu : 200 OK avec les données d’image (ou 422 si les paramètres du modèle sont invalides).