# webhook

## Hvad det er

En webhook er en HTTP-forespørgsel, som én tjeneste sender til en anden i det
øjeblik, noget sker hos den: en betaling går igennem, en faktura bliver betalt,
en lang opgave bliver færdig. Den modtagende side registrerer på forhånd en URL,
og afsenderen sender en beskrivelse af hver hændelse dertil med POST. Standard
Webhooks-specifikationen kalder webhooks »a common name for HTTP callbacks« og
beskriver dem som en måde, hvorpå tjenester kan underrette hinanden om
hændelser. Ordet skrives af og til i to ord, web hook, men Stripe, GitHub og
specifikationen skriver det alle i ét ord.

Specifikationen beskriver også en webhook som en del af en tjenestes API, en
slags API, der kører baglæns. Normalt kalder klienten tjenesten, når den vil
have noget. Med en webhook kalder tjenesten klienten, når den har nyt. Det gør
webhooks til trafik mellem servere, hvor begge sider kører en HTTP-server, og
nogle tjenester sender webhooks uden overhovedet at tilbyde et almindeligt API.
Specifikationen kalder afsenderen producer og modtageren consumer, og den URL,
modtageren registrerer, er dens webhook-endpoint.

I specifikationen rejser hændelsen som brødteksten i en HTTP POST, helst som
JSON. Brødteksten har en `type`, skrevet med punktummer og ordnet hierarkisk,
for eksempel `invoice.paid`; et `timestamp` for, hvornår hændelsen fandt sted, i
ISO 8601; og selve hændelsen under `data`. Alt andet ligger i headere:
`webhook-id` identificerer hændelsen, `webhook-timestamp` angiver, hvornår netop
dette forsøg blev foretaget, i Unix-sekunder, og `webhook-signature` beviser,
hvem der har sendt den. Stripe og GitHub bruger deres egne headernavne, blandt
andre `Stripe-Signature` og `X-Hub-Signature-256`, men bestanddelene er de
samme.

En payload kan være fuld og indeholde hele det ændrede objekt, eller tynd og
indeholde ikke stort andet end en identifikator, så modtageren selv må hente
resten gennem API'et. Specifikationen vejer de to op mod hinanden. En fuld
payload sparer modtageren for ekstra kald til API'et. En tynd payload betyder
færre data at generere og sende, mere fleksibilitet og mere plads til ændringer
senere, fordi en tynd payload altid kan vokse til en fuld, men ikke omvendt. Den
gør også dataadgangen sporbar, fordi modtageren udtrykkeligt skal bede om det,
den læser. Uanset hvad råder specifikationen til at holde payloads små, normalt
under 20 kB. Stripe tilbyder et tyndt format og markerer det som det anbefalede.

## Hvorfor det betyder noget

Uden webhooks må et system, der skal vide besked om en ændring et andet sted,
polle: spørge igen og igen, for det meste bare for at høre, at intet er sket, og
alligevel først opdage ændringen ved næste runde. En webhook vender det om. Den
tjeneste, der kender til hændelsen, kalder én gang, i det øjeblik den sker.
Sådan melder betalingsudbydere, kodehostingtjenester og e-mailudbydere tilbage
til de systemer, der er bygget oven på dem.

Den omvendte retning har en pris, og det meste af den betales af den modtagende
ende. Dens endpoint er en offentlig URL, som alle på internettet kan sende en
POST til, og intet ved en indgående forespørgsel viser, hvem der har sendt den,
så specifikationen gør verificering af ægthed til et krav for enhver sikker
implementering. Leveringen prøves igen, indtil modtageren svarer med en
2xx-status, så den samme hændelse kan ankomme mere end én gang; Stripe advarer
om, at endpoints af og til kan modtage dubletter. Rækkefølgen er heller ikke
lovet: Stripe skriver ligeud, at den ikke garanterer, at hændelser ankommer i
den rækkefølge, de blev skabt i.

Afsenderen må på sin side blive ved med at prøve. Specifikationen anbefaler en
tidsplan for nye forsøg, der strækker sig over flere dage, med eksponentiel
backoff og lidt tilfældig jitter. I dens eksempel gøres der ti forsøg, det
sidste 75 timer, 35 minutter og 5 sekunder efter det første. Stripe prøver igen
i op til tre dage i live mode. En modtager, der kommer tilbage inden for det
vindue, indhenter selv det forsømte; derefter kan man hos Stripe sende en
hændelse igen manuelt i op til 15 dage efter, at den blev oprettet, eller i op
til 30 dage med Stripes CLI. Når leveringen bliver ved med at fejle i lang tid,
anbefaler specifikationen, at afsenderen giver modtageren besked ad en anden
kanal, for eksempel e-mail, og deaktiverer endpointet.

