HMAC Validation

HMAC validation is how FastOG verifies that every subscription-based OG image request came from your own server — signed URLs keep your credits safe. Sign up, then choose a Dynamic API plan and create your API key and HMAC secret in the dashboard; paid API renders are clean. This page covers the server-side rules behind dynamic Open Graph images, from canonical query signing to constant-time comparison.

Overview

HMAC-SHA256 signature validation has been added to the /api/v1/og endpoint to prevent unauthorized use of API quota. This ensures only requests from the legitimate website can generate OG images.

Architecture

Two-layer authentication:

  1. HMAC Signature (first) - Validates the request originated from a trusted source (the website)
  2. API Key (second) - Identifies the user account for render-quota accounting

Single middleware (ApiKeyAuth): verifies the signature (query params) and resolves the API key for render-quota accounting in one pass — fail fast on bad signatures.

Query parameters, not headers: Since OG images are fetched by social network crawlers (LinkedIn, Twitter, Facebook) that don't send custom headers, the signature is passed in URL query parameters:

GET /api/v1/og?title=Hello&key=sk_quick_123abc&s=<hmac>

Implementation

Files Created/Modified

  • app/Http/Middleware/HmacSignature.php - New middleware validating HMAC signatures (query params)
  • routes/api.php - Added HMAC middleware to OG route
  • bootstrap/app.php - Registered hmac middleware alias
  • app/Models/ApiKey.php - Added hmac_secret field (per-user secrets)
  • app/Http/Controllers/DashboardController.php - Generates HMAC secret on API key creation
  • database/migrations/2026_08_03_214931_add_hmac_secret_to_api_keys_table.php - Migration for hmac_secret column
  • resources/js/Pages/Dashboard/ApiKeys.svelte - Shows HMAC secret to user (once)
  • docs/hmac-signature-guide.md - Client implementation guide
  • tests/Feature/HmacSignatureTest.php - 8 passing tests

Signature Algorithm

method = HTTP method (e.g., "GET")
path  = request path (e.g., "/api/v1/og")
query = the request's DECODED query params

canonical = every param except `s`, keys sorted (array values sorted too),
            each `key=value` RFC-3986 encoded (unreserved-only), joined with "&"
message   = method + "/" + path + "\n" + canonical
signature = HMAC-SHA256(message, hmac_secret)

Query parameters:
  key=<apiKey>
  s=<signature>
  (no timestamp)

key is part of the canonical query, so the signature is bound to the API key. Changing any param (title, template, image, …) or the key itself changes the canonical → signature mismatch → 401 → no render, no quota count. Identical params → identical signature → identical URL → cacheable at the edge forever, replayed without counting.

Security Features

  • Content-addressed signature - Any param change → 401 → no render, no quota count (the URL itself is the proof)
  • No timestamp - URLs are embedded in public HTML + cached forever; they must outlive 5 minutes
  • Constant-time comparison (hash_equals()) - Prevents timing attacks
  • API key in message - Signature is bound to the key; swapping keys fails
  • Path-specific - Signature valid only for exact path
  • Method-specific - GET signature won't work for POST
  • Per-user secrets - Each API key has its own HMAC secret, can be revoked independently (rotate = kill switch for leaked URLs)

Configuration

Generate Secret (Automatic)

The HMAC secret is automatically generated when creating an API key in the dashboard. It's shown once alongside the API key, then never shown again.

Manual generation (if needed):

bash
openssl rand -hex 32

Database Schema

sql
ALTER TABLE api_keys ADD COLUMN hmac_secret VARCHAR(64)
  COMMENT 'HMAC-SHA256 secret for signing OG image URLs (hex, 64 chars)';

Environment Variables

No global environment variable needed - secrets are stored per-user in the api_keys table.

Usage Example (Website Backend)

javascript
// Node.js example - generate signed OG image URL
const crypto = require("crypto");

// RFC-3986 encode (encodeURIComponent leaves !'()* unencoded).
const enc = (s) =>
    encodeURIComponent(s).replace(
        /[!'()*]/g,
        (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
    );

function generateOgImageUrl(baseUrl, params, apiKey, hmacSecret) {
    const all = { ...params, key: apiKey };

    // Canonical: every param (except s), sorted, RFC-3986 encoded, joined with "&".
    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 message = `GET/api/v1/og\n${canonical}`;
    const signature = crypto
        .createHmac("sha256", hmacSecret)
        .update(message)
        .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: "blog" },
    "sk_quick_123abc",
    process.env.HMAC_SECRET,
);

// <meta property="og:image" content="{ogUrl}" />

Error Responses

Status Code Description
401 missing_signature Missing key or s query parameter
401 invalid_signature Signature doesn't match the canonical query (tampered or wrong secret)

Testing

Run tests:

bash
php artisan test --filter HmacSignatureTest

All 8 tests pass:

  • ✅ Request without signature is rejected
  • ✅ Request with missing key param is rejected
  • ✅ Tampered content param (title changed) is rejected
  • ✅ Added param is rejected
  • ✅ Invalid signature is rejected
  • ✅ Valid signature passes HMAC check
  • ✅ Signature is path-specific
  • ✅ Signature is method-specific
  • ✅ Different API keys have different secrets

Performance Impact

  • Negligible - HMAC-SHA256 computation is ~0.1ms per request
  • No additional network calls - Validation is local
  • Fails fast - Invalid signatures rejected before API key lookup

Security Considerations

  1. Never expose the secret in client-side code - Sign on the server only
  2. Use HTTPS - HMAC protects against unauthorized requests, HTTPS protects against MITM
  3. Rotate secrets periodically - Especially if compromised (requires regenerating API key)
  4. Monitor failed signatures - Repeated failures may indicate attacks
  5. Per-user isolation - Each API key has its own HMAC secret, compromise of one doesn't affect others

References

Full API Reference

Try it free — no signup required

Preview and test dynamic OG images in seconds.

Open the Free OG Image Tester