Temat

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:

  1. Klucz API (w zapytaniu: key=sk_quick_123abc) — identyfikuje konto użytkownika (do rozliczania limitu renderowania)
  2. 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:

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 }}" />

Kompletny przykład: szablon 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>

Najlepsze praktyki bezpieczeństwa

  1. Bezpieczne przechowywanie sekretów – używaj zmiennych środowiskowych lub menedżera sekretów
  2. Nigdy nie ujawniaj sekretów w kodzie po stronie klienta – podpisuj URL-e wyłącznie po stronie serwera
  3. Używaj protokołu HTTPS – zapobiega atakom typu MITM
  4. Okresowo rotuj sekrety – zwłaszcza jeśli doszło do ich kompromitacji
  5. Monitoruj nieudane podpisy – powtarzające się błędy mogą świadczyć o atakach
  6. Izolacja per użytkownik – każdy klucz API ma własny sekret HMAC

Odpowiedzi błędów

Brakujący podpis

json
{
    "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

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

Testowanie

Wygeneruj adres URL testowy:

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>"

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

  1. Upewnij się, że używasz prawidłowego sekretu HMAC (sprawdź klucz API w panelu)
  2. Sprawdź kanoniczne zapytanie: każdy parametr zapytania oprócz s (w tym key), wartości zakodowane zgodnie z rfc3986, klucze posortowane leksykograficznie
  3. Zweryfikuj, że podpis jest zapisany małymi literami w formacie szesnastkowym (64 znaki)

Uzyskiwanie swojego sekretu HMAC

  1. Przejdź do Panelu → Klucze API
  2. Utwórz nowy klucz API (lub użyj istniejącego)
  3. Skopiuj sekret HMAC wyświetlany obok klucza API (pokazywany tylko raz!)
  4. Przechowuj go bezpiecznie w zmiennych środowiskowych
bash
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>

Testowanie za pomocą 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}"

Oczekiwany wynik: 200 OK z danymi obrazu (lub 422, jeśli parametry szablonu są nieprawidłowe).

Pełna dokumentacja API

Wypróbuj za darmo — bez rejestracji

Podglądaj i testuj dynamiczne obrazy OG w kilka sekund.

Otwórz darmowy tester obrazów OG

A BeCaSimulations site