Przewodnik po podpisach HMAC
Ten przewodnik po podpisach HMAC przeprowadzi Cię przez tworzenie podpisów HMAC-SHA256 dla FastOG, dzięki czemu będziesz mógł generować dynamiczne obrazy Open Graph na bieżąco — w modelu subskrypcyjnym, z darmowym kontem i planem Dynamic API (płatne rendery są czyste). Podpis możesz utworzyć ręcznie w dowolnym języku tutaj, albo najpierw użyć darmowego testera obrazów OG, aby zobaczyć format URL w praktyce.
Przegląd
Wszystkie żądania do /api/v1/og muszą zawierać podpis HMAC-SHA256, aby potwierdzić, że pochodzą z legalnej strony internetowej. Zapobiega to nieautoryzowanym podmiotom korzystającym z Twojego limitu API.
Dwie warstwy uwierzytelniania:
- Klucz API (w zapytaniu:
key=sk_quick_123abc) — identyfikuje konto użytkownika (do rozliczania limitu renderowania) - Podpis HMAC (w zapytaniu:
s=<signature>) — potwierdza, że żądanie pochodzi z Twojej strony internetowej
Oba muszą zostać zweryfikowane. Sam klucz API NIE jest wystarczający.
Dlaczego parametry zapytania? Obrazy OG są pobierane przez roboty sieci społecznościowych (LinkedIn, Twitter, Facebook), które nie wysyłają niestandardowych nagłówków. Podpis MUSI znajdować się w adresie URL.
Generowanie podpisu
Algorytm
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>
Wymagania
- Format podpisu: 64-znakowy ciąg szesnastkowy (SHA256)
- Użyj
hash_equals()do porównania w stałym czasie (zapobiega atakom typu timing) - Sekrety per użytkownik: Każdy klucz API ma własne pole
hmac_secret
Przykładowe implementacje
Pełne samouczki z wieloma frameworkami i wzorcami znajdziesz tutaj:
- Samouczek JavaScript/Node.js — Next.js, Express, czysty Node.js
- Next.js App Router
- Next.js Pages Router
- React
- SvelteKit
- Vue/Nuxt
- Astro
- Express
- Czysty Node.js
- 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 }}" />
Kompletny przykład: szablon 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>
Najlepsze praktyki bezpieczeństwa
- Bezpieczne przechowywanie sekretów – używaj zmiennych środowiskowych lub menedżera sekretów
- Nigdy nie ujawniaj sekretów w kodzie po stronie klienta – podpisuj URL-e wyłącznie po stronie serwera
- Używaj protokołu HTTPS – zapobiega atakom typu MITM
- Okresowo rotuj sekrety – zwłaszcza jeśli doszło do ich kompromitacji
- Monitoruj nieudane podpisy – powtarzające się błędy mogą świadczyć o atakach
- Izolacja per użytkownik – każdy klucz API ma własny sekret HMAC
Odpowiedzi błędów
Brakujący podpis
{
"error": "Missing signature parameters",
"code": "missing_signature",
"required_params": ["s", "key"],
"example": "/api/v1/og?title=Hello&key=sk_quick_123abc&s=<signature>"
}
Nieprawidłowy podpis
{
"error": "Invalid signature",
"code": "invalid_signature"
}
Testowanie
Wygeneruj adres URL testowy:
# 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>"
Oczekiwane odpowiedzi:
- ✅ Prawidłowy podpis → Zwraca obraz OG (lub błąd walidacji, jeśli brakuje parametrów szablonu)
- ❌ Nieprawidłowy podpis →
{"error": "...", "code": "invalid_signature"} - ❌ Brakujące parametry →
{"error": "...", "code": "missing_signature"}
Rozwiązywanie problemów
Podpis nie jest zgodny
- Upewnij się, że używasz prawidłowego sekretu HMAC (sprawdź klucz API w panelu)
- Sprawdź kanoniczne zapytanie: każdy parametr zapytania oprócz
s(w tymkey), wartości zakodowane zgodnie z rfc3986, klucze posortowane leksykograficznie - Zweryfikuj, że podpis jest zapisany małymi literami w formacie szesnastkowym (64 znaki)
Uzyskiwanie swojego sekretu HMAC
- Przejdź do Panelu → Klucze API
- Utwórz nowy klucz API (lub użyj istniejącego)
- Skopiuj sekret HMAC wyświetlany obok klucza API (pokazywany tylko raz!)
- Przechowuj go bezpiecznie w zmiennych środowiskowych
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>
Testowanie za pomocą 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}"
Oczekiwany wynik: 200 OK z danymi obrazu (lub 422, jeśli parametry szablonu są nieprawidłowe).