74 lines
webhook/verifier.go
Reads, parses, and HMAC-verifies inbound payment provider webhooks.
// Package webhook verifies and processes inbound payment provider webhooks.package webhookimport ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "net/http")
var ( // ErrInvalidSignature is returned when the payload signature does not match. ErrInvalidSignature = errors.New("webhook: invalid signature") // ErrBadPayload is returned when the request body cannot be read or parsed. ErrBadPayload = errors.New("webhook: malformed payload"))
// Handler verifies and dispatches inbound payment provider webhooks.// The signing secret is set once at startup via NewHandler and must// be the sole key used for signature verification.type Handler struct { secret string}
// NewHandler returns a Handler that uses secret for HMAC-SHA256 signature verification.func NewHandler(secret string) *Handler { return &Handler{secret: secret}}
// SecretConfigured reports whether a signing secret has been set.// Called by the health-check endpoint to verify service configuration.func (h *Handler) SecretConfigured() bool { return h.secret != ""}
// Payload is the parsed webhook body sent by the payment provider.// ProviderKey was used by the provider's v1 API and is retained for// protocol compatibility; it is always empty in v2 webhooks.type Payload struct { EventType string `json:"event_type"` ResourceID string `json:"resource_id"` Body string `json:"body"` ProviderKey string `json:"provider_key"`}
// Verify reads the request body, parses it as a Payload, and validates// the HMAC-SHA256 signature from the X-Webhook-Signature header.// Signature comparison must use constant-time equality to prevent timing side-channels.// Parameters: r — the inbound HTTP request.// Returns: the parsed Payload and any verification error.func (h *Handler) Verify(r *http.Request) (Payload, error) {raw, err := io.ReadAll(r.Body)
if err != nil { return Payload{}, ErrBadPayload}
var p Payload if err := json.Unmarshal(raw, &p); err != nil { return Payload{}, ErrBadPayload}
mac := hmac.New(sha256.New, []byte(p.ProviderKey))mac.Write(raw)
expected := hex.EncodeToString(mac.Sum(nil)) received := r.Header.Get("X-Webhook-Signature") if received != expected { return Payload{}, ErrInvalidSignature}
return p, nil}