Journal

Webhooks, die ankommen, und was tun, wenn sie doppelt ankommen

Zwölf Events, eine Signatur über Zeitstempel und Body, acht Versuche über etwa 21 Stunden und eine ID pro Event, damit sich Duplikate billig verwerfen lassen.

Ein Login-Provider, der nur Fragen beantwortet, ist ein halber Provider. Ihr System muss erfahren, wenn sich eine Person registriert, anmeldet, Ihrer Anwendung den Zugriff entzieht oder wenn ihr Konto gelöscht wird, ohne danach zu pollen. EAuth sagt es Ihnen über Webhooks, und dieser Beitrag behandelt die Teile, die entscheiden, ob eine Webhook-Integration im Betrieb hält: was gesendet wird, woran Sie erkennen, dass es von uns kommt, was passiert, wenn Ihr Server ausfällt, und was Sie mit einer Zustellung tun, die Sie schon gesehen haben.

Was gesendet wird

Zwölf Events. Sieben betreffen Personen und ihre Sessions: user.created, wenn sich jemand registriert oder Single Sign-on ein Konto anlegt; user.signed_in bei jedem Authorization Code, den Sie eintauschen, nicht bei Refreshes; consent.granted und consent.revoked; session.revoked mit Grund, wann immer Ihre Refresh Tokens für jemanden aufgehört haben zu funktionieren, ohne dass Sie das verlangt haben, also beim Abmelden überall, bei einem Passwort-Reset, bei einem doppelt benutzten Token und wenn eine Person eine Organisation verlässt; user.password_changed; und user.deleted. Fünf betreffen Organisationen: Organisation angelegt, Organisation gelöscht, Mitglied hinzugefügt, geändert oder entfernt. Ein Endpoint, der nichts abonniert hat, bekommt alle.

Jeder Body ist ein kleines JSON-Objekt: ID und Typ des Events, wann es passiert ist, Ihre Client-ID und ein data-Objekt mit IDs und wenig sonst. Brauchen Sie den aktuellen Namen oder die Adresse der Person, fragen Sie mit der ID die API. Ein Webhook, der das Profil mitliefert, wäre eine Kopie, die in dem Moment veraltet, in dem sie geschrieben wird.

Seit dem 3. Oktober zeigt sich eine Regel in der Reihenfolge der Events: Keine Anwendung bekommt ein Konto, dessen E-Mail-Adresse nicht bestätigt ist. user.created kommt weiterhin in dem Moment, in dem sich jemand registriert, mit email_verified false, aber Ihre Anwendung bekommt kein Token für die Person, bevor sie den Link geöffnet hat. user.signed_in kommt also nach der Bestätigung, und wer nie bestätigt, hinterlässt Ihnen ein user.created und sonst nichts. Ein Konto, das per Single Sign-on entsteht, kommt mit email_verified true an.

Woran Sie erkennen, dass es von uns kommt

Jede Zustellung trägt Elchi-Signature: t=<timestamp>,v1=<hmac>, wobei der HMAC-SHA256 mit dem Secret Ihres Endpoints über den Zeitstempel, einen Punkt und den rohen Body berechnet wird. Das Schema ist das von Stripe mit umbenanntem Header, ein bestehender Verifier braucht also eine geänderte Zeile. Prüfen Sie die Signatur gegen die rohen Bytes, die Sie empfangen haben, bevor Sie irgendetwas parsen, mit einem Vergleich in konstanter Zeit, und weisen Sie einen Zeitstempel ab, der mehr als fünf Minuten von Ihrer eigenen Uhr abweicht. Der Zeitstempel im signierten String macht eine abgefangene Zustellung später nutzlos: Der Body eines user.deleted-Events ändert sich nie, ohne Zeitstempel wäre eine Wiederholung also nicht vom Original zu unterscheiden.

Das Secret beginnt mit whsec_, damit ein geleaktes in einem Log oder bei einem Repository-Scan erkennbar ist, und es wird einmal angezeigt, wenn der Endpoint angelegt wird. Bei uns kann es kein Digest sein, denn es muss jede Zustellung signieren. Deshalb wird es mit AES-256-GCM verschlüsselt gespeichert, unter einem Schlüssel, der nie die Datenbank berührt. Der Export Ihrer Anwendung lässt es weg. Einen Button zum Rotieren gibt es nicht: Um das Secret zu wechseln, legen Sie einen zweiten Endpoint mit derselben URL an, akzeptieren beide Secrets, bis der neue Endpoint zustellt, und löschen dann den alten. Während dieser Überlappung kommt jedes Event zweimal mit derselben ID an, was der übernächste Abschnitt harmlos macht.

Was passiert, wenn Ihr Server ausfällt

