Module: Axn::Webhooks::Verifiers::StandardWebhooks
- Defined in:
- lib/axn/webhooks/verifiers/standard_webhooks.rb
Overview
Standard Webhooks (Svix) scheme. Secret is whsec_<base64>; the signed string is
id.timestamp.body; the signature header holds space-separated v1,<base64sig>
candidates; a ±tolerance replay window applies.
Class Method Summary collapse
-
.decode_secret(secret) ⇒ Object
NOT
secret.to_s(Codex round-4 finding): coercing here is what turned a nil secret — an unset ENV var, or aheader(...)resolver on an absent header — into "", which is valid Base64 and decodes to an EMPTY HMAC key. -
.describe_secret(secret) ⇒ Object
Describes a rejected secret's SHAPE for an error message, never its bytes: this can be raised per delivery attempt on the outbound side, and would otherwise flow the live signing credential into whatever Axn.config.on_exception is wired to.
-
.extract_v1(header) ⇒ Object
Keep only
v1,<sig>candidates, stripped to the bare base64 signature. - .invalid_secret_message(declaration, secret) ⇒ Object
-
.require_secret_key!(secret) ⇒ Object
The raw key for a secret resolved at REQUEST time, raising if it isn't usable.
-
.secret_key(secret) ⇒ Object
The raw HMAC key behind a
whsec_<base64>secret, or nil if the value isn't one.
Class Method Details
.decode_secret(secret) ⇒ Object
NOT secret.to_s (Codex round-4 finding): coercing here is what turned a nil secret —
an unset ENV var, or a header(...) resolver on an absent header — into "", which is
valid Base64 and decodes to an EMPTY HMAC key. That is an authentication bypass, since
anyone who knows the credential is missing can sign with the empty key. Callers must hand
this a String; secret_key guards the type, and require_secret_key! is the safe entry
point for anything resolved at request time.
20 21 22 23 24 |
# File 'lib/axn/webhooks/verifiers/standard_webhooks.rb', line 20 def decode_secret(secret) raise ArgumentError, "secret must be a String (got #{secret.class})" unless secret.is_a?(String) Base64.strict_decode64(secret.delete_prefix("whsec_")) end |
.describe_secret(secret) ⇒ Object
Describes a rejected secret's SHAPE for an error message, never its bytes: this can be raised per delivery attempt on the outbound side, and would otherwise flow the live signing credential into whatever Axn.config.on_exception is wired to.
54 55 56 57 58 59 |
# File 'lib/axn/webhooks/verifiers/standard_webhooks.rb', line 54 def describe_secret(secret) return secret.class.name unless secret.is_a?(String) return "a #{secret.length}-char String not prefixed with whsec_" unless secret.start_with?("whsec_") "a whsec_-prefixed String that failed to decode" end |
.extract_v1(header) ⇒ Object
Keep only v1,<sig> candidates, stripped to the bare base64 signature.
Done here (not via Signature's generic splitter) because that splitter treats
the comma as a separator and would break v1,<sig> into two tokens.
68 69 70 |
# File 'lib/axn/webhooks/verifiers/standard_webhooks.rb', line 68 def extract_v1(header) header.to_s.split(/\s+/).select { |t| t.start_with?("v1,") }.map { |t| t.delete_prefix("v1,") } end |
.invalid_secret_message(declaration, secret) ⇒ Object
61 62 63 |
# File 'lib/axn/webhooks/verifiers/standard_webhooks.rb', line 61 def (declaration, secret) "#{declaration} secret must be a whsec_<base64> value (got #{describe_secret(secret)})" end |
.require_secret_key!(secret) ⇒ Object
The raw key for a secret resolved at REQUEST time, raising if it isn't usable. Loud on purpose: a secret that has gone missing is a misconfiguration worth paging on, and must never degrade into a quiet :signature_mismatch that reads like a rotated key.
29 30 31 |
# File 'lib/axn/webhooks/verifiers/standard_webhooks.rb', line 29 def require_secret_key!(secret) secret_key(secret) || raise(Axn::Webhooks::Error, ("verify :standard_webhooks", secret)) end |
.secret_key(secret) ⇒ Object
The raw HMAC key behind a whsec_<base64> secret, or nil if the value isn't one.
The single source of truth for "is this a usable Standard Webhooks secret", shared by
inbound's declaration-time check and outbound's (Outbound::Signer), so the two can't drift.
The whsec_ prefix check is what carries this: an unprefixed secret is very often still
VALID Base64 — a 32-char hex secret is, and that's a common shape — so it would decode
silently to the wrong key rather than raising. The rescue is scoped to the decode alone;
a caller that RESOLVES a secret (from a callable or a secret store) must do so outside
this method, or its own ArgumentError would be swallowed and rewritten.
42 43 44 45 46 47 48 49 |
# File 'lib/axn/webhooks/verifiers/standard_webhooks.rb', line 42 def secret_key(secret) return nil unless secret.is_a?(String) && secret.start_with?("whsec_") key = decode_secret(secret) key.empty? ? nil : key rescue ArgumentError nil end |