feat(webhook): verify tiktok ingress signatures

This commit is contained in:
2026-06-05 00:29:41 +08:00
parent 959a56bac1
commit 0ea7a081e3
3 changed files with 117 additions and 1 deletions
@@ -6,10 +6,17 @@ package webhook
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gochat/gochat/internal/channel"
tiktokchannel "github.com/gochat/gochat/internal/channel/tiktok"
@@ -50,6 +57,12 @@ func (h *TikTokWebhookHandler) HandleTikTokWebhook(c *gin.Context) {
c.Request.Body.Close()
c.Request.Body = io.NopCloser(bytes.NewReader(body))
if err := verifyTikTokSignature(c.GetHeader("Tiktok-Signature"), body, time.Now()); err != nil {
applogger.L().Warnf("TikTok webhook: signature verification failed: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "signature verification failed"})
return
}
businessID := c.Param("business_id")
if businessID == "" {
businessID = extractTikTokBusinessID(body)
@@ -176,6 +189,50 @@ func extractTikTokBusinessID(body []byte) string {
return ""
}
func verifyTikTokSignature(signatureHeader string, body []byte, now time.Time) error {
clientSecret := os.Getenv("TIKTOK_APP_SECRET")
timestamp, signature := extractTikTokSignatureParts(signatureHeader)
if clientSecret == "" || timestamp == 0 || signature == "" {
return fmt.Errorf("missing tiktok signature credentials")
}
payload := fmt.Sprintf("%d.%s", timestamp, string(body))
mac := hmac.New(sha256.New, []byte(clientSecret))
mac.Write([]byte(payload))
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
return fmt.Errorf("invalid tiktok signature")
}
if now.Unix()-timestamp > 5 {
return fmt.Errorf("stale tiktok signature")
}
return nil
}
func extractTikTokSignatureParts(signatureHeader string) (int64, string) {
if signatureHeader == "" {
return 0, ""
}
var timestamp int64
var signature string
for _, part := range strings.Split(signatureHeader, ",") {
keyValue := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(keyValue) != 2 {
continue
}
switch keyValue[0] {
case "t":
parsed, err := strconv.ParseInt(keyValue[1], 10, 64)
if err == nil {
timestamp = parsed
}
case "s":
signature = keyValue[1]
}
}
return timestamp, signature
}
func tiktokDataString(data map[string]interface{}, key string) string {
if value, ok := data[key].(string); ok {
return value