package webhooks import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "log" "net/http" "os" ) type SquareWebhookEvent struct { Type string `json:"type"` EventID string `json:"event_id"` CreatedAt string `json:"created_at"` Data json.RawMessage `json:"data"` LocationID string `json:"location_id"` } func HandleSquareWebhook(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 512*1024) body, err := io.ReadAll(r.Body) if err != nil { log.Printf("Failed to read webhook body: %v", err) http.Error(w, "request body too large or unreadable", http.StatusRequestEntityTooLarge) return } defer r.Body.Close() // TODO(PROD): Replace this dev stub with production webhook verification. // // Square webhook verification requirements (from official docs): // 1. Header: `x-square-hmacsha256-signature` (NOT x-square-signature) // 2. Algorithm: HMAC-SHA256, output is **base64** encoded (not hex) // 3. Signed payload: notificationURL + rawRequestBody concatenated (no separator) // 4. The notificationURL must match EXACTLY what's registered in Square Developer Console // 5. Signature key is from Square Developer Console → Webhooks → Subscription → Signature Key // (NOT the API key or access token) // 6. ALWAYS verify — reject with 403 if missing/invalid // 7. Use timing-safe comparison (hmac.Equal) // // Reference: https://developer.squareup.com/docs/webhooks/step3validate // // For the Go SDK approach: // import "github.com/square/square-go-sdk" // client := square.NewClient() // err := client.Webhooks.VerifySignature(ctx, &square.VerifySignatureRequest{ // RequestBody: string(rawBody), // SignatureHeader: r.Header.Get("x-square-hmacsha256-signature"), // SignatureKey: os.Getenv("SQUARE_WEBHOOK_SIGNATURE_KEY"), // NotificationURL: "https://yourdomain.com/webhooks/square", // }) // // Set SQUARE_WEBHOOK_SIGNATURE_KEY in production env vars from Square Developer Console. // Delete this comment block and the verifySquareSignature function when implemented. signingKey := os.Getenv("SQUARE_WEBHOOK_SIGNATURE_KEY") if signingKey != "" { signature := r.Header.Get("x-square-signature") if signature == "" { log.Printf("Missing Square webhook signature header") http.Error(w, "Invalid signature", http.StatusForbidden) return } if !verifySquareSignature(body, signature, signingKey) { log.Printf("Invalid Square webhook signature") http.Error(w, "Invalid signature", http.StatusForbidden) return } } var event SquareWebhookEvent if err := json.Unmarshal(body, &event); err != nil { log.Printf("Failed to parse webhook event: %v", err) http.Error(w, "Invalid event", http.StatusBadRequest) return } log.Printf("[SQUARE-WEBHOOK] Received event: %s", event.Type) switch event.Type { case "payment.updated": handlePaymentUpdated(event.Data) case "refund.updated": handleRefundUpdated(event.Data) case "dispute.created": log.Printf("[SQUARE-WEBHOOK] Dispute created: %s", event.EventID) default: log.Printf("[SQUARE-WEBHOOK] Unknown event type: %s", event.Type) } w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func verifySquareSignature(body []byte, signature, signingKey string) bool { mac := hmac.New(sha256.New, []byte(signingKey)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected)) } func handlePaymentUpdated(data json.RawMessage) { log.Printf("[SQUARE-WEBHOOK] payment.updated: %s", string(data)) } func handleRefundUpdated(data json.RawMessage) { log.Printf("[SQUARE-WEBHOOK] refund.updated: %s", string(data)) }