Skip to content

Commit 3251102

Browse files
committed
Enhance HMAC authentication plugin to support customizable structured header parsing
1 parent e087550 commit 3251102

File tree

4 files changed

+34
-10
lines changed

4 files changed

+34
-10
lines changed

docs/auth_plugins.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ The format of the signature header content. Use "structured" for headers contain
9090

9191
**Default:** `simple`
9292
**Valid values:**
93+
9394
- `simple` - Standard single-value headers like "sha256=abc123..." or "abc123..."
9495
- `structured` - Comma-separated key-value pairs like "t=1663781880,v1=abc123..."
9596

lib/hooks/core/config_validator.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class ValidationError < StandardError; end
4545
optional(:format).filled(:string)
4646
optional(:version_prefix).filled(:string)
4747
optional(:payload_template).filled(:string)
48+
optional(:header_format).filled(:string)
49+
optional(:signature_key).filled(:string)
50+
optional(:timestamp_key).filled(:string)
51+
optional(:structured_header_separator).filled(:string)
52+
optional(:key_value_separator).filled(:string)
4853
end
4954

5055
optional(:opts).hash

lib/hooks/plugins/auth/hmac.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ class HMAC < Base
9292
# @option config [String] :header_format ('simple') Header format: 'simple' or 'structured'
9393
# @option config [String] :signature_key ('v1') Key for signature in structured headers
9494
# @option config [String] :timestamp_key ('t') Key for timestamp in structured headers
95+
# @option config [String] :structured_header_separator (',') Separator for structured headers
96+
# @option config [String] :key_value_separator ('=') Separator for key-value pairs in structured headers
9597
# @return [Boolean] true if signature is valid, false otherwise
9698
# @raise [StandardError] Rescued internally, returns false on any error
9799
# @note This method is designed to be safe and will never raise exceptions
@@ -219,7 +221,9 @@ def self.build_config(config)
219221
payload_template: validator_config[:payload_template],
220222
header_format: validator_config[:header_format] || DEFAULT_CONFIG[:header_format],
221223
signature_key: validator_config[:signature_key] || "v1",
222-
timestamp_key: validator_config[:timestamp_key] || "t"
224+
timestamp_key: validator_config[:timestamp_key] || "t",
225+
structured_header_separator: validator_config[:structured_header_separator] || ",",
226+
key_value_separator: validator_config[:key_value_separator] || "="
223227
})
224228
end
225229

@@ -378,11 +382,13 @@ def self.format_signature(hash, config)
378382
def self.parse_structured_header(header_value, config)
379383
signature_key = config[:signature_key]
380384
timestamp_key = config[:timestamp_key]
385+
separator = config[:structured_header_separator]
386+
key_value_separator = config[:key_value_separator]
381387

382388
# Parse comma-separated key-value pairs
383389
pairs = {}
384-
header_value.split(",").each do |pair|
385-
key, value = pair.split("=", 2)
390+
header_value.split(separator).each do |pair|
391+
key, value = pair.split(key_value_separator, 2)
386392
return nil if key.nil? || value.nil?
387393

388394
pairs[key.strip] = value.strip

spec/unit/lib/hooks/plugins/auth/hmac_spec.rb

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid)
699699

700700
describe ".parse_structured_header" do
701701
it "parses valid structured header with timestamp and signature" do
702-
config = { signature_key: "v1", timestamp_key: "t" }
702+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: "=", structured_header_separator: "," }
703703
header_value = "t=1663781880,v1=0123456789abcdef"
704704

705705
result = described_class.send(:parse_structured_header, header_value, config)
@@ -710,8 +710,20 @@ def test_iso_timestamp(iso_timestamp, should_be_valid)
710710
})
711711
end
712712

713+
it "parses valid structured header with timestamp and signature using different separators" do
714+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: ":", structured_header_separator: "." }
715+
header_value = "t:1663781880.v1:0123456789abcdef"
716+
717+
result = described_class.send(:parse_structured_header, header_value, config)
718+
719+
expect(result).to eq({
720+
signature: "0123456789abcdef",
721+
timestamp: "1663781880"
722+
})
723+
end
724+
713725
it "parses structured header with only signature" do
714-
config = { signature_key: "v1", timestamp_key: "t" }
726+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: "=", structured_header_separator: "," }
715727
header_value = "v1=abcdef123456"
716728

717729
result = described_class.send(:parse_structured_header, header_value, config)
@@ -722,7 +734,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid)
722734
end
723735

724736
it "handles extra whitespace in key-value pairs" do
725-
config = { signature_key: "v1", timestamp_key: "t" }
737+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: "=", structured_header_separator: "," }
726738
header_value = "t = 1663781880 , v1 = 0123456789abcdef "
727739

728740
result = described_class.send(:parse_structured_header, header_value, config)
@@ -734,7 +746,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid)
734746
end
735747

736748
it "returns nil for malformed header (missing equals)" do
737-
config = { signature_key: "v1", timestamp_key: "t" }
749+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: "=", structured_header_separator: "," }
738750
header_value = "t,v1=abcdef"
739751

740752
result = described_class.send(:parse_structured_header, header_value, config)
@@ -743,7 +755,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid)
743755
end
744756

745757
it "returns nil when signature key is missing" do
746-
config = { signature_key: "v1", timestamp_key: "t" }
758+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: "=", structured_header_separator: "," }
747759
header_value = "t=1663781880,other=value"
748760

749761
result = described_class.send(:parse_structured_header, header_value, config)
@@ -752,7 +764,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid)
752764
end
753765

754766
it "returns nil when signature value is empty" do
755-
config = { signature_key: "v1", timestamp_key: "t" }
767+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: "=", structured_header_separator: "," }
756768
header_value = "t=1663781880,v1="
757769

758770
result = described_class.send(:parse_structured_header, header_value, config)
@@ -761,7 +773,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid)
761773
end
762774

763775
it "ignores extra key-value pairs not in config" do
764-
config = { signature_key: "v1", timestamp_key: "t" }
776+
config = { signature_key: "v1", timestamp_key: "t", key_value_separator: "=", structured_header_separator: "," }
765777
header_value = "t=1663781880,v1=abcdef,extra=ignored,another=also_ignored"
766778

767779
result = described_class.send(:parse_structured_header, header_value, config)

0 commit comments

Comments
 (0)