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:
- HMAC Signature (first) - Validates the request originated from a trusted source (the website)
- 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 routebootstrap/app.php- Registeredhmacmiddleware aliasapp/Models/ApiKey.php- Addedhmac_secretfield (per-user secrets)app/Http/Controllers/DashboardController.php- Generates HMAC secret on API key creationdatabase/migrations/2026_08_03_214931_add_hmac_secret_to_api_keys_table.php- Migration for hmac_secret columnresources/js/Pages/Dashboard/ApiKeys.svelte- Shows HMAC secret to user (once)docs/hmac-signature-guide.md- Client implementation guidetests/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):
openssl rand -hex 32
Database Schema
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)
// 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:
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
- 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
- HMAC Signature Guide for Clients
- Quick Start Guide
- OWASP: Cryptographic Storage Cheat Sheet
- PHP: hash_hmac()
Full API Reference
Related
Try it free — no signup required
Preview and test dynamic OG images in seconds.
Open the Free OG Image Tester