## Sådan ser det ud i praksis

Her er headerne på en levering i specifikationens format. Identifikatoren,
tidsstemplet og signaturen er de eksempelværdier, specifikationen selv bruger;
værdien af `Host` og stien er opdigtede:

```http
POST /webhooks/billing HTTP/1.1
Host: example.com
Content-Type: application/json
webhook-id: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W
webhook-timestamp: 1674087231
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
```

Og her er brødteksten, en tynd payload fra samme specifikation, her vist med
indrykning. Når den faktisk sendes, anbefaler specifikationen, at den er
minificeret:

```json
{
  "type": "contact.created",
  "timestamp": "2022-11-03T20:26:10.344522Z",
  "data": {
    "id": "1f81eb52-5198-4599-803e-771906343485"
  }
}
```

Præfikset `v1` markerer en symmetrisk signatur: HMAC-SHA256 over hændelsens
identifikator, tidsstemplet og brødteksten, sat sammen med punktummer og
beregnet med en hemmelig nøgle, der er unik for endpointet. Fordi
identifikatoren og tidsstemplet signeres sammen med brødteksten, kan ingen af
dem ændres, uden at signaturen bliver ugyldig. Specifikationen tillader også
asymmetriske ed25519-signaturer, markeret `v1a`, og anbefaler dem, når man ikke
selv kontrollerer sikkerheden på både afsender- og modtagersiden.

GitHub signerer også sine leveringer med HMAC-SHA256, lægger hex-digesten i
`X-Hub-Signature-256` med præfikset `sha256=` og offentliggør en hemmelighed, en
payload og en signatur, som man kan afprøve en verifikator mod. Her er en
verifikator i Python, afprøvet mod netop det eksempel:

```python
import hashlib
import hmac


def verify(body: bytes, secret: str, header: str) -> bool:
    mac = hmac.new(secret.encode("utf-8"), body, hashlib.sha256)
    expected = "sha256=" + mac.hexdigest()
    return hmac.compare_digest(expected, header)


secret = "It's a Secret to Everybody"
header = (
    "sha256=757107ea0eb2509fc211221cce984b8a"
    "37570b6d7586c22c46f4379c8b043e17"
)
print(verify(b"Hello, World!", secret, header))  # True
print(verify(b"Hello, World! ", secret, header))  # False
```

Ét enkelt mellemrum til sidst i brødteksten er nok til at få verificeringen til
at fejle. `hmac.compare_digest` bruger den samme tid, uanset om strengene
afviger tidligt eller sent; GitHubs dokumentation siger, at man aldrig må
sammenligne signaturer med et almindeligt `==`. En solid handler arbejder
derefter i en fast rækkefølge: læs de rå bytes, verificér signaturen over præcis
de bytes, kontrollér, at tidsstemplet ikke er for gammelt, kontrollér, om
hændelsens identifikator er set før, læg hændelsen i en kø, og svar med en 2xx.
Det egentlige arbejde sker bagefter. Stripe nævner som eksempel at svare med
200, før en kundes faktura markeres som betalt i regnskabssystemet.

To projekter i sitets portefølje beskæftiger sig med webhooks fra den modtagende
ende. I projektet om
[adgangsret via Stripe og in‑app‑køb](/da/portfolio/built-stripe-and-in-app-purchase-entitlements/)
skal et abonnement blandt andet kunne overleve en webhook, der ankommer to
gange, og en webhook, der ankommer i forkert rækkefølge. I
[e‑mailsystemet med failover og leveringslog](/da/portfolio/built-email-with-failover-and-delivery-logging/)
melder indgående webhooks tilbage, hvad hver udbyder gjorde med en besked, og
projektet registrerer både det, der blev sendt, og det, der kom tilbage, i en
afsendelseslog og en tabel over leveringshændelser.

## Hvor det går galt

**Signaturen passer aldrig.** En almindelig årsag er, at modtagerens framework
parser JSON-brødteksten, og at koden serialiserer den igen, før HMAC'en
beregnes. Specifikationen kalder det en meget almindelig fejl:
JSON-serialisering er forskellig fra implementering til implementering, og et
enkelt overflødigt mellemrum gør signaturen ugyldig. Stripe skal have den rå
brødtekst af samme grund og advarer om, at enhver ændring af den får
verificeringen til at fejle. GitHub tilføjer, at en proxy eller load balancer
foran endpointet ikke må ændre payloaden eller headerne, og at payloaden bør
håndteres som UTF-8. Beregn signaturen over de bytes, præcis som de ankom, før
noget parser dem.

