Zur Dokumentation springen
Ruby

Eine Zustellung verifizieren

`OpenEmail.verify_webhook_signature`: in konstanter Zeit, mit einem Replay-Fenster und dem geparsten Event zurück.

In einem Request-Handler

Eine Webhook-URL ist öffentlich. Alles im Internet kann JSON in der richtigen Form per POST dorthin schicken. Ein Handler, der den type des Events liest, ohne die Signatur zu prüfen, ist daher eine offene Schreib-API.

app/controllers/open_email_webhooks_controller.rb
class OpenEmailWebhooksController < ActionController::API  def create    event = OpenEmail.verify_webhook_signature(      payload: request.raw_post,      headers: request.headers,      secret: ENV.fetch("OPENEMAIL_WEBHOOK_SECRET"),      tolerance_seconds: 300    )     Rails.logger.info("#{event[:type]} #{event[:data]}")    head :no_content  rescue OpenEmail::WebhookSignatureError    head :bad_request  endend

Übergeben Sie den ROHEN Body als String. Erneutes Parsen und Serialisieren ändert die Reihenfolge der Schlüssel und den Leerraum, und die Signatur passt dann nicht mehr. Deshalb löst ein Hash, etwa params aus Rails, einen ArgumentError aus, statt geprüft zu werden. headers: nimmt einen Hash mit dem Header-Namen in beliebiger Schreibweise, ein Rack-env, in dem der Header als HTTP_X_OPENEMAIL_SIGNATURE ankommt, oder request.headers aus Rails. Ein Wert, der ein Array ist, wird aus seinem ersten Element gelesen.

Zwei Dinge, die das hier erledigt und die eine selbstgebaute Prüfung meist nicht leistet: Es vergleicht den MAC in konstanter Zeit, sodass sich das korrekte Präfix nicht über die Laufzeit rekonstruieren lässt, und es weist eine Zustellung zurück, die in eine der beiden Richtungen mehr als tolerance_seconds: alt ist, fünf Minuten, sofern Sie nichts anderes angeben, sodass eine mitgeschnittene Anfrage nicht für immer wiederverwendbar ist. tolerance_seconds: 0 schaltet die Replay-Prüfung ab. Beide Fehler sind still. Ein Handler mit einem davon besteht jeden Test, auf den Sie kämen.

Es löst bei jedem Fehlschlag OpenEmail::WebhookSignatureError aus: bei einem fehlenden X-OpenEmail-Signature-Header, bei einem, der nicht die Form t=<seconds>,v1=<hex> hat, bei einem Zeitstempel außerhalb des Fensters oder bei einer Signatur, die nicht passt. Bei Erfolg gibt es den Body geparst als Hash mit Symbol-Schlüsseln zurück, id, type, createdAt und data, es gibt also kein zweites JSON.parse, das man falsch machen könnte.

Ein fehlendes oder leeres secret: löst stattdessen einen ArgumentError aus, weil das ein Fehler in Ihrer Konfiguration ist und keine schlechte Zustellung. Fangen Sie nur OpenEmail::WebhookSignatureError ab und antworten Sie mit 400. So antwortet ein falsch konfigurierter Server mit 500, und die Zustellung wird erneut versucht, sobald Sie das behoben haben.

Es verwendet OpenSSL aus der Standardbibliothek, das das Gem bereits lädt, und braucht daher nichts weiter.

In einer Rack- oder Sinatra-App

Außerhalb von Rails übergeben Sie das Rack-env selbst als headers: und lesen den rohen Body aus der Anfrage.

require "openemail"require "rack" webhook = lambda do |env|  event = OpenEmail.verify_webhook_signature(    payload: Rack::Request.new(env).body.read,    headers: env,    secret: ENV.fetch("OPENEMAIL_WEBHOOK_SECRET")  )   warn "#{event[:type]} #{event[:id]}"  [204, {}, []]rescue OpenEmail::WebhookSignatureError  [400, {"content-type" => "text/plain"}, ["bad signature"]]end run webhook

Die Rack-Version ist eine vollständige config.ru. Antworten Sie in beiden schnell: Eine Zustellung, die innerhalb von 5 Sekunden keine Antwort bekommt, gilt als fehlgeschlagen und wird später erneut gesendet. Übergeben Sie die Arbeit daher an einen Job und antworten Sie mit einem 2xx.

OpenEmail::WEBHOOK_SIGNATURE_HEADERS nennt die drei Header, die eine Zustellung trägt: X-OpenEmail-Signature, X-OpenEmail-Event mit dem Typ des Events und X-OpenEmail-Delivery mit seiner id, derselben id wie im Body. Diese id bleibt bei jeder Wiederholung und jedem erneuten Abspielen des Events gleich. Sie ist daher die, die Sie speichern, wenn Sie bereits bearbeitete Events überspringen.

Während das Secret rotiert

webhooks.rotate_secret hat kein Überlappungsfenster: Zustellungen werden ab dem Moment, in dem es zurückkehrt, mit dem neuen Secret signiert. Rollen Sie zuerst einen Empfänger aus, der beide Secrets akzeptiert, rotieren Sie, speichern Sie das neue Secret dort, wo der Empfänger es liest, und entfernen Sie dann das alte.

two_secrets.rb
def verify_delivery(payload, headers)  secrets = [ENV.fetch("OPENEMAIL_WEBHOOK_SECRET"), ENV["OPENEMAIL_WEBHOOK_SECRET_NEXT"]].compact   secrets.each_with_index do |secret, index|    return OpenEmail.verify_webhook_signature(payload:, headers:, secret:)  rescue OpenEmail::WebhookSignatureError    raise if index == secrets.size - 1  endend

webhooks.test sendet ein signiertes synthetisches Event. Rufen Sie es nach der Rotation auf, um zu beweisen, dass das neue Secret verifiziert, bevor Sie das alte entfernen.