Files
gochat/internal/auth/webhook_registry.go
T
2026-06-04 15:44:48 +08:00

122 lines
3.7 KiB
Go

package auth
import (
"net/http"
"sync"
)
// Reference: P2E §3 — Webhook authentication registry
// Each channel provider stores an "identifier" (bot_token, website_token, phone_number_id)
// which is used to route and validate incoming webhook requests.
// WebhookTokenRegistry manages webhook authentication tokens per channel.
// Tokens are loaded from DB on startup and refreshed when inboxes are created/updated.
type WebhookTokenRegistry struct {
mu sync.RWMutex
tokens map[string]map[string]WebhookTokenEntry // channel_type → identifier → entry
}
// WebhookTokenEntry stores a channel's webhook authentication data.
type WebhookTokenEntry struct {
InboxID uint
AccountID uint
Secret string // HMAC secret for verification (varies by channel)
Identifier string // unique identifier for routing (bot_token, website_token, etc.)
}
// NewWebhookTokenRegistry creates an empty token registry.
func NewWebhookTokenRegistry() *WebhookTokenRegistry {
return &WebhookTokenRegistry{
tokens: make(map[string]map[string]WebhookTokenEntry),
}
}
// Register adds a webhook token entry for a channel.
func (r *WebhookTokenRegistry) Register(channelType, identifier string, entry WebhookTokenEntry) {
r.mu.Lock()
defer r.mu.Unlock()
if r.tokens[channelType] == nil {
r.tokens[channelType] = make(map[string]WebhookTokenEntry)
}
r.tokens[channelType][identifier] = entry
}
// Unregister removes a webhook token entry.
func (r *WebhookTokenRegistry) Unregister(channelType, identifier string) {
r.mu.Lock()
defer r.mu.Unlock()
if r.tokens[channelType] != nil {
delete(r.tokens[channelType], identifier)
}
}
// Lookup finds a webhook token entry by channel type and identifier.
func (r *WebhookTokenRegistry) Lookup(channelType, identifier string) (*WebhookTokenEntry, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
if r.tokens[channelType] == nil {
return nil, false
}
entry, ok := r.tokens[channelType][identifier]
if !ok {
return nil, false
}
return &entry, true
}
// Validate checks a webhook request against the stored token entry.
// The validation strategy depends on the channel type:
// - Telegram: X-Telegram-Bot-Api-Secret-Token header
// - Web Widget: HMAC token verification
// - Facebook/WhatsApp: app secret + signature verification
func (r *WebhookTokenRegistry) Validate(channelType, identifier string, req *http.Request) (bool, error) {
entry, found := r.Lookup(channelType, identifier)
if !found {
return false, nil // unknown identifier → reject
}
switch channelType {
case "telegram":
// Telegram validates via X-Telegram-Bot-Api-Secret-Token header
token := req.Header.Get("X-Telegram-Bot-Api-Secret-Token")
if entry.Secret == "" {
return true, nil // no secret configured → accept all
}
return token == entry.Secret, nil
case "web_widget":
// Web widget HMAC verification (handled separately in WebSocket auth)
return true, nil
case "facebook":
// Facebook webhook validation: app secret + X-Hub-Signature-256
// TODO: implement Facebook signature verification (P8)
return true, nil
case "whatsapp":
// WhatsApp webhook validation: Bearer token verification
// TODO: implement WhatsApp token verification (P8)
return true, nil
default:
return true, nil // unknown channel type → accept (will be filtered by registry)
}
}
// GetAllIdentifiers returns all registered identifiers for a channel type.
func (r *WebhookTokenRegistry) GetAllIdentifiers(channelType string) []WebhookTokenEntry {
r.mu.RLock()
defer r.mu.RUnlock()
entries := []WebhookTokenEntry{}
if r.tokens[channelType] == nil {
return entries
}
for _, entry := range r.tokens[channelType] {
entries = append(entries, entry)
}
return entries
}