Files
hanko/backend/webhooks/manager.go
2025-09-25 19:15:20 +02:00

137 lines
3.6 KiB
Go

package webhooks
import (
"fmt"
"github.com/gobuffalo/pop/v6"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/teamhanko/hanko/backend/v2/config"
hankoJwk "github.com/teamhanko/hanko/backend/v2/crypto/jwk"
hankoJwt "github.com/teamhanko/hanko/backend/v2/crypto/jwt"
"github.com/teamhanko/hanko/backend/v2/persistence"
"github.com/teamhanko/hanko/backend/v2/webhooks/events"
"time"
)
type Manager interface {
Trigger(tx *pop.Connection, evt events.Event, data interface{})
GenerateJWT(data interface{}, event events.Event) (string, error)
}
type manager struct {
logger echo.Logger
webhooks Webhooks
jwtGenerator hankoJwt.Generator
audience []string
persister persistence.Persister
canExpireAtTime bool
}
func NewManager(cfg *config.Config, persister persistence.Persister, jwkManager hankoJwk.Manager, logger echo.Logger) (Manager, error) {
hooks := make(Webhooks, 0)
if cfg.Webhooks.Enabled {
for _, cfgHook := range cfg.Webhooks.Hooks {
hooks = append(hooks, NewConfigHook(cfgHook, logger))
}
}
const generateFailureMessage = "failed to create webhook jwt generator: %w"
signatureKey, err := jwkManager.GetSigningKey()
if err != nil {
errMessage := fmt.Errorf(generateFailureMessage, err)
logger.Error(errMessage)
return nil, errMessage
}
verificationKeys, err := jwkManager.GetPublicKeys()
if err != nil {
errMessage := fmt.Errorf(generateFailureMessage, err)
logger.Error(errMessage)
return nil, errMessage
}
g, err := hankoJwt.NewGenerator(signatureKey, verificationKeys)
if err != nil {
errMessage := fmt.Errorf(generateFailureMessage, err)
logger.Error(errMessage)
return nil, errMessage
}
var audience []string
if cfg.Session.Audience != nil && len(cfg.Session.Audience) > 0 {
audience = cfg.Session.Audience
} else {
audience = []string{cfg.Webauthn.RelyingParty.Id}
}
return &manager{
logger: logger,
webhooks: hooks,
jwtGenerator: g,
audience: audience,
persister: persister,
canExpireAtTime: cfg.Webhooks.AllowTimeExpiration,
}, nil
}
func (m *manager) Trigger(tx *pop.Connection, evt events.Event, data interface{}) {
// add db hooks - Done here to prevent a restart in case a hook is added or removed from the database
dbHooks, err := m.persister.GetWebhookPersister(tx).List(false)
if err != nil {
m.logger.Error(fmt.Errorf("unable to get database webhooks: %w", err))
return
}
hooks := m.webhooks
for _, dbHook := range dbHooks {
hooks = append(hooks, NewDatabaseHook(dbHook, m.persister.GetWebhookPersister(nil), m.logger))
}
dataToken, err := m.GenerateJWT(data, evt)
if err != nil {
m.logger.Error(fmt.Errorf("unable to generate JWT for webhook data: %w", err))
return
}
jobData := JobData{
Token: dataToken,
Event: evt,
}
hookChannel := make(chan Job, len(hooks))
for _, hook := range hooks {
if hook.HasEvent(evt) {
job := Job{
Data: jobData,
Hook: hook,
CanExpireAtTime: m.canExpireAtTime,
}
hookChannel <- job
}
}
close(hookChannel)
worker := NewWorker(hookChannel, m.logger)
go worker.Run()
}
func (m *manager) GenerateJWT(data interface{}, event events.Event) (string, error) {
issuedAt := time.Now()
expiration := issuedAt.Add(5 * time.Minute)
token := jwt.New()
_ = token.Set(jwt.SubjectKey, "hanko webhooks")
_ = token.Set(jwt.IssuedAtKey, issuedAt)
_ = token.Set(jwt.ExpirationKey, expiration)
_ = token.Set(jwt.AudienceKey, m.audience)
_ = token.Set("data", data)
_ = token.Set("evt", event)
signed, err := m.jwtGenerator.Sign(token)
if err != nil {
return "", err
}
return string(signed), nil
}