Skip to content

Commit 434b0e9

Browse files
authored
Strip query params (#222)
* Strip query params * Rubocop * Inverst the logic
1 parent 64da504 commit 434b0e9

File tree

8 files changed

+86
-18
lines changed

8 files changed

+86
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- **Drop support for Ruby < 3.2** - Now requires Ruby 3.2, 3.3, or 3.4+
66
- **Drop support for Rails 6.x** - Now requires Rails 7.2+ or 8.0+
77
- **Remove deprecated Ruby 2.x compatibility code**
8+
- **Canonical string no longer includes query parameters** – signatures now use only the request path. During a staged rollout you can temporarily accept legacy query-aware signatures by setting `ApiAuth.legacy_query_params_compatibility = true`.
89

910
## New Features
1011

@@ -30,8 +31,8 @@
3031
- RSpec ~> 3.13
3132
- Rake ~> 13.0
3233
- Rest-Client ~> 2.1
33-
- Remove implicit ActiveSupport requirement from runtime
3434
- Typhoeus ~> 1.4
35+
- Remove implicit ActiveSupport requirement from runtime
3536

3637
# 2.6.0 (2025-01-18)
3738

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ automatically added to the request. The canonical string is computed as follows:
3030
canonical_string = "#{http method},#{content-type},#{X-Authorization-Content-SHA256},#{request URI},#{timestamp}"
3131
```
3232

33+
> **Note:** As of v3.0 the "request URI" component above is just the path (query parameters are excluded) so signatures remain stable even when intermediaries rewrite a query string.
34+
3335
e.g.,
3436

3537
```ruby
@@ -58,6 +60,16 @@ access id that was attached in the header. The access id can be any integer or
5860
string that uniquely identifies the client. The signed request expires after 15
5961
minutes in order to avoid replay attacks.
6062

63+
### Legacy query parameter compatibility
64+
65+
Versions prior to 3.0 included the query string inside the canonical request URI. If you have to roll out the 3.0 change gradually across multiple services, you can temporarily enable support for legacy signatures on the server side:
66+
67+
```ruby
68+
ApiAuth.legacy_query_params_compatibility = true
69+
```
70+
71+
With the flag disabled (the default) only the path segment is considered part of the canonical string.
72+
6173
## References
6274

6375
* [Hash functions](https://en.wikipedia.org/wiki/Cryptographic_hash_function)

lib/api_auth/base.rb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ module ApiAuth
1010
class << self
1111
include Helpers
1212

13+
attr_writer :legacy_query_params_compatibility
14+
15+
def legacy_query_params_compatibility?
16+
!!@legacy_query_params_compatibility
17+
end
18+
1319
# Signs an HTTP request using the client's access id and secret key.
1420
# Returns the HTTP request object with the modified headers.
1521
#
@@ -90,9 +96,16 @@ def signatures_match?(headers, secret_key, options)
9096
options = { digest: digest }.merge(options)
9197

9298
header_sig = match_data[3]
93-
calculated_sig = hmac_signature(headers, secret_key, options)
99+
calculated_sig = hmac_signature(headers, secret_key, options, include_query: false)
100+
101+
return true if secure_equals?(header_sig, calculated_sig, secret_key)
94102

95-
secure_equals?(header_sig, calculated_sig, secret_key)
103+
if legacy_query_params_compatibility?
104+
legacy_sig = hmac_signature(headers, secret_key, options, include_query: true)
105+
return true if secure_equals?(header_sig, legacy_sig, secret_key)
106+
end
107+
108+
false
96109
end
97110

98111
def secure_equals?(m1, m2, key)
@@ -104,15 +117,15 @@ def sha1_hmac(key, message)
104117
OpenSSL::HMAC.digest(digest, key, message)
105118
end
106119

107-
def hmac_signature(headers, secret_key, options)
108-
canonical_string = headers.canonical_string(options[:override_http_method], options[:headers_to_sign])
120+
def hmac_signature(headers, secret_key, options, include_query: false)
121+
canonical_string = headers.canonical_string(options[:override_http_method], options[:headers_to_sign], include_query: include_query)
109122
digest = OpenSSL::Digest.new(options[:digest])
110123
b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
111124
end
112125

113126
def auth_header(headers, access_id, secret_key, options)
114127
hmac_string = "-HMAC-#{options[:digest].upcase}" unless options[:digest] == 'sha1'
115-
"APIAuth#{hmac_string} #{access_id}:#{hmac_signature(headers, secret_key, options)}"
128+
"APIAuth#{hmac_string} #{access_id}:#{hmac_signature(headers, secret_key, options, include_query: false)}"
116129
end
117130

118131
def parse_auth_header(auth_header)

lib/api_auth/headers.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def timestamp
7070
@request.timestamp
7171
end
7272

73-
def canonical_string(override_method = nil, headers_to_sign = [])
73+
def canonical_string(override_method = nil, headers_to_sign = [], include_query: false)
7474
request_method = override_method || @request.http_method
7575

7676
raise ArgumentError, 'unable to determine the http method from the request, please supply an override' if request_method.nil?
@@ -80,7 +80,7 @@ def canonical_string(override_method = nil, headers_to_sign = [])
8080
canonical_array = [request_method.upcase,
8181
@request.content_type,
8282
@request.content_hash,
83-
parse_uri(@request.original_uri || @request.request_uri),
83+
parse_uri(@request.original_uri || @request.request_uri, include_query: include_query),
8484
@request.timestamp]
8585

8686
if headers_to_sign.is_a?(Array) && headers_to_sign.any?
@@ -125,8 +125,8 @@ def sign_header(header)
125125

126126
private
127127

128-
def parse_uri(uri)
129-
canonical_request_uri(uri)
128+
def parse_uri(uri, include_query: false)
129+
canonical_request_uri(uri, nil, include_query: include_query)
130130
end
131131
end
132132
end

lib/api_auth/helpers.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ def value_present?(value)
3636
!value_blank?(value)
3737
end
3838

39-
def canonical_request_uri(base_url, additional_query = nil)
39+
def canonical_request_uri(base_url, additional_query = nil, include_query: false)
4040
base = base_url.to_s
4141
return '/' if base.empty?
4242

4343
uri = URI.parse(base)
44-
merged_query = merge_query_strings(uri.query, normalize_query_component(additional_query))
45-
uri.query = merged_query if value_present?(merged_query)
44+
if include_query
45+
merged_query = merge_query_strings(uri.query, normalize_query_component(additional_query))
46+
uri.query = merged_query if value_present?(merged_query)
47+
else
48+
uri.query = nil
49+
end
4650

4751
result = uri.respond_to?(:request_uri) ? uri.request_uri : uri.to_s
4852
value_present?(result) ? result : '/'

lib/api_auth/request_drivers/typhoeus_request.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def original_uri
5555
end
5656

5757
def request_uri
58-
canonical_request_uri(@request.base_url, params_query)
58+
canonical_request_uri(@request.base_url, params_query, include_query: true)
5959
end
6060

6161
def set_date

spec/api_auth_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,44 @@ def hmac(secret_key, request, canonical_string = nil, digest = 'sha1')
176176
expect(ApiAuth.authentic?(signed_request, '123', headers_to_sign: %w[HTTP_X_FORWARDED_FOR])).to eq true
177177
end
178178
end
179+
180+
context 'legacy query parameter compatibility' do
181+
let(:legacy_request) do
182+
Net::HTTP::Get.new('/resource.xml?foo=bar&bar=foo',
183+
'content-type' => 'text/plain',
184+
'date' => Time.now.utc.httpdate)
185+
end
186+
let(:access_id) { 'legacy' }
187+
let(:secret_key) { 'secret' }
188+
let(:headers) { ApiAuth::Headers.new(legacy_request) }
189+
190+
def legacy_signature(headers, secret)
191+
canonical = headers.canonical_string(nil, [], include_query: true)
192+
digest = OpenSSL::Digest.new('sha1')
193+
ApiAuth.b64_encode(OpenSSL::HMAC.digest(digest, secret, canonical))
194+
end
195+
196+
before do
197+
sig = legacy_signature(headers, secret_key)
198+
legacy_request['Authorization'] = "APIAuth #{access_id}:#{sig}"
199+
end
200+
201+
around do |example|
202+
original = ApiAuth.legacy_query_params_compatibility?
203+
example.run
204+
ApiAuth.legacy_query_params_compatibility = original
205+
end
206+
207+
it 'rejects legacy signatures by default' do
208+
ApiAuth.legacy_query_params_compatibility = false
209+
expect(ApiAuth.authentic?(legacy_request, secret_key)).to eq false
210+
end
211+
212+
it 'accepts legacy signatures when compatibility is enabled' do
213+
ApiAuth.legacy_query_params_compatibility = true
214+
expect(ApiAuth.authentic?(legacy_request, secret_key)).to eq true
215+
end
216+
end
179217
end
180218

181219
describe '.access_id' do

spec/headers_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
context 'uri has a string matching https:// in it' do
3535
let(:uri) { 'https://google.com/?redirect_to=https://www.example.com'.freeze }
3636

37-
it 'return /?redirect_to=https://www.example.com as canonical string path' do
38-
expect(subject.canonical_string).to eq('GET,,,/?redirect_to=https://www.example.com,')
37+
it 'return / as canonical string path' do
38+
expect(subject.canonical_string).to eq('GET,,,/,')
3939
end
4040

4141
it 'does not change request url (by removing host)' do
@@ -121,7 +121,7 @@
121121

122122
context 'the driver uses the original_uri' do
123123
it 'constructs the canonical_string with the original_uri' do
124-
expect(headers.canonical_string).to eq 'GET,text/html,12345,/api/resource.xml?foo=bar&bar=foo,Mon, 23 Jan 1984 03:29:56 GMT'
124+
expect(headers.canonical_string).to eq 'GET,text/html,12345,/api/resource.xml,Mon, 23 Jan 1984 03:29:56 GMT'
125125
end
126126
end
127127
end
@@ -147,7 +147,7 @@
147147
context 'the driver uses the original_uri' do
148148
it 'constructs the canonical_string with the original_uri' do
149149
expect(headers.canonical_string(nil, %w[X-FORWARDED-FOR]))
150-
.to eq 'GET,text/html,12345,/resource.xml?bar=foo&foo=bar,Mon, 23 Jan 1984 03:29:56 GMT,192.168.1.1'
150+
.to eq 'GET,text/html,12345,/resource.xml,Mon, 23 Jan 1984 03:29:56 GMT,192.168.1.1'
151151
end
152152
end
153153
end

0 commit comments

Comments
 (0)