docs: add webhook signature verification guide - #1004
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion guide Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
leggetter
left a comment
There was a problem hiding this comment.
Review summary — accurate guide, five minor suggestions inline
I verified the guide's claims against the signing implementation and they all hold:
- Defaults match
internal/destregistry/providers/destwebhook/destwebhook.go: hex HMAC-SHA256, content template{{.Body}}(body only), header templatev0={{.Signatures | join ","}}, prefixx-outpost-, secret templatewhsec_{{.RandomHex}}. - The secret is used verbatim as the HMAC key (
signature.go), so the "don't stripwhsec_or base64-decode" warning is correct and valuable. - Rotation ordering (current secret's signature first) and the comma-joined multi-signature format match
GenerateSignatures. - The timestamp header is RFC 3339 UTC, matching the example.
- Removing "reject old timestamps" from the destination page is a genuine correctness fix — the default signed content is the body only, so the timestamp header is unauthenticated and freshness checks add no replay protection.
The code examples use constant-time comparison in all three languages, read the raw body before parsing, and handle multi-signature headers correctly.
The inline comments are all minor polish: a Python 3.10+ type-hint compatibility nit, a robustness guard for the Node example, linking the rotation-window duration, mentioning per-header name overrides in the Customization section, and an optional nav-placement thought. None are blockers.
Generated by Claude Code
|
|
||
| SIGNATURE_HEADER = "x-outpost-signature" | ||
|
|
||
| def verify_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool: |
There was a problem hiding this comment.
The str | None union syntax requires Python 3.10+. Users on 3.8/3.9 who paste this get a TypeError at import time. Consider Optional[str] (with from typing import Optional) or dropping the annotation to keep the sample maximally copy-pasteable.
Generated by Claude Code
There was a problem hiding this comment.
Both 3.8/3.9 have hit EOL by the end of last year. I assume folks can do their own translation if necessary? Not super opinionated tho, just a thought. What do you think?
|
|
||
| ## Customization | ||
|
|
||
| The scheme above is Outpost's default. Changing `DESTINATIONS_WEBHOOK_HEADER_PREFIX` only renames the headers — this guide stays accurate with the prefix substituted. Customizing the [signature templates, algorithm, or encoding](/docs/outpost/destinations/webhook#signatures) changes the scheme itself, and the resulting deliveries no longer match this guide. |
There was a problem hiding this comment.
Minor completeness point: besides the prefix, individual system header names can also be pinned to a custom name or disabled entirely (e.g. DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_NAME — the destination page documents this for event-id). A brief mention that per-header overrides exist would cover deployments where prefix substitution alone doesn't explain the header names a reader sees.
Generated by Claude Code
There was a problem hiding this comment.
Reworded to be a bit more generic instead of listing all available configuration options.
| "title": "Migrate to Outpost" | ||
| }, | ||
| { | ||
| "slug": "guides/verify-webhook-signatures", |
There was a problem hiding this comment.
Placement is fine as-is, but this is arguably the first guide most event consumers need — worth considering a higher slot in the Guides list.
Generated by Claude Code
There was a problem hiding this comment.
I think the "Migrate to Outpost" is a more important guide. This is the 2nd one already. I personally think it makes sense this way.
leggetter
left a comment
There was a problem hiding this comment.
Reviewed with Claude, reviewed the review, and then got Claude to post some comments.
07f011a to
af144c0
Compare
The Customization section promised the guide "stays accurate with the prefix substituted," which is wrong when an operator pins a header name outright (DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_NAME and friends). State the assumption instead of enumerating knobs: the guide covers default-mode signatures and default header naming, prefix substitution is the one supported deviation, and anything else is the operator's scheme to document. Link now points at Operator Configuration rather than the destination-level Configuration anchor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
af144c0 to
89896d1
Compare
verifySignature returns false for every other "cannot verify" case —
missing header, wrong prefix, no candidate match — but threw on a body
that wasn't bytes, so callers had to wrap it in try/catch to get a
trustworthy boolean.
Reachable through the sample as written: body-parser sets req.body = {}
before its content-type check, so a request that doesn't match
express.raw({ type: "application/json" }) reaches createHmac().update({})
and Express turns the throw into a 500 instead of a 401. Real deliveries
always match — destwebhook hardcodes Content-Type and reserves it from
custom_headers — so this only shows up on stray traffic, but the same
hole opens anywhere the body arrives undefined.
Accept strings as well as Buffers: crypto's update() handles both, and
rejecting strings would turn a valid body shape into a silent
verification failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"A rotation window" left readers no way to know how long they have to swap the stored secret. Name the 24-hour default at first mention and point at the destination page's Secret Rotation section, which already documents the trigger, the sequence, and the field that sets the window. Kept to a parenthetical: the receiving side accepts any matching candidate regardless of the window length, so the duration is context rather than something the verification code acts on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds
guides/verify-webhook-signatures: how default-mode webhook signatures are constructed (hex HMAC-SHA256 over the raw body, secret used as-is including thewhsec_prefix), verification steps, and rotation-safe code examples in Node.js, Python, and Go. The only customization it accounts for is the header prefix; signature template/algorithm/encoding changes are noted as out of scope. Linked from the webhook destination page and added to the Guides nav.The code examples were tested against real deliveries: fixtures generated through
destwebhook's publisher (single secret, rotation, custom prefix, unicode body) all verify with the samples verbatim, and tampered-body/wrong-secret/stripped-whsec_cases correctly fail.Heads-up: the destination page's verification steps drop "reject old timestamps" — the default signature covers only the body, so the timestamp header isn't authenticated; the guide points to event-id dedupe instead.
🤖 Generated with Claude Code