webhook

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 27 Imported by: 0

README

webhook

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

webhook is a protocol-independent Go module for exact-byte webhook verification, replay protection, deterministic outbound signing, and bounded delivery. It uses net/http, HMAC-SHA-256 or HMAC-SHA-512, explicit clocks and limits, and no production unsafe or cgo.

The module does not claim support for any vendor preset. The generic v1 scheme is specified by the signature reference and has independently generated Python fixtures in testdata/vectors/v1.json. Protocol ambiguities and application policies are recorded in the specification decision register against the pinned source manifest.

Install

go get github.com/faustbrian/go-webhook

Go 1.26 or newer is required because the optional published outbox adapter requires it.

Receive

verifier, err := webhook.NewVerifier(webhook.VerifierConfig{
    Algorithm: webhook.SHA256,
    Keys: []webhook.VerificationKey{{ID: "2026-07", Secret: secret}},
    Tolerance: 5 * time.Minute,
})
if err != nil { return err }

handler, err := verifier.Middleware(webhook.MiddlewareConfig{
    Request: webhook.RequestOptions{
        MaxBodyBytes: 1 << 20,
        HeaderLimits: webhook.HeaderLimits{MaxSignatures: 2, MaxBytes: 1024},
    },
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        body, _ := webhook.VerifiedBodyFromContext(r.Context())
        _ = body // decode only after verification
        w.WriteHeader(http.StatusNoContent)
}))
if err != nil { return err }

When replay protection is configured, provide an atomic ReplayStore and an event-ID extractor. See the inbound guide.

Send

Construct a Signer, a strict SSRFPolicy, and a Deliverer. A delivery must have an endpoint and event ID. More than one attempt additionally requires an idempotency key. Redirects are never followed by NewSecureHTTPClient.

See the outbound guide and executable examples in example_test.go.

Optional packages integrate idempotency, log, telemetry, queue, and outbox. webhooktest supplies deterministic consumer fixtures.

Guarantees

  • exact received bytes are hashed before application decoding;
  • every signing operation uses a signed, injectable random nonce;
  • signature comparison uses hmac.Equal;
  • malformed or duplicate signature fields are rejected deterministically;
  • replay storage is atomic and fails closed;
  • request, response, header, attempt, DNS, and fan-out work are bounded;
  • endpoint policy is checked before every attempt and again at dial time;
  • observations exclude payloads, signatures, event IDs, keys, and URLs.

Documentation

Development

make check
make safety
make interoperability
make conformance

Security reports follow SECURITY.md. Contributions follow CONTRIBUTING.md. The project is MIT licensed. Dependency attribution is recorded in THIRD_PARTY_NOTICES.md.

Ecosystem

Use the Golib documentation portal to choose companion packages, supported stacks, recipes, and operations guidance.

Documentation

Overview

Package webhook provides protocol-independent webhook signing, verification, replay protection, and delivery primitives.

Index

Examples

Constants

View Source
const IdempotencyHeader = "Idempotency-Key"
View Source
const (
	// SignatureHeader carries one strict structured value per signing key.
	SignatureHeader = "Webhook-Signature"
)

Variables

