Documentation
¶
Overview ¶
Package webhook verifies signed incoming webhooks: Middleware checks an HMAC-SHA256 of the raw body (GitHub, Shopify, your own integrations), Standard checks Standard Webhooks signatures (Svix, Resend, Clerk) with a timestamp replay window, and Dedupe drops at-least-once redeliveries.
r.Post("/webhooks/github", handle).Use(webhook.Middleware("X-Hub-Signature-256", secret, 1<<20))
Middleware bounds the body, verifies the signature header and answers 401 (ErrSignature) on mismatch; VerifyHMAC and Sign are the building blocks for custom schemes and tests.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/security.md.
Example ¶
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"github.com/AlexGalhardo/vukem/webhook"
)
func main() {
secret := []byte("whsec_example")
handler := webhook.Middleware("X-Hub-Signature-256", secret, 1<<20)(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
fmt.Fprintf(w, "accepted %s", body)
}),
)
payload := `{"action":"opened"}`
req := httptest.NewRequest(http.MethodPost, "/webhooks/github", strings.NewReader(payload))
req.Header.Set("X-Hub-Signature-256", webhook.Sign(secret, []byte(payload)))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println(rec.Code, rec.Body.String())
forged := httptest.NewRequest(http.MethodPost, "/webhooks/github", strings.NewReader(payload))
forged.Header.Set("X-Hub-Signature-256", "sha256=00")
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, forged)
fmt.Println(rec.Code)
}
Output: 200 accepted {"action":"opened"} 401
Index ¶
- Variables
- func Dedupe(store cache.Store, id func(*http.Request) string, o DedupeOptions) func(http.Handler) http.Handler
- func Middleware(header string, secret []byte, maxBody int64) func(http.Handler) http.Handler
- func Sign(secret, payload []byte) string
- func Standard(secret string, o StandardOptions) (func(http.Handler) http.Handler, error)
- func StandardID(r *http.Request) string
- func VerifyHMAC(secret, payload []byte, signature string) bool
- type DedupeOptions
- type StandardOptions
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrSignature = &errs.HTTPError{Status: http.StatusUnauthorized, Message: "Invalid webhook signature."}
ErrSignature is the 401 answered for missing or wrong signatures.
Functions ¶
func Dedupe ¶ added in v0.22.0
func Dedupe(store cache.Store, id func(*http.Request) string, o DedupeOptions) func(http.Handler) http.Handler
Dedupe runs the handler at most once per event id: providers deliver at least once, so a redelivery of a handled event gets 200 with "Webhook-Duplicate: true" without running the handler again, and a duplicate that arrives while the first is running gets 409 (the provider retries). Deliveries answered with a status of 300 or above are forgotten so the retry runs. A missing id is a 400, and a store failure is a 503 (fail closed). Put it after the signature check.
func Middleware ¶
Middleware verifies the signature in header against the raw body (at most maxBody bytes, default 1 MiB when maxBody <= 0) and restores the body for the handler. Failures get 401 and a "webhook.signature_invalid" security event. It does not bound replays; use Standard when the provider signs a timestamp, and Dedupe to drop redeliveries.
func Standard ¶ added in v0.22.0
Standard verifies the Standard Webhooks scheme (https://www.standardwebhooks.com, used by Svix, Resend, Clerk and others): webhook-signature carries one or more "v1,<base64 HMAC-SHA256>" over "<webhook-id>.<webhook-timestamp>.<body>", and webhook-timestamp must be within Tolerance. secret is the provider's "whsec_<base64>" value (the prefix is optional). A malformed secret is an error at boot rather than a webhook that never verifies.
func StandardID ¶ added in v0.22.0
StandardID returns the webhook-id header, the event id for Dedupe with Standard Webhooks providers.
func VerifyHMAC ¶
VerifyHMAC reports whether signature is the HMAC-SHA256 of payload with secret. The signature may be hex or base64 and may carry a "sha256=" prefix. The comparison is constant time.
Example ¶
package main
import (
"fmt"
"github.com/AlexGalhardo/vukem/webhook"
)
func main() {
secret := []byte("whsec_example")
sig := webhook.Sign(secret, []byte("hello"))
fmt.Println(webhook.VerifyHMAC(secret, []byte("hello"), sig))
fmt.Println(webhook.VerifyHMAC(secret, []byte("hello!"), sig))
}
Output: true false
Types ¶
type DedupeOptions ¶ added in v0.22.0
type DedupeOptions struct {
// TTL remembers handled event ids (default 72h, longer than providers'
// retry windows).
TTL time.Duration
// LockTTL bounds a delivery that is still running (default 1 minute).
LockTTL time.Duration
}
DedupeOptions tune Dedupe.
type StandardOptions ¶ added in v0.22.0
type StandardOptions struct {
// MaxBody bounds the body (default 1 MiB).
MaxBody int64
// Tolerance is how far webhook-timestamp may be from now, in either
// direction (default 5 minutes). It bounds replays of captured requests.
Tolerance time.Duration
// Now is the clock (default time.Now).
Now func() time.Time
}
StandardOptions tune Standard.