**Den samme hændelse ankommer to gange.** Nye forsøg og netværksproblemer giver
begge dubletter. Ifølge specifikationen er `webhook-id` den samme ved hvert nyt
forsøg med en hændelse og fungerer som idempotensnøgle; dens eksempel er at
gemme identifikatorerne i Redis i fem minutter. Stripe beder dig registrere de
hændelses-id'er, du har behandlet, og springe dem over, du allerede har set.
Stripe bemærker også, at der i nogle tilfælde oprettes og sendes to separate
Event-objekter; dem kan man genkende på objektets id i `data.object` sammen med
hændelsestypen. GitHubs `X-GitHub-Delivery`-header forbliver den samme, når en
levering bliver sendt igen. Behandlingen bør være idempotent, så en anden
levering ikke ændrer noget.

**En opsnappet levering bliver afspillet igen.** En gyldig signatur forbliver
gyldig, så en angriber, der opsnapper én levering, kan sende den igen. Derfor
signeres tidsstemplet: specifikationen siger, at et `webhook-timestamp` uden for
en tilladt tolerance skal afvises. Stripes biblioteker bruger som standard fem
minutter, Stripe advarer om, at en tolerance på 0 slår kontrollen helt fra, og
anbefaler NTP til at holde serverens ur præcist. Både Stripe og specifikationen
sætter et nyt tidsstempel på hvert nyt forsøg, så et legitimt nyt forsøg aldrig
er for gammelt. Gem de sete identifikatorer mindst lige så længe som tolerancen,
så bliver alt, der afspilles igen inden for vinduet, fanget som en dublet.

**Hændelserne ankommer i forkert rækkefølge.** Stripes eksempel: når et
abonnement oprettes, kan det give `customer.subscription.created`,
`invoice.created`, `invoice.paid` og, hvis der er en debitering,
`charge.created`, og intet garanterer, at de ankommer i den rækkefølge. Stripes
snapshot-hændelser angiver `created` i hele sekunder, så forskellige hændelser
kan have samme tidsstempel, og Stripe siger, at man ikke skal bruge det til at
sortere hændelser eller finde dubletter. Den robuste fremgangsmåde behandler en
hændelse som en besked om, at noget har ændret sig, og henter objektets aktuelle
tilstand fra API'et, før den handler på den.

**Handleren er for langsom.** GitHub forventer en 2xx inden for 10 sekunder;
derefter afbryder GitHub forbindelsen og regner leveringen som mislykket.
Specifikationen anbefaler en timeout for forespørgslen på et sted mellem 15 og
30 sekunder. En handler, der udfører det egentlige arbejde, inden den svarer,
løber ind i timeouten, afsenderen prøver igen, og arbejdet bliver udført to
gange. Stripe og GitHub anbefaler begge en kø. Stripe peger på spidsbelastningen
i starten af måneden, når alle abonnementer fornyes.

**Kun en 2xx tæller.** Alt andet er en fejl, også en 3xx, for både
specifikationen og Stripe, så et endpoint, der er flyttet bag en omdirigering,
fejler ved hver levering, indtil den registrerede URL bliver opdateret.
Specifikationen giver nogle få koder en betydning for afsenderen: `410 Gone`
betyder, at endpointet skal deaktiveres, en 429, 502 eller 504 betyder, at
afsenderen skal sætte tempoet ned, og en `retry-after`-header bør styre det
næste forsøg.

**For afsenderen er URL'en fjendtligt input.** Kunder kan angive en hvilken som
helst URL, og afsenderens servere vil kalde den. Specifikationen kalder
webhook-systemer særligt sårbare over for server-side request forgery (SSRF): en
URL, der peger på en intern adresse, får afsenderen til at kalde ind i sit eget
netværk. Dens råd er at sende alle webhook-forespørgsler gennem en proxy, der
filtrerer interne IP-adresser fra, for eksempel Stripes smokescreen, og at køre
webhook-workerne i et privat subnet, der ikke kan nå interne tjenester.

## Læs mere

- [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md)
  — version 1.0.0: payloadens opbygning, signaturmetoden, headere, tidsplanen
  for nye forsøg, håndtering af statuskoder og SSRF.
- [Receive Stripe events in your webhook endpoint](https://docs.stripe.com/webhooks)
  — tynde payloads og snapshot-payloads, nye forsøg, dubletter, hændelser i
  forkert rækkefølge og beskyttelse mod replay-angreb.
- [Best practices for using webhooks](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks)
  — GitHubs liste: få hændelser, en webhook-hemmelighed, HTTPS, et svar inden
  for 10 sekunder, ny levering og `X-GitHub-Delivery`.
- [Validating webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries)
  — `X-Hub-Signature-256`, sammenligning i konstant tid og et testeksempel at
  kontrollere en verifikator mod.

<https://engineer.company/da/glossary/webhook/>
