go 41 lines · 7 steps

Verifying webhook HMAC signatures in Gin

A Gin middleware that recomputes an HMAC-SHA256 over the raw request body and rejects requests whose signature doesn't match.

Explained by highlit
1package middleware
2 
3import (
4 "bytes"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "io"
9 "net/http"
10 
11 "github.com/gin-gonic/gin"
12)
13 
14func VerifyWebhookSignature(secret string) gin.HandlerFunc {
15 return func(c *gin.Context) {
16 body, err := c.GetRawData()
17 if err != nil {
18 c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "cannot read body"})
19 return
20 }
21 
22 c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
23 
24 provided := c.GetHeader("X-Signature-256")
25 if provided == "" {
26 c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing signature"})
27 return
28 }
29 
30 mac := hmac.New(sha256.New, []byte(secret))
31 mac.Write(body)
32 expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
33 
34 if !hmac.Equal([]byte(expected), []byte(provided)) {
35 c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
36 return
37 }
38 
39 c.Next()
40 }
41}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Webhook authenticity comes from an HMAC over the exact raw bytes, so read and verify the body before anything else parses it.
  2. 2Reading the body consumes it — restore it with a fresh reader so downstream handlers can still bind the payload.
  3. 3Compare signatures with a constant-time function like hmac.Equal to avoid leaking information through timing.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Verifying webhook HMAC signatures in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code