İçeriğe geç
Can Uğurlu
Geri dön

Shopify webhook'ta HMAC doğrulaması nasıl yapılır

Shopify webhook URL’iniz internete açık. URL’i öğrenen herkes o adrese sahte sipariş POST edebilir, doğrulama yapmıyorsanız bunu gerçek sipariş sanıp işlersiniz. Tek savunma X-Shopify-Hmac-Sha256 başlığını doğrulamak.

Doğrulama şu: request body’sinin ham hâli üzerinden, app secret’ınızla HMAC-SHA256 hesaplanır, base64’e çevrilir, gelen başlıkla karşılaştırılır. Eşleşmiyorsa istek Shopify’dan gelmemiştir.

Bunu atlamanın bedeli soyut değil. Doğrulaması olmayan bir orders/create endpoint’ine sahte bir istek gönderen kişi, sisteminize gerçek gibi görünen bir sipariş sokar. O sipariş stok düşürür, fatura kesme sürecini tetikler, e-posta gönderir. Hepsi hiç var olmayan bir satış için.

Hesaplama

$calculated = base64_encode(
    hash_hmac('sha256', $rawBody, $webhookSecret, true)
);

$rawBody request’in ham gövdesi, $webhookSecret Shopify Partner panelinden alınan app secret. Dördüncü parametre true, çıktının binary olmasını sağlıyor; base64_encode’dan önce bu şart.

getContent() kullanın, all() değil

Laravel’de $request->all() JSON’u diziye çeviriyor. O diziyi tekrar json_encode ile string’e çevirseniz bile Shopify’ın gönderdiği baytlarla eşleşmeyebilir; alan sırası, boşluk karakteri ya da sayı biçimi bir tık farklıysa hash tutmaz.

Doğrusu getContent():

$rawBody = $request->getContent();
$hmacHeader = $request->header('X-Shopify-Hmac-Sha256');

$calculated = base64_encode(
    hash_hmac('sha256', $rawBody, config('services.shopify.webhook_secret'), true)
);

if (! hash_equals($calculated, (string) $hmacHeader)) {
    abort(401);
}

Body’yi değiştiren middleware imzayı bozar

Bir middleware gövdeyi okuyup yeniden yazıyorsa (loglama için pretty-print ediyor, boşluk kırpıyor, encoding değiştiriyor) hash bir daha tutmaz. HMAC baytlar üzerinden hesaplanıyor, “aynı veri” yetmiyor, aynı bayt dizisi gerekiyor.

Doğrulama middleware’ini stack’in en başına koyun. Body’ye dokunan başka bir şey ondan önce çalışmasın.

hash_equals kullanın, === değil

if ($calculated === $hmacHeader) {
    // yanlış karşılaştırma
}

=== string karşılaştırması ilk farklı karakterde duruyor. Bu, karşılaştırma süresinden hash’in doğru kısmını tahmin etmeye yarayan bir zamanlama farkı açıyor. hash_equals sabit sürede çalışıyor, süre farkı bilgi sızdırmıyor.

Middleware olarak kaydedin

Yukarıdaki mantığı tek bir middleware’e toplayın, her webhook rotası aynı kontrolden geçsin:

class VerifyShopifyWebhook
{
    public function handle(Request $request, Closure $next)
    {
        $rawBody = $request->getContent();
        $hmacHeader = $request->header('X-Shopify-Hmac-Sha256');

        $calculated = base64_encode(
            hash_hmac('sha256', $rawBody, config('services.shopify.webhook_secret'), true)
        );

        if (! $hmacHeader || ! hash_equals($calculated, $hmacHeader)) {
            abort(401);
        }

        return $next($request);
    }
}

Secret’ı config/services.php üzerinden okuyun, kod içine gömmeyin:

// config/services.php
'shopify' => [
    'webhook_secret' => env('SHOPIFY_WEBHOOK_SECRET'),
],

Yerelde test edin

Doğrulamayı gözle kontrol etmeyin, gerçek bir imza üretip gönderin:

BODY='{"id":123,"email":"test@example.com"}'
SECRET="shpss_..."

HMAC=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)

curl -X POST https://example.test/webhooks/shopify/orders-create \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Hmac-Sha256: $HMAC" \
  -d "$BODY"

$SECRET değerini bozup aynı isteği tekrar gönderin. 401 dönmüyorsa doğrulama çalışmıyor demektir, prod’a çıkmadan burada yakalayın.

Doğrulamadan sonra parse, parse’tan sonra kuyruk

Doğrulama JSON’u parse etmeden önce çalışmalı. Body bozuksa ya da imza tutmuyorsa parse’a hiç gerek yok, direkt 401 dönün:

Route::post('/webhooks/shopify/orders-create', OrdersCreateController::class)
    ->middleware(VerifyShopifyWebhook::class);

Kuyruğa atma da doğrulamadan sonra gelir. İş kaydını dispatch() etmeden önce imza kontrolünden geçirin, aksi hâlde kuyruğunuz sahte siparişlerle dolar ve worker bunları gerçekmiş gibi işler. Webhook’u önce kabul edip sonra işlemek ayrı bir konu, ama sıralama burada da aynı: önce güvenlik kontrolü, sonra kuyruk.

Özet

X-Shopify-Hmac-Sha256’yı ham body üzerinden, hash_equals ile karşılaştırın. getContent() kullanın, all() değil. Doğrulama middleware’ini stack’in başına koyun ve parse’tan önce çalıştırın.


Bu yazıyı paylaş:

Önceki Yazı
Cache-Control başlıklarını nginx'te doğru yazmak
Sonraki Yazı
MySQL'de utf8 değil utf8mb4 kullanın