feat(webhook): verify tiktok ingress signatures
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user