package webhooks import ( "crypto/hmac" "crypto/sha1" "crypto/sha256" "crypto/subtle" "encoding/hex" "net/http" "strings" "github.com/OliveTin/OliveTin/internal/config" log "github.com/sirupsen/logrus" ) type AuthVerifier struct { config config.WebhookConfig } func NewAuthVerifier(cfg config.WebhookConfig) *AuthVerifier { return &AuthVerifier{config: cfg} } func (v *AuthVerifier) Verify(r *http.Request, payload []byte) bool { verifier := v.getVerifier() if verifier == nil { return v.handleUnknownAuthType() } return verifier(r, payload) } type authVerifierFunc func(*http.Request, []byte) bool func (v *AuthVerifier) getVerifier() authVerifierFunc { if v.config.AuthType == "" || v.config.AuthType == "none" { return func(_ *http.Request, _ []byte) bool { return true } } verifierMap := v.buildVerifierMap() if verifier, ok := verifierMap[v.config.AuthType]; ok { return verifier } return nil } func (v *AuthVerifier) buildVerifierMap() map[string]authVerifierFunc { return map[string]authVerifierFunc{ "hmac-sha256": v.verifyHMAC256, "hmac-sha1": v.verifyHMAC1, "bearer": func(r *http.Request, _ []byte) bool { return v.verifyBearer(r) }, "basic": func(r *http.Request, _ []byte) bool { return v.verifyBasic(r) }, } } func (v *AuthVerifier) handleUnknownAuthType() bool { log.WithFields(log.Fields{ "authType": v.config.AuthType, }).Warnf("Unknown auth type, rejecting") return false } func (v *AuthVerifier) verifyHMAC256(r *http.Request, payload []byte) bool { if v.config.Secret == "" { log.Warnf("HMAC-SHA256 auth requires secret") return false } headerName := v.config.AuthHeader if headerName == "" { headerName = "X-Webhook-Signature" } signature := r.Header.Get(headerName) if signature == "" { log.Debugf("Missing signature header: %s", headerName) return false } expectedSig := strings.TrimPrefix(signature, "sha256=") mac := hmac.New(sha256.New, []byte(v.config.Secret)) mac.Write(payload) computedSig := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expectedSig), []byte(computedSig)) } func (v *AuthVerifier) verifyHMAC1(r *http.Request, payload []byte) bool { if v.config.Secret == "" { log.Warnf("HMAC-SHA1 auth requires secret") return false } headerName := v.config.AuthHeader if headerName == "" { headerName = "X-Webhook-Signature" } signature := r.Header.Get(headerName) if signature == "" { log.Debugf("Missing signature header: %s", headerName) return false } expectedSig := strings.TrimPrefix(signature, "sha1=") mac := hmac.New(sha1.New, []byte(v.config.Secret)) mac.Write(payload) computedSig := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expectedSig), []byte(computedSig)) } func (v *AuthVerifier) verifyBearer(r *http.Request) bool { if v.config.Secret == "" { log.Warnf("Bearer auth requires secret") return false } authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { log.Debugf("Missing or invalid Bearer token") return false } token := strings.TrimPrefix(authHeader, "Bearer ") tokenBytes := []byte(token) secretBytes := []byte(v.config.Secret) return len(tokenBytes) == len(secretBytes) && subtle.ConstantTimeCompare(tokenBytes, secretBytes) == 1 } func (v *AuthVerifier) verifyBasic(r *http.Request) bool { if v.config.Secret == "" { log.Warnf("Basic auth requires secret") return false } username, password, ok := r.BasicAuth() if !ok { log.Debugf("Missing Basic auth header") return false } return v.verifyBasicCredentials(username, password) } func (v *AuthVerifier) verifyBasicCredentials(username, password string) bool { parts := strings.SplitN(v.config.Secret, ":", 2) if len(parts) == 2 { return v.verifyBasicWithUsername(username, password, parts[0], parts[1]) } return v.verifyBasicPasswordOnly(password) } func (v *AuthVerifier) verifyBasicWithUsername(username, password, expectedUsername, expectedPassword string) bool { usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUsername)) passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) return usernameMatch == 1 && passwordMatch == 1 } func (v *AuthVerifier) verifyBasicPasswordOnly(password string) bool { return subtle.ConstantTimeCompare([]byte(password), []byte(v.config.Secret)) == 1 }