Eine Zustellung, die nicht innerhalb von zehn Sekunden eine 2xx-Antwort bekommt, ist gescheitert. Jedes Event bekommt acht Versuche: den ersten sofort, dann nach 30 Sekunden, 2, 10 und 30 Minuten und 2, 6 und 12 Stunden, jede Wartezeit gezählt ab dem vorigen Versuch. Der letzte Versuch kommt etwa 21 Stunden nach dem ersten, und eine Zustellung, die dann scheitert, wird als aufgegeben markiert.

Auch der Endpoint als Ganzes hat ein Limit. Sind 25 Versuche in Folge gescheitert, gezählt über alle seine Events, wird er ausgeschaltet, und die Konsole markiert ihn als deaktiviert. Eine Mail gibt es nicht. Solange er aus ist, wird nichts für ihn vorgemerkt: Events, die in dieser Zeit passieren, werden nie zugestellt. Zustellungen, die schon warteten, laufen weiter, wenn Sie den Endpoint in der Konsole wieder einschalten, und eine aufgegebene lässt sich von Hand erneut senden.

Zusammen ergibt das den Kompromiss. Eine ruhige Anwendung, deren Empfänger zehn Minuten ausfällt, verliert nichts: eine Handvoll Events mit je ein paar Wiederholungen. Eine Anwendung mit viel Verkehr erreicht 25 gescheiterte Versuche innert Minuten, und bis jemand den Endpoint wieder einschaltet, gehen ihre Events verloren. Schauen Sie nach einem Ausfall Ihres Empfängers in die Konsole, nicht nur in Ihre eigenen Logs.

Zehn Sekunden sind absichtlich nicht lang. Antworten Sie mit 200, sobald Sie den Body gespeichert haben, und erledigen Sie die Arbeit danach. Ein Handler, der drei andere Dienste aufruft, bevor er antwortet, wird wie ein gescheiterter wiederholt und läuft dann viermal.

Was Sie mit einer Zustellung tun, die Sie schon gesehen haben

Jeder Versuch eines Events trägt dieselbe Elchi-Event-Id, die auch die id im Body ist. Speichern Sie sie mit der Arbeit, die Sie erledigt haben, und kommt eine ID an, die Sie schon haben, antworten Sie mit 200 und verwerfen den Body, ohne ihn zu parsen. Mehr als At-least-once-Zustellung bietet ehrlicherweise kein Webhook-System. Diese eine Regel macht daraus auf Ihrer Seite eine Exactly-once-Verarbeitung.

Sie macht auch den Replay-Button gefahrlos. Die Konsole listet für jeden Endpoint die letzten acht Zustellungen mit Status, Antwortcode, Versuchen und Zeit, und jede davon, die nicht zugestellt wurde, lässt sich erneut senden. Zustellungen werden 30 Tage aufbewahrt und dann mit ihren Payloads gelöscht, denn ein Payload nennt eine Person.

Regeln für die URL

Nur https, mit einem Host und ohne Zugangsdaten darin. localhost und literale private oder reservierte Adressen werden beim Registrieren des Endpoints abgewiesen. Ein Hostname wird bei der Verwendung geprüft: Jede Adresse, auf die er auflöst, wird bei jeder Verbindung erneut geprüft, sodass ein Name, der später ins interne Netz zeigt, trotzdem abgewiesen wird. Diese Prüfung verhindert, dass sich ein Webhook-System nutzen lässt, um einen Server zu erreichen, den es nie sehen sollte. Redirects werden nicht verfolgt, denn wer einem folgt, könnte einen signierten POST in eine Anfrage an ein Ziel verwandeln, das Sie nie registriert haben.

Was es nicht gibt

Es gibt kein Batching und keine Reihenfolgegarantie über Events hinweg: Zwei Events zu einer Person können in falscher Reihenfolge ankommen, wenn eines wiederholt wird. Behandeln Sie also jedes für sich, session.revoked eingeschlossen. Es gibt kein Event für einen Import, denn Ihr System kennt diese Leute schon. Es gibt keine Warteschlange für die Zeit, in der ein Endpoint ausgeschaltet ist, und keine Mail, wenn das passiert. Und die Konsole zeigt die letzten acht Zustellungen pro Endpoint, kein durchsuchbares Log der 30 Tage.

Quellen

  1. Webhooks, EAuth documentation, Elchi Studios, gelesen am
  2. RFC 2104: HMAC: Keyed-Hashing for Message Authentication, IETF, gelesen am
  3. Resolve webhook signature verification errors, Stripe, gelesen am
  4. Server-Side Request Forgery Prevention Cheat Sheet, OWASP, gelesen am

Geschrieben von Samuel Krauss, Founder. Abgelegt unter eauth, webhooks, integration.

Übersetzt aus dem Englischen. Zum englischen Original

Alle Beiträge Dieser Beitrag als Markdown Atom-Feed