go 37 lines · 1 tab

Webhook signature verification with HMAC (timing-safe compare)

Leah Thompson Jan 2026
1 tab
package webhooks

import (
  "bytes"
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "errors"
  "io"
  "net/http"
)

func Verify(secret []byte, r *http.Request) ([]byte, error) {
  sigHex := r.Header.Get("X-Signature-SHA256")
  if sigHex == "" {
    return nil, errors.New("missing signature")
  }
  sig, err := hex.DecodeString(sigHex)
  if err != nil {
    return nil, errors.New("bad signature encoding")
  }

  body, err := io.ReadAll(r.Body)
  if err != nil {
    return nil, err
  }
  _ = r.Body.Close()
  r.Body = io.NopCloser(bytes.NewReader(body))

  mac := hmac.New(sha256.New, secret)
  mac.Write(body)
  expected := mac.Sum(nil)
  if !hmac.Equal(sig, expected) {
    return nil, errors.New("invalid signature")
  }
  return body, nil
}
1 file · go Explain with highlit

Webhook endpoints should assume the internet is hostile. I verify the request with an HMAC signature derived from the raw body and a shared secret, and I use hmac.Equal to avoid timing leaks. The key detail is reading the body exactly once: the server must compute the signature over the same bytes the client signed, so I read r.Body into a buffer, validate, then replace r.Body with a new reader if downstream code needs it. I also enforce a short clock skew window using an X-Timestamp header to reduce replay risk, and I log only the event_id, not the raw payload. This pattern turns “anyone can POST JSON” into an authenticated integration boundary.


Related snips

Share this code

Here's the card — post it anywhere.

Webhook signature verification with HMAC (timing-safe compare) — share card
Link copied