Verify a Signed Webhook
Webhook signatures are normally calculated from the exact bytes sent by the
provider. Read those bytes once, verify them with crypto/hmac, then decode the
same payload.
mkdir zinc-webhookcd zinc-webhookgo mod init example.com/zinc-webhookgo get github.com/0mjs/zincApplication
Section titled “Application”package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "log" "strings"
"github.com/0mjs/zinc" "github.com/0mjs/zinc/middleware")
type event struct { ID string `json:"id"` Type string `json:"type"`}
func verify(body []byte, header, secret string) bool { signature := strings.TrimPrefix(header, "sha256=") provided, err := hex.DecodeString(signature) if err != nil { return false }
mac := hmac.New(sha256.New, []byte(secret)) _, _ = mac.Write(body) return hmac.Equal(provided, mac.Sum(nil))}
func main() { const secret = "replace-me-from-your-environment"
app := zinc.New() app.Post("/webhooks/billing", middleware.BodyLimit(1<<20), func(c *zinc.Context) error { body, err := c.BodyBytes() if err != nil { return zinc.ErrBadRequest.WithMessage("could not read webhook body") }
if !verify(body, c.GetHeader("X-Hub-Signature-256"), secret) { return zinc.ErrUnauthorized.WithMessage("invalid webhook signature") }
var incoming event if err := json.Unmarshal(body, &incoming); err != nil { return zinc.ErrBadRequest.WithMessage("invalid webhook payload") }
log.Printf("accepted webhook %s (%s)", incoming.ID, incoming.Type) return c.Status(zinc.StatusAccepted).JSON(zinc.Map{"accepted": true}) }, )
log.Fatal(app.Listen(":8080"))}Generate a local signature
Section titled “Generate a local signature”body='{"id":"evt_123","type":"invoice.paid"}'signature=$(printf %s "$body" | openssl dgst -sha256 -hmac 'replace-me-from-your-environment' -hex | sed 's/^.* //')
curl -i http://localhost:8080/webhooks/billing \ -H 'Content-Type: application/json' \ -H "X-Hub-Signature-256: sha256=$signature" \ --data "$body"Production notes
Section titled “Production notes”- Read the secret from your deployment environment; never commit it.
- Keep the body limit close to the provider’s documented maximum.
- Store processed event IDs so provider retries are idempotent.
- Return quickly and move slow work to a queue or Zinc job.