View Source
var (
	// ErrBodyTooLarge means a request body exceeded its configured hard limit.
	ErrBodyTooLarge = errors.New("webhook body too large")
	// ErrBodyRead means the exact request body could not be captured.
	ErrBodyRead = errors.New("webhook body read failed")
)
View Source
var (
	ErrDeliveryFailed   = errors.New("webhook delivery failed")
	ErrEndpointRejected = errors.New("webhook endpoint rejected")
	ErrResponseTooLarge = errors.New("webhook response too large")
	ErrFanOutLimit      = errors.New("webhook fan-out limit exceeded")
)
View Source
var (
	// ErrMalformedSignatureHeader means signature header syntax was invalid or
	// ambiguous. One malformed value rejects the entire header set.
	ErrMalformedSignatureHeader = errors.New("malformed webhook signature header")
	// ErrSignatureHeadersTooLarge means header count or bytes exceeded limits.
	ErrSignatureHeadersTooLarge = errors.New("webhook signature headers too large")
	// ErrMalformedSignedHeader means a fixed behavior-changing header was
	// duplicated, oversized, or otherwise unsafe to canonicalize.
	ErrMalformedSignedHeader = errors.New("malformed webhook signed header")
)
View Source
var (
	// ErrInvalidSignature means that no supplied signature authenticated.
	ErrInvalidSignature = errors.New("invalid webhook signature")
	// ErrInvalidTimestamp means that a signature timestamp is missing, invalid,
	// or outside the configured tolerance.
	ErrInvalidTimestamp = errors.New("invalid webhook timestamp")
	// ErrInvalidConfiguration means a signer or verifier configuration is not
	// safe to use.
	ErrInvalidConfiguration = errors.New("invalid webhook configuration")
	// ErrNoActiveKey means no configured signing key is active.
	ErrNoActiveKey = errors.New("no active webhook signing key")
	// ErrReplay means an authenticated event ID was already atomically stored.
	ErrReplay = errors.New("webhook replay detected")
	// ErrMissingEventID means replay protection was requested without an ID.
	ErrMissingEventID = errors.New("webhook event ID is required")
	// ErrReplayStore means replay state could not be checked and the request was
	// rejected closed.
	ErrReplayStore = errors.New("webhook replay store unavailable")
	// ErrNonceGeneration means a signer could not produce a safe nonce.
	ErrNonceGeneration = errors.New("webhook nonce generation failed")
)
View Source
var ErrDeliveryEncoding = errors.New("invalid webhook delivery encoding")
View Source
var ErrInvalidEnvelope = errors.New("invalid webhook envelope")

Functions

func Canonicalize

func Canonicalize(message Message, keyID string, algorithm Algorithm) ([]byte, error)

Canonicalize constructs the versioned, unambiguous bytes signed by v1.

func CaptureBody

func CaptureBody(request *http.Request, maxBytes int64) ([]byte, error)

CaptureBody reads at most maxBytes+1 bytes, closes the original body, and restores an independent reader only after a complete successful capture. A declared oversized body is rejected before the first read.

func MarshalDeliveryRequest

func MarshalDeliveryRequest(delivery DeliveryRequest, maxBytes int) ([]byte, error)

MarshalDeliveryRequest emits deterministic v1 bytes for queue and outbox adapters and rejects output beyond maxBytes.

func NewSecureHTTPClient

func NewSecureHTTPClient(policy *SSRFPolicy, timeout time.Duration) (*http.Client, error)

NewSecureHTTPClient creates a direct, no-proxy client whose transport re-resolves and revalidates addresses at dial time. Redirects are returned to the caller without being followed.

func SetSignatureHeaders

func SetSignatureHeaders(header http.Header, signatures []Signature) error

SetSignatureHeaders replaces all signature headers with strict v1 values.

func VerifiedBodyFromContext

func VerifiedBodyFromContext(ctx context.Context) ([]byte, bool)

VerifiedBodyFromContext returns a copy of the exact authenticated body.

Types

type Algorithm

type Algorithm string

Algorithm identifies a supported HMAC construction.

const (
	// SHA256 is HMAC-SHA-256.
	SHA256 Algorithm = "sha256"
	// SHA512 is HMAC-SHA-512.
	SHA512 Algorithm = "sha512"
)

type DeadLetterFunc

type DeadLetterFunc func(ctx context.Context, result DeliveryResult) error

DeadLetterFunc receives a terminal bounded result for durable handling by the application. The core does not implement storage or a queue.

type Deliverer

type Deliverer struct {
	// contains filtered or unexported fields
}

Deliverer signs and sends bounded webhook attempts.

func NewDeliverer

func NewDeliverer(config DeliveryConfig) (*Deliverer, error)

NewDeliverer validates every mandatory safety bound and injectable.

func (*Deliverer) Deliver

func (d *Deliverer) Deliver(ctx context.Context, delivery DeliveryRequest) (DeliveryResult, error)

Deliver performs bounded attempts. Retries are disabled for requests that do not carry an explicit idempotency key.

Example
package main

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"net/netip"
	"net/url"
	"time"

	webhook "github.com/faustbrian/go-webhook"
)

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.WriteHeader(http.StatusNoContent)
	}))
	defer server.Close()
	endpoint, _ := url.Parse(server.URL)
	prefix := netip.MustParsePrefix("127.0.0.0/8")
	policy, _ := webhook.NewSSRFPolicy(webhook.SSRFPolicyConfig{
		Resolver:  net.DefaultResolver,
		AllowHTTP: true, AllowedPrefixes: []netip.Prefix{prefix}, MaxAddresses: 8,
	})
	client, _ := webhook.NewSecureHTTPClient(policy, time.Second)
	signer, _ := webhook.NewSigner(webhook.SignerConfig{
		Algorithm: webhook.SHA256,
		Keys:      []webhook.SigningKey{{ID: "current", Secret: []byte("example-secret")}},
	})
	identifier := 0
	deliverer, _ := webhook.NewDeliverer(webhook.DeliveryConfig{
		Client: client, Signer: signer, EndpointPolicy: policy,
		Retry: webhook.RetryPolicy{MaxAttempts: 1},
		IDGenerator: func() (string, error) {
			identifier++
			return fmt.Sprintf("id-%d", identifier), nil
		},
		MaxRequestBytes: 64, MaxResponseBytes: 64, MaxFanOut: 1,
		HeaderLimits: webhook.HeaderLimits{MaxSignatures: 1, MaxBytes: 512},
	})
	result, _ := deliverer.Deliver(context.Background(), webhook.DeliveryRequest{
		Endpoint: endpoint, Body: []byte("hello"), EventID: "event-1",
	})
	fmt.Println(result.Attempts[0].StatusCode, len(result.Attempts))
}
Output:
204 1

func (*Deliverer) DeliverOnce

func (d *Deliverer) DeliverOnce(ctx context.Context, delivery DeliveryRequest) (DeliveryResult, error)

DeliverOnce performs exactly one HTTP attempt even when the Deliverer retry policy allows more. Queue and outbox consumers use this to avoid nested retry multiplication.

func (*Deliverer) FanOut

func (d *Deliverer) FanOut(ctx context.Context, deliveries []DeliveryRequest, workers int) ([]FanOutResult, error)

FanOut runs deliveries with a bounded worker pool and no durable queue.

func (*Deliverer) Replay

func (d *Deliverer) Replay(
	ctx context.Context,
	originalDeliveryID string,
	delivery DeliveryRequest,
) (DeliveryResult, error)

Replay audits operator intent before starting a new independently identified delivery. It does not mutate or reuse the original attempt record.

type DeliveryAttempt

type DeliveryAttempt struct {
	ID             string
	Number         int
	StartedAt      time.Time
	CompletedAt    time.Time
	StatusCode     int
	Classification FailureClassification
	RetryAfter     time.Duration
	Diagnostic     string
}

DeliveryAttempt records one actual HTTP attempt without payloads, secrets, signatures, endpoint query strings, or sensitive response headers.

type DeliveryConfig

type DeliveryConfig struct {
	Client           HTTPDoer
	Signer           *Signer
	EndpointPolicy   EndpointPolicy
	Retry            RetryPolicy
	Clock            func() time.Time
	Sleep            SleepFunc
	IDGenerator      func() (string, error)
	MaxRequestBytes  int64
	MaxResponseBytes int64
	MaxFanOut        int
	HeaderLimits     HeaderLimits
	DeadLetter       DeadLetterFunc
	ReplayHook       ReplayHook
	Observer         Observer
}

DeliveryConfig configures a bounded deliverer.

type DeliveryRequest

type DeliveryRequest struct {
	Endpoint       *url.URL
	Body           []byte
	EventID        string
	IdempotencyKey string
	Headers        http.Header
	Metadata       map[string]string
}

DeliveryRequest describes one endpoint delivery.

func UnmarshalDeliveryRequest

func UnmarshalDeliveryRequest(encoded []byte, maxBytes int) (DeliveryRequest, error)

UnmarshalDeliveryRequest bounds input before strict decoding and rejects unknown fields, trailing data, unsafe URLs, and incomplete identities.

type DeliveryResult

type DeliveryResult struct {
	ID           string
	EventID      string
	Attempts     []DeliveryAttempt
	ResponseBody []byte
}

DeliveryResult contains bounded delivery evidence.

type EndpointPolicy

type EndpointPolicy interface {
	Validate(ctx context.Context, endpoint *url.URL) error
}

EndpointPolicy validates an endpoint immediately before every attempt.

type EndpointPolicyFunc

type EndpointPolicyFunc func(ctx context.Context, endpoint *url.URL) error

EndpointPolicyFunc adapts a function into an endpoint policy.

func (EndpointPolicyFunc) Validate

func (f EndpointPolicyFunc) Validate(ctx context.Context, endpoint *url.URL) error

Validate implements EndpointPolicy.

type Envelope

type Envelope struct {
	ID       string
	Type     string
	Source   string
	Subject  string
	Time     time.Time
	Data     json.RawMessage
	Metadata map[string]string
}

Envelope is a small protocol-independent event envelope. Its v1 JSON field order and UTC timestamp representation are compatibility-sensitive.

func (Envelope) MarshalJSON

func (e Envelope) MarshalJSON() ([]byte, error)

MarshalJSON validates required fields and emits deterministic JSON.

type ErrorHook

type ErrorHook func(ctx context.Context, err error)

ErrorHook receives internal verification failures. Implementations must not log request bodies, signatures, secrets, or unredacted sensitive headers.

type EventIDExtractor

type EventIDExtractor func(request *http.Request, body []byte) (string, error)

EventIDExtractor extracts a replay identifier after authentication.

func HeaderEventID

func HeaderEventID(name string, maxBytes int) EventIDExtractor

HeaderEventID returns a strict single-header extractor with a byte limit.

type FailureClassification

type FailureClassification string

FailureClassification is a stable delivery outcome category.

const (
	FailureNone      FailureClassification = "none"
	FailureRetryable FailureClassification = "retryable"
	FailureTerminal  FailureClassification = "terminal"
	FailureExhausted FailureClassification = "exhausted"
)

type FanOutResult

type FanOutResult struct {
	Result DeliveryResult
	Err    error
}

FanOutResult preserves the input order of a bounded fan-out operation.

type HTTPDoer

type HTTPDoer interface {
	Do(request *http.Request) (*http.Response, error)
}

HTTPDoer is implemented by http.Client and compatible client wrappers.

type HeaderLimits

type HeaderLimits struct {
	MaxSignatures int
	MaxBytes      int
}

HeaderLimits bounds parsing before any base64 decoding or allocation based on decoded values.

type Message

type Message struct {
	Timestamp      time.Time
	Nonce          string
	Method         string
	Path           string
	RawQuery       string
	Host           string
	ContentType    string
	IdempotencyKey string
	Body           []byte
	Metadata       map[string]string
}

Message contains the exact request components covered by a signature. Body is never transformed before hashing.

type MiddlewareConfig

type MiddlewareConfig struct {
	Request       RequestOptions
	FailureStatus int
	OnError       ErrorHook
}

MiddlewareConfig configures inbound verification middleware.

type NetIPResolver

type NetIPResolver interface {
	LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
}

NetIPResolver is implemented by net.Resolver and controlled test resolvers.

type NonceGenerator

type NonceGenerator func() (string, error)

NonceGenerator returns a fresh public nonce for one signing operation. Implementations must be concurrency-safe.

type Observation

type Observation struct {
	Operation      Operation
	Outcome        Outcome
	Reason         Reason
	Duration       time.Duration
	Algorithm      Algorithm
	StatusCode     int
	Attempt        int
	Classification FailureClassification
}

Observation intentionally has no payload, signature, key, event ID, endpoint, header, query, or arbitrary error field.

type Observer

type Observer interface {
	Observe(ctx context.Context, observation Observation)
}

Observer receives secret-safe observations.

Example
package main

import (
	"context"
	"fmt"

	webhook "github.com/faustbrian/go-webhook"
)

func main() {
	observer := webhook.ObserverFunc(func(_ context.Context, event webhook.Observation) {
		fmt.Println(event.Operation, event.Outcome)
	})
	observer.Observe(context.Background(), webhook.Observation{
		Operation: webhook.OperationVerification, Outcome: webhook.OutcomeSuccess,
	})
}
Output:
verification success

type ObserverFunc

type ObserverFunc func(ctx context.Context, observation Observation)

ObserverFunc adapts a function into an Observer.

func (ObserverFunc) Observe

func (f ObserverFunc) Observe(ctx context.Context, observation Observation)

Observe implements Observer.

type Operation

type Operation string

Operation is a low-cardinality observed operation.

const (
	OperationVerification    Operation = "verification"
	OperationReplay          Operation = "replay"
	OperationDeliveryAttempt Operation = "delivery_attempt"
)

type Outcome

type Outcome string

Outcome is a low-cardinality observed result.

const (
	OutcomeSuccess  Outcome = "success"
	OutcomeRejected Outcome = "rejected"
	OutcomeRetry    Outcome = "retry"
	OutcomeFailure  Outcome = "failure"
)

type Reason

type Reason string

Reason is a stable failure category that cannot contain caller data.

const (
	ReasonNone      Reason = "none"
	ReasonSignature Reason = "signature"
	ReasonTimestamp Reason = "timestamp"
	ReasonReplay    Reason = "replay"
	ReasonStore     Reason = "store"
	ReasonLimit     Reason = "limit"
	ReasonCanceled  Reason = "canceled"
	ReasonTransport Reason = "transport"
	ReasonStatus    Reason = "status"
	ReasonPolicy    Reason = "policy"
	ReasonInternal  Reason = "internal"
)

type ReplayHook

type ReplayHook func(ctx context.Context, originalDeliveryID, eventID string) error

ReplayHook records an operator-requested replay before a new delivery starts.

type ReplayStore

type ReplayStore interface {
	CheckAndRecord(ctx context.Context, key string, expiresAt time.Time) (recorded bool, err error)
}

ReplayStore atomically checks and records replay keys. Implementations MUST return true only when the key was absent and was recorded with expiresAt in the same atomic operation. false, nil means the key already existed.

type RequestOptions

type RequestOptions struct {
	MaxBodyBytes int64
	HeaderLimits HeaderLimits
	Metadata     map[string]string
	EventID      EventIDExtractor
}

RequestOptions controls bounded HTTP signing and verification.

type RetryPolicy

type RetryPolicy struct {
	MaxAttempts int
	BaseDelay   time.Duration
	MaxDelay    time.Duration
}

RetryPolicy bounds attempts and exponential backoff.

func (RetryPolicy) Delay

func (p RetryPolicy) Delay(attempt int, now time.Time, retryAfter string) time.Duration

Delay returns a Retry-After delay when valid, otherwise exponential backoff.

type SSRFPolicy

type SSRFPolicy struct {
	// contains filtered or unexported fields
}

SSRFPolicy validates URL syntax and every resolved address.

func NewSSRFPolicy

func NewSSRFPolicy(config SSRFPolicyConfig) (*SSRFPolicy, error)

NewSSRFPolicy constructs a policy with explicit resolver and DNS bounds.

func (*SSRFPolicy) Validate

func (p *SSRFPolicy) Validate(ctx context.Context, endpoint *url.URL) error

Validate implements EndpointPolicy and resolves hostnames immediately.

type SSRFPolicyConfig

type SSRFPolicyConfig struct {
	Resolver        NetIPResolver
	AllowHTTP       bool
	AllowedPrefixes []netip.Prefix
	DeniedPrefixes  []netip.Prefix
	MaxAddresses    int
}

SSRFPolicyConfig configures default-deny endpoint validation. AllowedPrefixes are explicit exceptions for private test or trusted network endpoints.

type Signature

type Signature struct {
	Version   string
	Algorithm Algorithm
	KeyID     string
	Timestamp time.Time
	Nonce     string
	Value     string
}

Signature is a transport-neutral signature value.

func ParseSignatureHeaders

func ParseSignatureHeaders(header http.Header, limits HeaderLimits) ([]Signature, error)

ParseSignatureHeaders parses all values strictly. Comma-combined values, duplicate key IDs, noncanonical timestamps, and unknown fields are rejected.

type Signer

type Signer struct {
	// contains filtered or unexported fields
}

Signer creates signatures for every active key, newest first.

Example (Rotation)
package main

import (
	"fmt"
	"net/http"
	"time"

	webhook "github.com/faustbrian/go-webhook"
)

func main() {
	now := time.Unix(1_700_000_000, 0)
	signer, _ := webhook.NewSigner(webhook.SignerConfig{
		Algorithm: webhook.SHA512,
		Keys: []webhook.SigningKey{
			{ID: "new", Secret: []byte("new-secret"), NotBefore: now.Add(-time.Hour)},
			{ID: "old", Secret: []byte("old-secret"), NotAfter: now.Add(time.Hour)},
		},
		Clock: func() time.Time { return now },
	})
	signatures, _ := signer.Sign(webhook.Message{Timestamp: now, Method: http.MethodPost, Path: "/hooks", Host: "receiver.example"})
	fmt.Println(len(signatures))
}
Output:
2

func NewSigner

func NewSigner(config SignerConfig) (*Signer, error)

NewSigner validates and copies signer configuration.

func (*Signer) Sign

func (s *Signer) Sign(message Message) ([]Signature, error)

Sign creates one signature for each active key.

func (*Signer) SignRequest

func (s *Signer) SignRequest(request *http.Request, options RequestOptions) ([]Signature, []byte, error)

SignRequest captures and restores the exact body, signs the effective request target, and replaces any preexisting signature headers.

type SignerConfig

type SignerConfig struct {
	Algorithm      Algorithm
	Keys           []SigningKey
	Clock          func() time.Time
	NonceGenerator NonceGenerator
}

SignerConfig configures outbound signatures.

type SigningKey

type SigningKey struct {
	ID        string
	Secret    []byte
	NotBefore time.Time
	NotAfter  time.Time
	Revoked   bool
}

SigningKey is a versioned outbound secret. Empty validity bounds are open.

type SleepFunc

type SleepFunc func(ctx context.Context, duration time.Duration) error

SleepFunc provides cancellable, injectable delivery backoff.

type Verification

type Verification struct {
	KeyID     string
	Algorithm Algorithm
	Timestamp time.Time
	Nonce     string
}

Verification describes the key and algorithm that authenticated a message.

func VerificationFromContext

func VerificationFromContext(ctx context.Context) (Verification, bool)

VerificationFromContext returns the authenticated verification result.

type VerificationError

type VerificationError struct {
	Kind       error
	Diagnostic string
}

VerificationError separates a stable caller-facing message from internal diagnostics. Diagnostic must only be sent to a secret-safe internal sink.

func (*VerificationError) Error

func (e *VerificationError) Error() string

Error implements error without exposing internal diagnostic details.

func (*VerificationError) SafeMessage

func (e *VerificationError) SafeMessage() string

SafeMessage returns a stable response suitable for an untrusted caller.

func (*VerificationError) Unwrap

func (e *VerificationError) Unwrap() error

Unwrap enables errors.Is checks against the failure category.

type VerificationKey

type VerificationKey struct {
	ID        string
	Secret    []byte
	NotBefore time.Time
	NotAfter  time.Time
	Revoked   bool
}

VerificationKey is a versioned inbound secret. Empty validity bounds are open.

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier authenticates signatures with bounded clock skew.

func NewVerifier

func NewVerifier(config VerifierConfig) (*Verifier, error)

NewVerifier validates and copies verifier configuration.

func (*Verifier) Middleware

func (v *Verifier) Middleware(config MiddlewareConfig, next http.Handler) (http.Handler, error)

Middleware authenticates and replay-checks a request before invoking next. Failure responses always contain only the stable safe message.

Example
package main

import (
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	webhook "github.com/faustbrian/go-webhook"
)

func main() {
	now := time.Unix(1_700_000_000, 0)
	secret := []byte("example-secret-at-least-rotate-in-production")
	signer, _ := webhook.NewSigner(webhook.SignerConfig{
		Algorithm: webhook.SHA256,
		Keys:      []webhook.SigningKey{{ID: "current", Secret: secret}},
		Clock:     func() time.Time { return now },
	})
	verifier, _ := webhook.NewVerifier(webhook.VerifierConfig{
		Algorithm: webhook.SHA256,
		Keys:      []webhook.VerificationKey{{ID: "current", Secret: secret}},
		Clock:     func() time.Time { return now }, Tolerance: time.Minute,
	})
	next := http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
		body, _ := webhook.VerifiedBodyFromContext(request.Context())
		fmt.Println(string(body))
	})
	handler, _ := verifier.Middleware(webhook.MiddlewareConfig{Request: webhook.RequestOptions{
		MaxBodyBytes: 64, HeaderLimits: webhook.HeaderLimits{MaxSignatures: 1, MaxBytes: 512},
	}}, next)
	request := httptest.NewRequest(http.MethodPost, "https://receiver.example/hooks", bytes.NewBufferString("hello"))
	_, _, _ = signer.SignRequest(request, webhook.RequestOptions{MaxBodyBytes: 64, HeaderLimits: webhook.HeaderLimits{MaxSignatures: 1, MaxBytes: 512}})
	handler.ServeHTTP(httptest.NewRecorder(), request)
}
Output:
hello

func (*Verifier) Verify

func (v *Verifier) Verify(message Message, signatures []Signature) (Verification, error)

Verify accepts the first valid signature without exposing which checks failed. All signature comparisons use hmac.Equal.

func (*Verifier) VerifyAndRecord

func (v *Verifier) VerifyAndRecord(
	ctx context.Context,
	message Message,
	signatures []Signature,
	eventID string,
) (Verification, error)

VerifyAndRecord authenticates a message and then uses the replay store's atomic check-and-record operation. It fails closed on every storage error.

func (*Verifier) VerifyRequest

func (v *Verifier) VerifyRequest(
	ctx context.Context,
	request *http.Request,
	options RequestOptions,
) (verification Verification, body []byte, err error)

VerifyRequest bounds and parses headers before capturing the exact body. It authenticates before invoking the event-ID extractor or replay store.

type VerifierConfig

type VerifierConfig struct {
	Algorithm       Algorithm
	Keys            []VerificationKey
	Clock           func() time.Time
	Tolerance       time.Duration
	ReplayStore     ReplayStore
	ReplayTTL       time.Duration
	ReplayNamespace string
	Observer        Observer
}

VerifierConfig configures inbound signature verification.

Directories

Path Synopsis
adapters
idempotency
Package webhookidempotency adapts idempotency leases to webhook replay checks.
Package webhookidempotency adapts idempotency leases to webhook replay checks.
otel
Package webhookotel adapts secret-safe webhook observations to the telemetry runtime.
Package webhookotel adapts secret-safe webhook observations to the telemetry runtime.
outbox
Package webhookoutbox adapts webhook deliveries to outbox envelopes and relay publishers.
Package webhookoutbox adapts webhook deliveries to outbox envelopes and relay publishers.
queue
Package webhookqueue adapts bounded delivery requests to queue messages.
Package webhookqueue adapts bounded delivery requests to queue messages.
slog
Package webhookslog adapts secret-safe webhook observations to log's standard slog surface.
Package webhookslog adapts secret-safe webhook observations to log's standard slog surface.
Package webhooktest provides deterministic clocks, nonces, identifiers, signers, and verifiers for consumer tests.
Package webhooktest provides deterministic clocks, nonces, identifiers, signers, and verifiers for consumer tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL