From bf340eb907ec704e092f3ecf0c4daaf5d25299c7 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sat, 16 May 2026 05:40:17 +0300 Subject: [PATCH] fix(parsing): ignore quoted payment schemes Signed-off-by: EfeDurmaz16 --- lib/mpp/challenge.rb | 70 ++++++++++++++++++++++++++++++++++++++-- test/mpp/test_parsing.rb | 39 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/lib/mpp/challenge.rb b/lib/mpp/challenge.rb index 25e72b9..6026f32 100644 --- a/lib/mpp/challenge.rb +++ b/lib/mpp/challenge.rb @@ -61,17 +61,81 @@ def self.from_www_authenticate(header) # Parse multiple Payment challenges from a merged WWW-Authenticate header. # Handles RFC 9110 ยง11.6.1 comma-separated authentication schemes. def self.from_www_authenticate_list(header) - indices = [] - header.scan(/Payment\s+/i) { indices << T.must(Regexp.last_match).begin(0) } + indices = payment_scheme_indices(header) return [] if indices.empty? indices.each_with_index.map do |start_idx, i| - end_idx = (i + 1 < indices.length) ? indices[i + 1] : header.length + end_idx = if i + 1 < indices.length + indices[i + 1] + else + next_auth_scheme_index(header, start_idx + "Payment".length) || header.length + end chunk = T.must(header[start_idx...end_idx]).sub(/,\s*$/, "") from_www_authenticate(chunk) end end + def self.payment_scheme_indices(header) + indices = [] + each_auth_scheme_index(header) do |index, scheme| + indices << index if scheme.casecmp("Payment").zero? + end + indices + end + + def self.next_auth_scheme_index(header, offset) + each_auth_scheme_index(header, offset) do |index, _scheme| + return index + end + nil + end + + def self.each_auth_scheme_index(header, offset = 0) + in_quote = false + escaped = false + i = offset + + while i < header.length + char = T.must(header[i]) + + if in_quote + if escaped + escaped = false + elsif char == "\\" + escaped = true + elsif char == "\"" + in_quote = false + end + i += 1 + next + end + + if char == "\"" + in_quote = true + i += 1 + next + end + + if scheme_boundary?(header, i) + match = T.must(header[i..]).match(/\A([A-Za-z][A-Za-z0-9._~+\/-]*)\s+/) + if match + yield i, T.must(match[1]) + i += T.must(match[0]).length + next + end + end + + i += 1 + end + end + + def self.scheme_boundary?(header, index) + return true if index == 0 + + previous = T.must(header[0...index]).rstrip + previous.end_with?(",") + end + # Serialize to a WWW-Authenticate header value. def to_www_authenticate(realm) Mpp::Parsing.format_www_authenticate(self, realm) diff --git a/test/mpp/test_parsing.rb b/test/mpp/test_parsing.rb index f8e269e..e670a23 100644 --- a/test/mpp/test_parsing.rb +++ b/test/mpp/test_parsing.rb @@ -241,6 +241,45 @@ def test_from_www_authenticate_list_multiple assert_equal "other", result[1].method end + def test_from_www_authenticate_list_ignores_payment_scheme_inside_quotes + c1 = Mpp::Challenge.create( + secret_key: "s1", + realm: "api, Payment realm", + method: "tempo", + intent: "charge", + request: {"amount" => "100"} + ) + c2 = Mpp::Challenge.create( + secret_key: "s2", + realm: "api.example.com", + method: "stripe", + intent: "charge", + request: {"amount" => "200"} + ) + header = "#{c1.to_www_authenticate("api, Payment realm")}, #{c2.to_www_authenticate("api.example.com")}" + result = Mpp::Challenge.from_www_authenticate_list(header) + + assert_equal 2, result.length + assert_equal "api, Payment realm", result[0].realm + assert_equal "tempo", result[0].method + assert_equal "stripe", result[1].method + end + + def test_from_www_authenticate_list_stops_before_next_non_payment_scheme + challenge = Mpp::Challenge.create( + secret_key: "test-secret", + realm: "api.example.com", + method: "tempo", + intent: "charge", + request: {"amount" => "1000000"} + ) + header = "#{challenge.to_www_authenticate("api.example.com")}, Bearer realm=\"fallback\"" + result = Mpp::Challenge.from_www_authenticate_list(header) + + assert_equal 1, result.length + assert_equal challenge.id, result[0].id + end + def test_from_www_authenticate_list_empty assert_equal [], Mpp::Challenge.from_www_authenticate_list("Bearer token123") assert_equal [], Mpp::Challenge.from_www_authenticate_list("")