From 75e58836a71061acd9b20b1d423f232734094025 Mon Sep 17 00:00:00 2001 From: David Varga Date: Thu, 2 Apr 2026 12:42:07 +0200 Subject: [PATCH 1/2] Fix oauth_* accessors to unwrap array values from wrap_values Parameters stored via wrap_values are arrays, but callers expect scalar strings from oauth_timestamp, oauth_nonce, oauth_consumer_key etc. Only oauth_signature had the [...].flatten.first unwrap; all other accessors returned arrays when params came from POST body or query string, causing NoMethodError (e.g. Array#to_i on timestamp). Apply the same unwrap pattern to all oauth_* accessors for consistency. --- lib/oauth/request_proxy/base.rb | 21 +++--- .../request_proxy/base_accessors_spec.rb | 67 +++++++++++++++++++ 2 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 spec/oauth/request_proxy/base_accessors_spec.rb diff --git a/lib/oauth/request_proxy/base.rb b/lib/oauth/request_proxy/base.rb index a8411cb3..3cc628b9 100644 --- a/lib/oauth/request_proxy/base.rb +++ b/lib/oauth/request_proxy/base.rb @@ -23,15 +23,15 @@ def initialize(request, options = {}) ## OAuth parameters def oauth_callback - parameters["oauth_callback"] + [parameters["oauth_callback"]].flatten.first end def oauth_consumer_key - parameters["oauth_consumer_key"] + [parameters["oauth_consumer_key"]].flatten.first end def oauth_nonce - parameters["oauth_nonce"] + [parameters["oauth_nonce"]].flatten.first end def oauth_signature @@ -40,31 +40,26 @@ def oauth_signature end def oauth_signature_method - case parameters["oauth_signature_method"] - when Array - parameters["oauth_signature_method"].first - else - parameters["oauth_signature_method"] - end + [parameters["oauth_signature_method"]].flatten.first end def oauth_timestamp - parameters["oauth_timestamp"] + [parameters["oauth_timestamp"]].flatten.first end def oauth_token - parameters["oauth_token"] + [parameters["oauth_token"]].flatten.first end # OAuth 1.0a only: value returned to the Consumer after user authorization # and required when exchanging a Request Token for an Access Token. # Not present in OAuth 1.0 flows. def oauth_verifier - parameters["oauth_verifier"] + [parameters["oauth_verifier"]].flatten.first end def oauth_version - parameters["oauth_version"] + [parameters["oauth_version"]].flatten.first end # TODO: deprecate these diff --git a/spec/oauth/request_proxy/base_accessors_spec.rb b/spec/oauth/request_proxy/base_accessors_spec.rb new file mode 100644 index 00000000..f01f12b8 --- /dev/null +++ b/spec/oauth/request_proxy/base_accessors_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "oauth/request_proxy/base" + +RSpec.describe OAuth::RequestProxy::Base do + # Subclasses that use wrap_values (e.g. ActionControllerRequest, + # ActionDispatchRequest) store all parameters as arrays. These specs + # verify that every oauth_* accessor safely unwraps array values so + # callers always receive a scalar string (or nil), regardless of whether + # the parameter arrived as a scalar or a wrapped array. + let(:proxy_class) do + Class.new(OAuth::RequestProxy::Base) do + attr_writer :params + + def parameters + @params || {} + end + end + end + + def proxy_with(params) + proxy_class.new(Object.new).tap { |p| p.params = params } + end + + shared_examples "scalar accessor" do |method, key| + it "returns a scalar when the parameter is stored as a single-element array" do + p = proxy_with(key => ["value"]) + expect(p.public_send(method)).to eq("value") + end + + it "returns a scalar when the parameter is stored as a plain string" do + p = proxy_with(key => "value") + expect(p.public_send(method)).to eq("value") + end + + it "returns nil when the parameter is absent" do + p = proxy_with({}) + expect(p.public_send(method)).to be_nil + end + end + + include_examples "scalar accessor", :oauth_consumer_key, "oauth_consumer_key" + include_examples "scalar accessor", :oauth_nonce, "oauth_nonce" + include_examples "scalar accessor", :oauth_timestamp, "oauth_timestamp" + include_examples "scalar accessor", :oauth_token, "oauth_token" + include_examples "scalar accessor", :oauth_signature_method,"oauth_signature_method" + include_examples "scalar accessor", :oauth_callback, "oauth_callback" + include_examples "scalar accessor", :oauth_verifier, "oauth_verifier" + include_examples "scalar accessor", :oauth_version, "oauth_version" + + describe "#oauth_signature" do + it "returns a scalar when the parameter is stored as a single-element array" do + p = proxy_with("oauth_signature" => ["sig"]) + expect(p.oauth_signature).to eq("sig") + end + + it "returns a scalar when the parameter is stored as a plain string" do + p = proxy_with("oauth_signature" => "sig") + expect(p.oauth_signature).to eq("sig") + end + + it "returns an empty string when the parameter is absent" do + p = proxy_with({}) + expect(p.oauth_signature).to eq("") + end + end +end From 1a13817542f2f3d406354ed640df221e7a8725d2 Mon Sep 17 00:00:00 2001 From: David Varga Date: Thu, 2 Apr 2026 14:26:10 +0200 Subject: [PATCH 2/2] Remove redundant RackRequest#signature override Base already defines oauth_signature with array-safe unwrapping and aliases it as signature. The override in RackRequest duplicated that logic without the safety, making it a latent bug for array-valued params and a broken inherited method for ActionDispatchRequest which wraps all params via wrap_values. --- lib/oauth/request_proxy/rack_request.rb | 4 ---- spec/oauth/request_proxy/rack_spec.rb | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/oauth/request_proxy/rack_request.rb b/lib/oauth/request_proxy/rack_request.rb index f006fc4a..f35c1f42 100644 --- a/lib/oauth/request_proxy/rack_request.rb +++ b/lib/oauth/request_proxy/rack_request.rb @@ -26,10 +26,6 @@ def parameters end end - def signature - parameters["oauth_signature"] - end - protected def query_params diff --git a/spec/oauth/request_proxy/rack_spec.rb b/spec/oauth/request_proxy/rack_spec.rb index 26ef2cf4..81009318 100644 --- a/spec/oauth/request_proxy/rack_spec.rb +++ b/spec/oauth/request_proxy/rack_spec.rb @@ -23,6 +23,23 @@ def app expect(proxy.parameters).to include("a" => "1", "b" => "2") end + describe "#signature" do + it "returns a scalar when oauth_signature is a plain string" do + proxy = OAuth::RequestProxy::RackRequest.new(Object.new, clobber_request: true, parameters: {"oauth_signature" => "sig"}) + expect(proxy.signature).to eq("sig") + end + + it "returns a scalar when oauth_signature is a single-element array" do + proxy = OAuth::RequestProxy::RackRequest.new(Object.new, clobber_request: true, parameters: {"oauth_signature" => ["sig"]}) + expect(proxy.signature).to eq("sig") + end + + it "returns an empty string when oauth_signature is absent" do + proxy = OAuth::RequestProxy::RackRequest.new(Object.new, clobber_request: true, parameters: {}) + expect(proxy.signature).to eq("") + end + end + it "proxies Rack::Request POST form params" do post "/test", {"x" => "9", "y" => "10"} rack_req = last_request