Mengapa kontrak API OAuth2 penting untuk webhook multi-tenant?

Larutan webhook multi-tenant menuntut kepastian bahwa setiap payload valid, datang dari issuer yang sah, dan dapat diproses ulang tanpa duplikasi. Kontrak API OAuth2 di Laravel menjadi jawaban karena mengatur endpoints, scope per tenant, serta pemeriksaan issuer sebelum payload diproses. Langsung mengamankan entry point webhook adalah inti dari masalah ini.

Artikel ini menunjukkan cara merancang endpoint, middleware, dan pipeline yang melindungi webhook dari replay, sekaligus menjaga sinkronisasi refresh token di backend. Di akhir, Anda akan memiliki rujukan konfigurasi route, middleware policy, dan pengujian integrasi untuk validasi otentikasi serta replay-safe.

Menentukan kontrak API OAuth2 untuk webhook tenant

Mulailah dengan mendefinisikan endpoint publik yang hanya menerima request dengan token OAuth2 yang memuat scope khusus webhook. Definisikan scope seperti webhook:tenant.{tenant_id} agar setiap tenant hanya bisa mengakses webhook-nya sendiri.

Kontrak mesinnya:

  • Endpoint: POST /api/webhooks/{tenant} dengan JSON payload terstruktur.
  • Scope: satu scope per tenant atau satu scope umum plus claim tenant_id.
  • Issuer: server OAuth2 internal atau pihak ketiga, selalu bandingkan iss token dengan daftar issuer yang diizinkan.

Tip: jangan hanya mengandalkan scopes. Kombinasikan dengan claim tenant_id dan identifier issuer untuk memastikan token tidak valid diberbagai tenant.

Menerapkan middleware auth dan pemeriksaan issuer

Buat middleware khusus Laravel yang memverifikasi token Personal Access/Client Credentials dan mengonfirmasi issuer, scope, serta tenant_id. Middleware ini mengikat request ke tenant yang benar dan menolak jika claim tidak cocok.

Contoh pengecekan di middleware:

public function handle(Request $request, Closure $next, string $scopePrefix)
{
    $token = $request->user();
    if (! $token || ! str_starts_with($token->scope, $scopePrefix)) {
        abort(403, 'Scope invalid');
    }

    if ($token->issuer !== config('webhook.valid_issuer')) {
        abort(403, 'Issuer tidak valid');
    }

    $tenantId = $request->route('tenant');
    if ($token->tenant_id !== $tenantId) {
        abort(403, 'Tenant mismatch');
    }

    return $next($request);
}

Letakkan middleware ini dalam app/Http/Kernel.php agar bisa dipanggil via ->middleware(['oauth.webhook']). Jika OAuth server Anda menorolekan token ke tabel custom, pastikan get claim terjadi melalui helper yang sama di middleware.

Strategi idempotensi payload dan retry-safe handling

Webhook sering di-retry oleh penyedia eksternal. Desain kontrak agar payload bisa diproses ulang tanpa efek ganda:

  • Idempotensi key: setiap payload menyertakan header seperti X-Idempotency-Key. Simpan key ini di tabel webhook_idempotency_keys bersamaan dengan status pemrosesan. Jika key sudah ada, kembalikan status yang sama tanpa menjalankan logika bisnis lagi.
  • Payload hash: tambahkan hash dari body untuk memastikan key bukan hanya replay request kosong.
  • Replay-safe handling: sebelum memanggil service internal, cek duplikat via job queue deduplikasi atau flag di database.

Gunakan queue worker yang menerapkan tries() dan timeout() agar proses panjang dapat diulang tanpa kehilangan state idempotensi.

Menyinkronkan refresh token di backend dengan pipeline webhook

Untuk menjaga token OAuth2 tetap valid, buat pipeline yang secara berkala menyinkronkan refresh token tenant:

  1. Buat job yang memanggil grant refresh_token untuk setiap tenant, cek expires_at.
  2. Simpan token baru di tabel tenant_oauth_tokens bersama timestamp sinkronisasi.
  3. Ketika webhook diterima, middleware membaca token paling mutakhir agar pengiriman ke layanan downstream tetap autentik.

Pertimbangkan pipeline berikut:

TenantOauthSyncJob::withChain([
    new RefreshTokenJob($tenant),
    new UpdateCacheJob($tenant),
])->dispatch();

Dengan cara ini, backend siap mengeluarkan token valid sebelum webhook memicu aksi downstream.

Contoh konfigurasi route dan middleware policy

Contoh konfigurasi route di routes/api.php:

Route::prefix('webhooks')->middleware(['oauth.webhook'])->group(function () {
    Route::post('{tenant}', [WebhookController::class, 'handle'])
        ->middleware('tenant.scope:webhook:tenant');
});

Policy tambahan bisa memeriksa header idempotensi dan queue:

public function handle(Request $request, Closure $next)
{
    if (! $request->hasHeader('X-Idempotency-Key')) {
        abort(400, 'Idempotency key required');
    }

    $key = $request->header('X-Idempotency-Key');
    if (IdempotencyKey::exists($key, $request->route('tenant'))) {
        return response()->json(['status' => 'duplicate'], 200);
    }

    IdempotencyKey::store($key, $request->route('tenant'));

    return $next($request);
}

Policy ini bisa dikemas sebagai middleware tambahan webhook.idempotency sehingga route tetap bersih.

Uji integrasi untuk otentikasi dan replay-safe

Gunakan fitur HTTP testing Laravel untuk memastikan pipeline bekerja:

public function test_webhook_authenticated_and_idempotent()
{
    $payload = ['event' => 'order.paid', 'data' => [...]];
    $token = $this->createOauthTokenForTenant('tenant-123');

    $response = $this->withHeaders([
        'Authorization' => 'Bearer ' . $token,
        'X-Idempotency-Key' => 'key-001',
    ])->postJson('/api/webhooks/tenant-123', $payload);

    $response->assertOk();

    $second = $this->withHeaders([
        'Authorization' => 'Bearer ' . $token,
        'X-Idempotency-Key' => 'key-001',
    ])->postJson('/api/webhooks/tenant-123', $payload);

    $second->assertStatus(200);
    $second->assertJson(['status' => 'duplicate']);
}

Tambahkan asersi tambahan untuk memastikan issuer dan scope ditentukan dengan benar dan job queue menandai status requeued ketika terjadi exception.

Kesimpulan

Kontrak API OAuth2 di Laravel bukan sekadar endpoint; ia adalah rangkaian aturan—scope, issuer, middleware, idempotensi, dan pipeline refresh token—yang memastikan webhook tenant diproses aman, konsisten, dan siap retry. Dengan mengikuti pola ini, aplikasi dapat menghindari replay attack, menjaga otorisasi tenant, serta menyinkronkan token tanpa menyisakan gap keamanan.