一般WebhooksWebhook を確認中

Svixライブラリを使用したWebhookの検証方法

公式ライブラリを使用した検証

ライブラリを使用した検証

npm install svix  
# Or  
yarn add svix  

次に、以下のコードを使用してWebhookを検証します。ペイロードはリクエストの生の(文字列)ボディであり、ヘッダーはリクエストで渡されたヘッダーです。

生のリクエストボディを使用する

Webhookを検証する際には、生のリクエストボディを使用する必要があります。暗号署名は、些細な変更にも影響を受けるためです。リクエストをJSONとして解析してから文字列化するというフレームワークに注意してください。これでも署名検証が壊れます。

様々なフレームワークで生のリクエストボディを取得する方法については、以下の例を参照してください。

署名は、エンドポイントを追加した場所(例:アプリケーションポータル)から取得する必要があります。

import { Webhook } from "svix";
 
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
 
// These were all sent from the server
const headers = {
  "svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
  "svix-timestamp": "1614265330",
  "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"test": 2432232314}';
 
const wh = new Webhook(secret);
// Throws on error, returns the verified content on success
wh.verify(payload, headers); 

フレームワーク固有の例

お気に入りのフレームワークに上記の例を調整する方法を次に示します。

Python (Django)

from django.http import HttpResponse
 
from svix.webhooks import Webhook, WebhookVerificationError
 
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
 
@csrf_exempt
def webhook_handler(request):
    headers = request.headers
    payload = request.body
 
    try:
        wh = Webhook(secret)
        msg = wh.verify(payload, headers)
    except WebhookVerificationError as e:
        return HttpResponse(status=400)
 
    # Do something with the message...
 
    return HttpResponse(status=204)

Python (Flask)

from flask import request
 
from svix.webhooks import Webhook, WebhookVerificationError
 
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
 
@app.route('/webhook/')
def webhook_handler():
    headers = request.headers
    payload = request.get_data()
 
    try:
        wh = Webhook(secret)
        msg = wh.verify(payload, headers)
    except WebhookVerificationError as e:
        return ('', 400)
 
    # Do something with the message...
 
    return ('', 204)

Python (FastAPI)

from fastapi import Request, Response, status
 
from svix.webhooks import Webhook, WebhookVerificationError
 
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
 
@router.post("/webhook/", status_code=status.HTTP_204_NO_CONTENT)
async def webhook_handler(request: Request, response: Response):
    headers = request.headers
    payload = await request.body()
 
    try:
        wh = Webhook(secret)
        msg = wh.verify(payload, headers)
    except WebhookVerificationError as e:
        response.status_code = status.HTTP_400_BAD_REQUEST
        return
 
    # Do something with the message...

Node.js (Next.js)

import { Webhook } from "svix";
import { buffer } from "micro";
 
export const config = {
    api: {
        bodyParser: false,
    },
}
 
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
 
export default async function handler(req, res) {
    const payload = (await buffer(req)).toString();
    const headers = req.headers;
 
    const wh = new Webhook(secret);
    let msg;
    try {
        msg = wh.verify(payload, headers);
    } catch (err) {
        res.status(400).json({});
    }
 
    // Do something with the message...
 
    res.json({});
}

Node.js (Next.js 13 App Router)

import { Webhook } from "svix";
 
const webhookSecret: string = process.env.WEBHOOK_SECRET || "your-secret";
 
export async function POST(req: Request) {
  const svix_id = req.headers.get("svix-id") ?? "";
  const svix_timestamp = req.headers.get("svix-timestamp") ?? "";
  const svix_signature = req.headers.get("svix-signature") ?? "";
 
  const body = await req.text();
 
  const sivx = new Webhook(webhookSecret);
 
  let msg;
  
  try {
    msg = sivx.verify(body, {
      "svix-id": svix_id,
      "svix-timestamp": svix_timestamp,
      "svix-signature": svix_signature,
    });
  } catch (err) {
    return new Response("Bad Request", { status: 400 });
  }
 
  console.log(msg);
 
  // Rest
 
  return new Response("OK", { status: 200 });
}

Node.js (Netlify Functions)

import { Webhook } from "svix";
 
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
 
 
export const handler = async ({body, headers}) => {
    const payload = body;
 
    const wh = new Webhook(secret);
    let msg;
    try {
        msg = wh.verify(payload, headers);
    } catch (err) {
        res.status(400).json({});
    }
 
    // Do something with the message...
 
    res.json({});
}

Node.js (Express)

注: この例をより大きなコードベースに統合する際には、express.json()ミドルウェアをWebhookルートに適用しないようにする必要があります。ペイロードは、事前に解析することなくwh.verifyに渡す必要があるためです。

import { Webhook } from "svix";
import bodyParser from "body-parser";
 
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
 
app.post('/webhook', bodyParser.raw({ type: 'application/json' }), (req, res) => {
    const payload = req.body;
    const headers = req.headers;
 
    const wh = new Webhook(secret);
    let msg;
    try {
        msg = wh.verify(payload, headers);
    } catch (err) {
        res.status(400).json({});
    }
 
    // Do something with the message...
 
    res.json({});
});

Node.js (NestJS)

rawBodyフラグをtrueに設定してアプリケーションを初期化します。詳細はNestJSドキュメントを参照してください。

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create(
    AppModule,
    { rawBody: true } // add rawBody flag
  );
  await app.listen(3000);
}
bootstrap();
// webhook.controller.ts
import { Controller, Post, RawBodyRequest, Req } from '@nestjs/common';
import { Request } from 'express';
import { Webhook } from 'svix';
 
