HMAC検証
HMAC検証は、FastOGがサブスクリプションベースのOG画像リクエストがすべて自社サーバーから送信されたものであることを確認する仕組みです。署名付きURLによりクレジットを保護します。サインアップ後、Dynamic APIプランを選択し、ダッシュボードでAPIキーとHMACシークレットを作成してください。有料APIレンダリングはクリーンです。このページでは、動的Open Graph画像のサーバーサイドのルールについて、正規クエリ署名から定数時間比較までを説明します。
概要
/api/v1/og エンドポイントに HMAC-SHA256 署名検証を追加し、API クォータの不正使用を防止します。これにより、正当なウェブサイトからのリクエストのみが OG 画像を生成できるようになります。
アーキテクチャ
2層認証:
- HMAC署名(第1層)— リクエストが信頼できる送信元(Webサイト)から発信されたことを検証します
- APIキー(第2層)— レンダリングクォータの計上用にユーザーアカウントを識別します
単一ミドルウェア(ApiKeyAuth): 署名(クエリパラメータ)を検証し、レンダリングクォータの計上用にAPIキーを解決します。これを1回の処理で行い、不正な署名は即座に失敗させます。
ヘッダーではなくクエリパラメータ: OG画像はソーシャルネットワークのクローラー(LinkedIn、Twitter、Facebook)によって取得され、これらのクローラーはカスタムヘッダーを送信しないため、署名はURLのクエリパラメータで渡されます。
GET /api/v1/og?title=Hello&key=sk_quick_123abc&s=<hmac>
実装
作成・変更したファイル
app/Http/Middleware/HmacSignature.php- HMAC署名(クエリパラメータ)を検証する新しいミドルウェアroutes/api.php- OGルートにHMACミドルウェアを追加bootstrap/app.php-hmacミドルウェアのエイリアスを登録app/Models/ApiKey.php-hmac_secretフィールド(ユーザーごとのシークレット)を追加app/Http/Controllers/DashboardController.php- APIキー作成時にHMACシークレットを生成database/migrations/2026_08_03_214931_add_hmac_secret_to_api_keys_table.php- hmac_secretカラム用のマイグレーションresources/js/Pages/Dashboard/ApiKeys.svelte- HMACシークレットをユーザーに表示(一度のみ)docs/hmac-signature-guide.md- クライアント実装ガイドtests/Feature/HmacSignatureTest.php- 8件のテストが成功
署名アルゴリズム
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は正規クエリの一部であるため、署名はAPIキーに紐づいています。パラメータ(タイトル、テンプレート、画像など)またはキー自体を変更すると、正規化文字列が変わり、署名が不一致となり、401が返ってレンダリングもクォータ消費も行われません。パラメータが同一なら署名も同一となり、URLも同一になるため、エッジで無期限にキャッシュ可能で、リプレイしてもクォータは消費されません。
セキュリティ機能
- コンテンツアドレス型署名 - パラメータを変更すると401が返り、レンダリングもクォータ消費も発生しません(URL自体が証明となります)
- タイムスタンプなし - URLは公開HTMLに埋め込まれ、無期限にキャッシュされるため、5分を超えて有効である必要があります
- 定数時間比較(
hash_equals()) - タイミング攻撃を防止 - メッセージにAPIキーを含める - 署名はキーに紐づいており、キーを交換すると失敗します
- パス固有 - 署名は正確なパスに対してのみ有効
- メソッド固有 - GET用の署名はPOSTでは機能しません
- ユーザーごとのシークレット - 各APIキーに独自のHMACシークレットがあり、個別に失効させることができます(ローテーション=漏洩したURLのキルスイッチ)
設定
シークレットの生成(自動)
HMACシークレットは、ダッシュボードでAPIキーを作成する際に自動的に生成されます。APIキーと一緒に一度だけ表示され、その後は再び表示されることはありません。
手動生成(必要な場合):
openssl rand -hex 32
データベーススキーマ
ALTER TABLE api_keys ADD COLUMN hmac_secret VARCHAR(64)
COMMENT 'HMAC-SHA256 secret for signing OG image URLs (hex, 64 chars)';
環境変数
グローバルな環境変数は不要です。シークレットはapi_keysテーブルにユーザーごとに保存されます。
使用例(ウェブサイトのバックエンド)
// 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}" />
エラーレスポンス
| ステータス | コード | 説明 |
|---|---|---|
| 401 | missing_signature |
key または s クエリパラメータがありません |
| 401 | invalid_signature |
シグネチャが正規クエリと一致しません(改ざんまたはシークレットの誤り) |
テスト
テストを実行します:
php artisan test --filter HmacSignatureTest
8件のテストがすべて成功します:
- ✅ 署名なしのリクエストは拒否される
- ✅ キーパラメータが欠落したリクエストは拒否される
- ✅ 改ざんされたコンテンツパラメータ(タイトル変更)は拒否される
- ✅ 追加されたパラメータは拒否される
- ✅ 無効な署名は拒否される
- ✅ 有効な署名はHMACチェックを通過する
- ✅ 署名はパス固有である
- ✅ 署名はメソッド固有である
- ✅ APIキーが異なるとシークレットも異なる
パフォーマンスへの影響
- 無視できる程度 - HMAC-SHA256 の計算はリクエストあたり約 0.1ms です
- 追加のネットワーク呼び出しなし - 検証はローカルで行われます
- 迅速に失敗 - 無効な署名は API キーの検索前に拒否されます
セキュリティに関する考慮事項
- クライアントサイドのコードでシークレットを公開しない — 署名はサーバー側でのみ行う
- HTTPSを使用する — HMACは不正なリクエストを防ぎ、HTTPSは中間者攻撃(MITM)を防ぐ
- シークレットを定期的にローテーションする — 特に漏えいした場合(APIキーの再生成が必要)
- 署名の失敗を監視する — 繰り返しの失敗は攻撃の兆候である可能性がある
- ユーザーごとの分離 — 各APIキーには独自のHMACシークレットがあり、1つの漏えいが他に影響しない