HMAC Validation

HMAC validation is how FastOG verifies that every credit-based OG image request came from your own server — signed URLs keep your credits safe. Sign up free for 100 starter credits (create your API key and HMAC secret in the dashboard); free renders carry a FastOG watermark until your first payment. This page covers the server-side rules behind dynamic Open Graph images, from the 5-minute timestamp window to constant-time comparison.

Overview

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

Architecture

Two-layer authentication:
  • HMAC Signature (first) - Validates the request originated from a trusted source (the website)
  • API Key (second) - Identifies the user account for credit deduction
  • Middleware order matters: HmacSignature runs BEFORE ApiKeyAuth to fail fast on invalid 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>&t=<timestamp>

    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

    timestamp = current Unix timestamp (seconds)
    method = HTTP method (e.g., "GET")
    path = request path (e.g., "/api/v1/og") - NO query string
    apiKey = the full API key (e.g., "sk_quick_123abc")
    
    message = method + path + timestamp + apiKey
    signature = HMAC-SHA256(message, hmac_secret)
    
    Query parameters:
      key=<apiKey>
      s=<signature>
      t=<timestamp>

    Security Features

    • 5-minute timestamp window - Prevents replay attacks
    • Constant-time comparison (hash_equals()) - Prevents timing attacks
    • API key in message - Prevents signature reuse across different API keys
    • 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

    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");
    
    function generateOgImageUrl(baseUrl, params, apiKey, hmacSecret) {
        const timestamp = Math.floor(Date.now() / 1000);
        const url = new URL(baseUrl);
    
        // Add query params
        url.searchParams.set("title", params.title);
        url.searchParams.set("key", apiKey);
        url.searchParams.set("t", timestamp);
    
        // Compute signature: method + path + timestamp + apiKey
        const path = url.pathname;
        const message = `GET${path}${timestamp}${apiKey}`;
        const signature = crypto
            .createHmac("sha256", hmacSecret)
            .update(message)
            .digest("hex");
    
        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" },
        "sk_quick_123abc",
        process.env.HMAC_SECRET,
    );
    
    // <meta property="og:image" content="{ogUrl}" />

    const message = method + path + timestamp + apiKey;

    const signature = crypto.createHmac('sha256', hmacSecret)

    .update(message)

    .digest('hex');

    return {

    key: apiKey,

    s: signature,

    t: timestamp.toString(),

    };

    }

    // Usage in meta tag:

    const params = signRequest('GET', '/api/v1/og', 'sk_quick_123abc', process.env.HMAC_SECRET);

    const ogUrl = https://fastog.com/api/v1/og?title=Hello&key=${params.key}&s=${params.s}&t=${params.t};

    //

    
    ## Error Responses
    
    | Status | Code | Description |
    |--------|------|-------------|
    | 401 | `missing_signature` | Missing `s`, `t`, or `key` query parameter |
    | 401 | `invalid_signature_format` | Signature not 64 hex characters |
    | 401 | `expired_timestamp` | Timestamp > 5 minutes old |
    | 401 | `invalid_signature` | Signature doesn't match expected value |
    | 401 | `missing_api_key` | Missing API key |
    
    ## 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 - ✅ Request with expired timestamp is rejected - ✅ Request with invalid signature is rejected - ✅ Request with valid signature passes HMAC check - ✅ Signature is path-specific - ✅ Signature is method-specific - ✅ Different API keys have different secrets <h2>Migration Notes</h2> <h3>For Existing API Clients</h3> If you have existing API clients (other than the website), they need to be updated to: 1. Generate HMAC signatures 2. Include key, s, and t query parameters <strong>OR</strong> create a separate endpoint without HMAC for trusted internal services. <h3>Rollback</h3> To disable HMAC validation temporarily: 1. Remove HmacSignature::class from routes/api.php`

  • Keep the middleware file for future re-enablement
  • 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

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

    Full API Reference

    Related

    Try it free — no signup required

    Preview and test dynamic OG images in seconds.

    Open the Free OG Image Tester