@Controller('webhook')
class WebhookController {
  @Post()
  webhook(@Req() request: RawBodyRequest<Request>) {
    const secret = 'whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw';
    const wh = new Webhook(secret);
 
    const payload = request.rawBody.toString('utf8');
    const headers = request.headers;
 
    let msg;
    try {
      msg = wh.verify(payload, headers);
    } catch (err) {
      // handle error
    }
 
    // Do something with the message...
  }
}

Go (標準ライブラリ)

package main
 
import (
    "io"
    "log"
    "net/http"
 
    svix "github.com/svix/svix-webhooks/go"
)
 
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
 
func main() {
 
    wh, err := svix.NewWebhook(secret)
    if err != nil {
        log.Fatal(err)
    }
 
    http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
        headers := r.Header
        payload, err := io.ReadAll(r.Body)
        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }
 
        err = wh.Verify(payload, headers)
        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }
 
        // Do something with the message...
 
        w.WriteHeader(http.StatusNoContent)
 
    })
    http.ListenAndServe(":8080", nil)
}

Go (Gin)

package main
 
import (
    "io"
    "log"
    "net/http"
 
    "github.com/gin-gonic/gin"
    svix "github.com/svix/svix-webhooks/go"
)
 
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
 
func main() {
 
    wh, err := svix.NewWebhook(secret)
    if err != nil {
        log.Fatal(err)
    }
 
    r := gin.Default()
    r.POST("/webhook", func(c *gin.Context) {
        headers := c.Request.Header
        payload, err := io.ReadAll(c.Request.Body)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
 
        err = wh.Verify(payload, headers)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
 
        // Do something with the message...
 
        c.JSON(200, gin.H{})
    })
    r.Run()
}

Rust (axum)

以下のwebhook_inルートをaxumルーターに追加します。

use axum::{body::Bytes, http::StatusCode};
use hyper::HeaderMap;
 
pub const SECRET: &'static str = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
 
pub async fn webhook_in(headers: HeaderMap, body: Bytes) -> StatusCode {
    let wh = svix::webhooks::Webhook::new(SECRET);
    if let Err(_) = wh {
        return StatusCode::INTERNAL_SERVER_ERROR;
    }
 
    if let Err(_) = wh.verify(&body, &headers) {
        return StatusCode::BAD_REQUEST;
    }
 
    // Do something with the message...
 
    StatusCode::NO_CONTENT
}

Ruby (Ruby on Rails)

プロジェクトを設定したら、config/routes.rbファイルのRails.application.routes.drawブロックの先頭にルートを追加します。

Rails.application.routes.draw do
  post "/webhook", to: "webhook#index"
 
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

上記のルートは、POST /webhookリクエストがWebhookControllerのindexアクションにマップされることを宣言しています。

WebhookControllerとそのindexアクションを作成するには、コントローラージェネレーターを実行します(適切なルートが既に存在するため、--skip-routesオプションを使用します)。

bin/rails generate controller Webhook index --skip-routes  

Railsはいくつかのファイルを作成します。

    create  app/controllers/webhook_controller.rb
    invoke  erb
    create    app/views/webhook
    create    app/views/webhook/index.html.erb
    invoke  test_unit
    create    test/controllers/webhook_controller_test.rb
    invoke  helper
    create    app/helpers/webhook_helper.rb
    invoke    test_unit
    invoke  assets
    invoke    scss
    create      app/assets/stylesheets/webhook.scss

これで、新しく作成されたapp/controllers/webhook_controller.rbファイルに検証ロジックを追加できます。

require  'svix'  
  
class  WebhookController  < ApplicationController  
 protect_from_forgery with:  :null_session  # disables CSRF middleware; required for API endpoints  
 def  index begin payload = request.body.read headers = request.headers wh = Svix::Webhook.new("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw")  
 json = wh.verify(payload, headers)  
 # Do something with the message...  
 head :no_content rescue head :bad_request end endend  
 

PHP (Laravel)

routes/api.phpファイルで、最後のuseディレクティブの後に次の行を追加します。

use Svix\Webhook;
use Svix\Exception\WebhookVerificationException;
 
Route::post('webhook', function(Request $request) {
    $payload = $request->getContent();
    $headers = collect($request->headers->all())->transform(function ($item) {
        return $item[0];
    });
 
    try {
        $wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw");
        $json = $wh->verify($payload, $headers);
 
        # Do something with the message...
 
        return response()->noContent();
    } catch (WebhookVerificationException $e) {
        return response(null, 400);
    }
});