Help us shape the future of Go! Take the 2026 Go Developer Survey

webhook

package
v0.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 14 Imported by: 0

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

Examples

Constants

This section is empty.

Variables

View Source
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

func Middleware(header string, secret []byte, maxBody int64) func(http.Handler) http.Handler

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 Sign

func Sign(secret, payload []byte) string

Sign returns the hex HMAC-SHA256 of payload (for tests and outgoing webhooks).

func Standard added in v0.22.0

func Standard(secret string, o StandardOptions) (func(http.Handler) http.Handler, error)

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

func StandardID(r *http.Request) string

StandardID returns the webhook-id header, the event id for Dedupe with Standard Webhooks providers.

func VerifyHMAC

func VerifyHMAC(secret, payload []byte, signature string) bool

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL