Documentation
¶
Overview ¶
Package webhook defines the webhook domain entity and its store interface. This is for authsome's own webhook subscriptions. Actual delivery is delegated to Relay via the bridge.EventRelay interface.
Index ¶
- Constants
- Variables
- func SignBody(secret []byte, ts time.Time, body []byte) string
- func SignRelay(secret string, ts time.Time, body []byte) string
- func StoredSecret(w *Webhook) string
- func VerifyRelay(body []byte, secret, timestamp, sig string, tolerance time.Duration) error
- func VerifySignature(secret, body []byte, header string, tolerance time.Duration) error
- type Store
- type Webhook
Constants ¶
const ( RelaySignatureHeader = "X-Relay-Signature" RelayTimestampHeader = "X-Relay-Timestamp" )
RelaySignatureHeader and RelayTimestampHeader are the headers Relay, the delivery service behind authsome webhooks, sets on every delivery. The signature is the same HMAC-SHA256 over "<unix-seconds>.<body>" as X-Authsome-Signature, carried as "v1=<hex>" with the timestamp in its own header.
const DefaultSignatureTolerance = 5 * time.Minute
DefaultSignatureTolerance is the max clock skew allowed between sender and receiver before VerifySignature rejects with ErrTimestampSkew. Stripe uses 5 minutes; we match for familiarity.
const SignatureHeader = "X-Authsome-Signature"
SignatureHeader is the canonical HTTP header carrying the signature envelope on outgoing webhook deliveries.
Format (Stripe-style for operator familiarity):
X-Authsome-Signature: t=<unix-seconds>,v1=<hex(hmac-sha256)>
The HMAC is computed over `<unix-seconds>.<raw-body-bytes>` so the signature also binds the timestamp — an attacker can't replay an old signature with a fresh timestamp without knowing the secret.
const SignatureVersion = "v1"
SignatureVersion is the only currently-supported signature scheme. New schemes (e.g. ed25519) would land as v2 alongside v1 so receivers can roll forward without breaking existing senders.
Variables ¶
var ( ErrMalformedHeader = errors.New("webhook: malformed signature header") ErrUnknownVersion = errors.New("webhook: unknown signature version") ErrTimestampSkew = errors.New("webhook: signature timestamp outside tolerance") ErrSignatureMismatch = errors.New("webhook: signature mismatch") )
Errors returned by VerifySignature. Callers should match with errors.Is so the signaling stays stable across error wrapping.
Functions ¶
func SignBody ¶
SignBody computes the X-Authsome-Signature value for a given timestamp, raw request body, and shared secret. Senders compose this once per delivery and set the header on the outgoing request; receivers reproduce it under their own secret to verify.
The timestamp is included in the header in plaintext AND folded into the HMAC input — this is intentional. A receiver that only MAC'd the body would accept a replayed (body, signature) pair with a fresh timestamp; binding the timestamp closes that gap.
func SignRelay ¶
SignRelay produces the X-Relay-Signature value Relay would put on a delivery of body at ts: "v1=<hex hmac-sha256 over "<ts>.<body>">". The engine uses it for the test delivery it makes before registering a webhook, so the receiver sees exactly what Relay will send.
func StoredSecret ¶
StoredSecret is what a store writes to the secret column: nothing once the webhook delivers through a Relay endpoint (Relay holds the secret), and the plaintext only for a row that has not been adopted yet, so the legacy shape is preserved until adoption rewrites it.
func VerifyRelay ¶
VerifyRelay checks a delivery from Relay: timestamp is the value of X-Relay-Timestamp, sig the value of X-Relay-Signature, secret the webhook's secret as shown when it was created or last rotated. A zero tolerance means DefaultSignatureTolerance.
func VerifySignature ¶
VerifySignature returns nil when header was produced by SignBody using the same secret over the same body within tolerance of now. Pass tolerance=0 to use DefaultSignatureTolerance.
Comparison is constant-time; failures are returned as wrapped sentinel errors so callers can distinguish "stale timestamp" (worth logging at debug, often a clock-drift symptom) from "MAC didn't match" (worth alerting — either a misconfigured secret or an attacker probing).
Types ¶
type Store ¶
type Store interface {
CreateWebhook(ctx context.Context, w *Webhook) error
GetWebhook(ctx context.Context, webhookID id.WebhookID) (*Webhook, error)
UpdateWebhook(ctx context.Context, w *Webhook) error
DeleteWebhook(ctx context.Context, webhookID id.WebhookID) error
ListWebhooks(ctx context.Context, appID id.AppID) ([]*Webhook, error)
}
Store defines the persistence interface for webhook operations.
type Webhook ¶
type Webhook struct {
ID id.WebhookID `json:"id"`
AppID id.AppID `json:"app_id"`
EnvID id.EnvironmentID `json:"env_id"`
URL string `json:"url"`
Events []string `json:"events"`
// Secret is the signing secret in plaintext. It is set on the way out
// of CreateWebhook and RotateWebhookSecret, shown to the caller once,
// and never stored: a row that has a RelayEndpointID reads back with an
// empty Secret. Rows written before webhooks became Relay endpoints
// still carry their plaintext until they are adopted, and read it back
// so adoption can hand the same secret to Relay.
Secret string `json:"-"`
// SecretHash is store.HashToken of the current secret, so a caller can
// check which secret they hold without the store holding it.
SecretHash string `json:"-"`
// RelayEndpointID names the Relay endpoint that delivers for this
// webhook. Empty on a row written before webhooks became endpoints.
RelayEndpointID string `json:"relay_endpoint_id,omitempty"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Webhook represents a registered webhook endpoint.