api-auth 2.2.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -52
  3. data/.rubocop_todo.yml +92 -0
  4. data/.travis.yml +15 -14
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile +1 -1
  7. data/README.md +77 -38
  8. data/VERSION +1 -1
  9. data/api_auth.gemspec +15 -11
  10. data/gemfiles/http2.gemfile +7 -0
  11. data/gemfiles/http3.gemfile +7 -0
  12. data/gemfiles/http4.gemfile +7 -0
  13. data/gemfiles/rails_5.gemfile +5 -7
  14. data/gemfiles/rails_51.gemfile +5 -5
  15. data/gemfiles/rails_52.gemfile +9 -0
  16. data/gemfiles/rails_60.gemfile +11 -0
  17. data/lib/api_auth.rb +3 -0
  18. data/lib/api_auth/base.rb +2 -2
  19. data/lib/api_auth/headers.rb +19 -8
  20. data/lib/api_auth/railtie.rb +9 -5
  21. data/lib/api_auth/request_drivers/action_controller.rb +1 -0
  22. data/lib/api_auth/request_drivers/faraday.rb +2 -1
  23. data/lib/api_auth/request_drivers/grape_request.rb +87 -0
  24. data/lib/api_auth/request_drivers/http.rb +96 -0
  25. data/lib/api_auth/request_drivers/httpi.rb +1 -0
  26. data/lib/api_auth/request_drivers/net_http.rb +1 -1
  27. data/lib/api_auth/request_drivers/rack.rb +1 -0
  28. data/lib/api_auth/request_drivers/rest_client.rb +3 -2
  29. data/spec/api_auth_spec.rb +7 -0
  30. data/spec/headers_spec.rb +26 -8
  31. data/spec/request_drivers/action_controller_spec.rb +10 -4
  32. data/spec/request_drivers/action_dispatch_spec.rb +17 -11
  33. data/spec/request_drivers/curb_spec.rb +9 -3
  34. data/spec/request_drivers/faraday_spec.rb +6 -0
  35. data/spec/request_drivers/grape_request_spec.rb +279 -0
  36. data/spec/request_drivers/http_spec.rb +190 -0
  37. data/spec/request_drivers/httpi_spec.rb +6 -0
  38. data/spec/request_drivers/net_http_spec.rb +6 -0
  39. data/spec/request_drivers/rack_spec.rb +6 -0
  40. data/spec/request_drivers/rest_client_spec.rb +93 -15
  41. data/spec/spec_helper.rb +3 -4
  42. metadata +102 -66
  43. data/gemfiles/rails_4.gemfile +0 -11
  44. data/gemfiles/rails_41.gemfile +0 -11
  45. data/gemfiles/rails_42.gemfile +0 -11
@@ -21,6 +21,7 @@ module ApiAuth
21
21
 
22
22
  def populate_content_md5
23
23
  return unless @request.body
24
+
24
25
  @request.headers['Content-MD5'] = calculated_md5
25
26
  fetch_headers
26
27
  end
@@ -1,4 +1,3 @@
1
- require 'time'
2
1
  module ApiAuth
3
2
  module RequestDrivers # :nodoc:
4
3
  class NetHttpRequest # :nodoc:
@@ -29,6 +28,7 @@ module ApiAuth
29
28
 
30
29
  def populate_content_md5
31
30
  return unless @request.class::REQUEST_HAS_BODY
31
+
32
32
  @request['Content-MD5'] = calculated_md5
33
33
  end
34
34
 
@@ -27,6 +27,7 @@ module ApiAuth
27
27
 
28
28
  def populate_content_md5
29
29
  return unless %w[POST PUT].include?(@request.request_method)
30
+
30
31
  @request.env['Content-MD5'] = calculated_md5
31
32
  fetch_headers
32
33
  end
@@ -29,13 +29,14 @@ module ApiAuth
29
29
  end
30
30
 
31
31
  def populate_content_md5
32
- return unless %i[post put].include?(@request.method)
32
+ return unless %w[post put].include?(@request.method.to_s)
33
+
33
34
  @request.headers['Content-MD5'] = calculated_md5
34
35
  save_headers
35
36
  end
36
37
 
37
38
  def md5_mismatch?
38
- if %i[post put].include?(@request.method)
39
+ if %w[post put].include?(@request.method.to_s)
39
40
  calculated_md5 != content_md5
40
41
  else
41
42
  false
@@ -169,6 +169,13 @@ describe 'ApiAuth' do
169
169
  expect(ApiAuth.authentic?(signed_request, '123', clock_skew: 60.seconds)).to eq false
170
170
  end
171
171
  end
172
+
173
+ context 'when passed the headers_to_sign option' do
174
+ it 'validates the request' do
175
+ request['X-Forwarded-For'] = '192.168.1.1'
176
+ expect(ApiAuth.authentic?(signed_request, '123', headers_to_sign: %w[HTTP_X_FORWARDED_FOR])).to eq true
177
+ end
178
+ end
172
179
  end
173
180
 
174
181
  describe '.access_id' do
@@ -7,14 +7,6 @@ describe ApiAuth::Headers do
7
7
  subject(:headers) { described_class.new(request) }
8
8
  let(:uri) { '' }
9
9
 
10
- context 'empty uri' do
11
- let(:uri) { ''.freeze }
12
-
13
- it 'adds / to canonical string' do
14
- expect(subject.canonical_string).to eq('GET,,,/,')
15
- end
16
- end
17
-
18
10
  context 'uri with just host without /' do
19
11
  let(:uri) { 'http://google.com'.freeze }
20
12
 
@@ -133,6 +125,32 @@ describe ApiAuth::Headers do
133
125
  end
134
126
  end
135
127
  end
128
+
129
+ context 'when headers to sign are provided' do
130
+ let(:request) do
131
+ Faraday::Request.create('GET') do |req|
132
+ req.options = Faraday::RequestOptions.new(Faraday::FlatParamsEncoder)
133
+ req.params = Faraday::Utils::ParamsHash.new
134
+ req.url('/resource.xml?foo=bar&bar=foo')
135
+ req.headers = { 'X-Forwarded-For' => '192.168.1.1' }
136
+ end
137
+ end
138
+ subject(:headers) { described_class.new(request) }
139
+ let(:driver) { headers.instance_variable_get('@request') }
140
+
141
+ before do
142
+ allow(driver).to receive(:content_type).and_return 'text/html'
143
+ allow(driver).to receive(:content_md5).and_return '12345'
144
+ allow(driver).to receive(:timestamp).and_return 'Mon, 23 Jan 1984 03:29:56 GMT'
145
+ end
146
+
147
+ context 'the driver uses the original_uri' do
148
+ it 'constructs the canonical_string with the original_uri' do
149
+ 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'
151
+ end
152
+ end
153
+ end
136
154
  end
137
155
  end
138
156
 
@@ -80,12 +80,12 @@ if defined?(ActionController::Request)
80
80
  describe 'setting headers correctly' do
81
81
  let(:request) do
82
82
  ActionController::Request.new(
83
- 'PATH_INFO' => '/resource.xml',
84
- 'QUERY_STRING' => 'foo=bar&bar=foo',
83
+ 'PATH_INFO' => '/resource.xml',
84
+ 'QUERY_STRING' => 'foo=bar&bar=foo',
85
85
  'REQUEST_METHOD' => 'PUT',
86
- 'CONTENT_TYPE' => 'text/plain',
86
+ 'CONTENT_TYPE' => 'text/plain',
87
87
  'CONTENT_LENGTH' => '11',
88
- 'rack.input' => StringIO.new("hello\nworld")
88
+ 'rack.input' => StringIO.new("hello\nworld")
89
89
  )
90
90
  end
91
91
 
@@ -231,4 +231,10 @@ if defined?(ActionController::Request)
231
231
  end
232
232
  end
233
233
  end
234
+
235
+ describe 'fetch_headers' do
236
+ it 'returns request headers' do
237
+ expect(driven_request.fetch_headers).to include('CONTENT-TYPE' => 'text/plain')
238
+ end
239
+ end
234
240
  end
@@ -7,15 +7,15 @@ if defined?(ActionDispatch::Request)
7
7
 
8
8
  let(:request) do
9
9
  ActionDispatch::Request.new(
10
- 'AUTHORIZATION' => 'APIAuth 1044:12345',
11
- 'PATH_INFO' => '/resource.xml',
12
- 'QUERY_STRING' => 'foo=bar&bar=foo',
10
+ 'AUTHORIZATION' => 'APIAuth 1044:12345',
11
+ 'PATH_INFO' => '/resource.xml',
12
+ 'QUERY_STRING' => 'foo=bar&bar=foo',
13
13
  'REQUEST_METHOD' => 'PUT',
14
- 'CONTENT_MD5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
15
- 'CONTENT_TYPE' => 'text/plain',
14
+ 'CONTENT_MD5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
15
+ 'CONTENT_TYPE' => 'text/plain',
16
16
  'CONTENT_LENGTH' => '11',
17
- 'HTTP_DATE' => timestamp,
18
- 'rack.input' => StringIO.new("hello\nworld")
17
+ 'HTTP_DATE' => timestamp,
18
+ 'rack.input' => StringIO.new("hello\nworld")
19
19
  )
20
20
  end
21
21
 
@@ -80,12 +80,12 @@ if defined?(ActionDispatch::Request)
80
80
  describe 'setting headers correctly' do
81
81
  let(:request) do
82
82
  ActionDispatch::Request.new(
83
- 'PATH_INFO' => '/resource.xml',
84
- 'QUERY_STRING' => 'foo=bar&bar=foo',
83
+ 'PATH_INFO' => '/resource.xml',
84
+ 'QUERY_STRING' => 'foo=bar&bar=foo',
85
85
  'REQUEST_METHOD' => 'PUT',
86
- 'CONTENT_TYPE' => 'text/plain',
86
+ 'CONTENT_TYPE' => 'text/plain',
87
87
  'CONTENT_LENGTH' => '11',
88
- 'rack.input' => StringIO.new("hello\nworld")
88
+ 'rack.input' => StringIO.new("hello\nworld")
89
89
  )
90
90
  end
91
91
 
@@ -230,5 +230,11 @@ if defined?(ActionDispatch::Request)
230
230
  end
231
231
  end
232
232
  end
233
+
234
+ describe 'fetch_headers' do
235
+ it 'returns request headers' do
236
+ expect(driven_request.fetch_headers).to include('CONTENT_TYPE' => 'text/plain')
237
+ end
238
+ end
233
239
  end
234
240
  end
@@ -6,9 +6,9 @@ describe ApiAuth::RequestDrivers::CurbRequest do
6
6
  let(:request) do
7
7
  headers = {
8
8
  'Authorization' => 'APIAuth 1044:12345',
9
- 'Content-MD5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
10
- 'Content-Type' => 'text/plain',
11
- 'Date' => timestamp
9
+ 'Content-MD5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
10
+ 'Content-Type' => 'text/plain',
11
+ 'Date' => timestamp
12
12
  }
13
13
  Curl::Easy.new('/resource.xml?foo=bar&bar=foo') do |curl|
14
14
  curl.headers = headers
@@ -91,4 +91,10 @@ describe ApiAuth::RequestDrivers::CurbRequest do
91
91
  expect(driven_request.md5_mismatch?).to be false
92
92
  end
93
93
  end
94
+
95
+ describe 'fetch_headers' do
96
+ it 'returns request headers' do
97
+ expect(driven_request.fetch_headers).to include('CONTENT-TYPE' => 'text/plain')
98
+ end
99
+ end
94
100
  end
@@ -256,4 +256,10 @@ describe ApiAuth::RequestDrivers::FaradayRequest do
256
256
  end
257
257
  end
258
258
  end
259
+
260
+ describe 'fetch_headers' do
261
+ it 'returns request headers' do
262
+ expect(driven_request.fetch_headers).to include('CONTENT-TYPE' => 'text/plain')
263
+ end
264
+ end
259
265
  end
@@ -0,0 +1,279 @@
1
+ require 'spec_helper'
2
+
3
+ describe ApiAuth::RequestDrivers::GrapeRequest do
4
+ let(:default_method) { 'PUT' }
5
+ let(:default_params) do
6
+ { 'message' => "hello\nworld" }
7
+ end
8
+ let(:default_options) do
9
+ {
10
+ method: method,
11
+ params: params
12
+ }
13
+ end
14
+ let(:default_env) do
15
+ Rack::MockRequest.env_for('/', options)
16
+ end
17
+ let(:method) { default_method }
18
+ let(:params) { default_params }
19
+ let(:options) { default_options.merge(request_headers) }
20
+ let(:env) { default_env }
21
+
22
+ let(:request) do
23
+ Grape::Request.new(env)
24
+ end
25
+
26
+ let(:timestamp) { Time.now.utc.httpdate }
27
+ let(:request_headers) do
28
+ {
29
+ 'HTTP_X_HMAC_AUTHORIZATION' => 'APIAuth 1044:12345',
30
+ 'HTTP_X_HMAC_CONTENT_MD5' => 'WEqCyXEuRBYZbohpZmUyAw==',
31
+ 'HTTP_X_HMAC_CONTENT_TYPE' => 'text/plain',
32
+ 'HTTP_X_HMAC_DATE' => timestamp
33
+ }
34
+ end
35
+
36
+ subject(:driven_request) { ApiAuth::RequestDrivers::GrapeRequest.new(request) }
37
+
38
+ describe 'getting headers correctly' do
39
+ it 'gets the content_type' do
40
+ expect(driven_request.content_type).to eq('text/plain')
41
+ end
42
+
43
+ it 'gets the content_md5' do
44
+ expect(driven_request.content_md5).to eq('WEqCyXEuRBYZbohpZmUyAw==')
45
+ end
46
+
47
+ it 'gets the request_uri' do
48
+ expect(driven_request.request_uri).to eq('http://example.org/')
49
+ end
50
+
51
+ it 'gets the timestamp' do
52
+ expect(driven_request.timestamp).to eq(timestamp)
53
+ end
54
+
55
+ it 'gets the authorization_header' do
56
+ expect(driven_request.authorization_header).to eq('APIAuth 1044:12345')
57
+ end
58
+
59
+ describe '#calculated_md5' do
60
+ it 'calculates md5 from the body' do
61
+ expect(driven_request.calculated_md5).to eq('WEqCyXEuRBYZbohpZmUyAw==')
62
+ end
63
+
64
+ context 'no body' do
65
+ let(:params) { {} }
66
+
67
+ it 'treats no body as empty string' do
68
+ expect(driven_request.calculated_md5).to eq('1B2M2Y8AsgTpgAmY7PhCfg==')
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'http_method' do
74
+ context 'when put request' do
75
+ let(:method) { 'put' }
76
+
77
+ it 'returns upcased put' do
78
+ expect(driven_request.http_method).to eq('PUT')
79
+ end
80
+ end
81
+
82
+ context 'when get request' do
83
+ let(:method) { 'get' }
84
+
85
+ it 'returns upcased get' do
86
+ expect(driven_request.http_method).to eq('GET')
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'setting headers correctly' do
93
+ let(:request_headers) do
94
+ {
95
+ 'HTTP_X_HMAC_CONTENT_TYPE' => 'text/plain'
96
+ }
97
+ end
98
+
99
+ describe '#populate_content_md5' do
100
+ context 'when getting' do
101
+ let(:method) { 'get' }
102
+
103
+ it "doesn't populate content-md5" do
104
+ driven_request.populate_content_md5
105
+ expect(request.headers['Content-Md5']).to be_nil
106
+ end
107
+ end
108
+
109
+ context 'when posting' do
110
+ let(:method) { 'post' }
111
+
112
+ it 'populates content-md5' do
113
+ driven_request.populate_content_md5
114
+ expect(request.headers['Content-Md5']).to eq('WEqCyXEuRBYZbohpZmUyAw==')
115
+ end
116
+
117
+ it 'refreshes the cached headers' do
118
+ driven_request.populate_content_md5
119
+ expect(driven_request.content_md5).to eq('WEqCyXEuRBYZbohpZmUyAw==')
120
+ end
121
+ end
122
+
123
+ context 'when putting' do
124
+ let(:method) { 'put' }
125
+
126
+ it 'populates content-md5' do
127
+ driven_request.populate_content_md5
128
+ expect(request.headers['Content-Md5']).to eq('WEqCyXEuRBYZbohpZmUyAw==')
129
+ end
130
+
131
+ it 'refreshes the cached headers' do
132
+ driven_request.populate_content_md5
133
+ expect(driven_request.content_md5).to eq('WEqCyXEuRBYZbohpZmUyAw==')
134
+ end
135
+ end
136
+
137
+ context 'when deleting' do
138
+ let(:method) { 'delete' }
139
+
140
+ it "doesn't populate content-md5" do
141
+ driven_request.populate_content_md5
142
+ expect(request.headers['Content-Md5']).to be_nil
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '#set_date' do
148
+ before do
149
+ allow(Time).to receive_message_chain(:now, :utc, :httpdate).and_return(timestamp)
150
+ end
151
+
152
+ it 'sets the date header of the request' do
153
+ allow(Time).to receive_message_chain(:now, :utc, :httpdate).and_return(timestamp)
154
+ driven_request.set_date
155
+ expect(request.headers['Date']).to eq(timestamp)
156
+ end
157
+
158
+ it 'refreshes the cached headers' do
159
+ driven_request.set_date
160
+ expect(driven_request.timestamp).to eq(timestamp)
161
+ end
162
+ end
163
+
164
+ describe '#set_auth_header' do
165
+ it 'sets the auth header' do
166
+ driven_request.set_auth_header('APIAuth 1044:54321')
167
+ expect(request.headers['Authorization']).to eq('APIAuth 1044:54321')
168
+ end
169
+ end
170
+ end
171
+
172
+ describe 'md5_mismatch?' do
173
+ context 'when getting' do
174
+ let(:method) { 'get' }
175
+
176
+ it 'is false' do
177
+ expect(driven_request.md5_mismatch?).to be false
178
+ end
179
+ end
180
+
181
+ context 'when posting' do
182
+ let(:method) { 'post' }
183
+
184
+ context 'when calculated matches sent' do
185
+ it 'is false' do
186
+ expect(driven_request.md5_mismatch?).to be false
187
+ end
188
+ end
189
+
190
+ context "when calculated doesn't match sent" do
191
+ let(:params) { { 'message' => 'hello only' } }
192
+
193
+ it 'is true' do
194
+ expect(driven_request.md5_mismatch?).to be true
195
+ end
196
+ end
197
+ end
198
+
199
+ context 'when putting' do
200
+ let(:method) { 'put' }
201
+
202
+ context 'when calculated matches sent' do
203
+ it 'is false' do
204
+ expect(driven_request.md5_mismatch?).to be false
205
+ end
206
+ end
207
+
208
+ context "when calculated doesn't match sent" do
209
+ let(:params) { { 'message' => 'hello only' } }
210
+ it 'is true' do
211
+ expect(driven_request.md5_mismatch?).to be true
212
+ end
213
+ end
214
+ end
215
+
216
+ context 'when deleting' do
217
+ let(:method) { 'delete' }
218
+
219
+ it 'is false' do
220
+ expect(driven_request.md5_mismatch?).to be false
221
+ end
222
+ end
223
+ end
224
+
225
+ describe 'authentics?' do
226
+ let(:request_headers) { {} }
227
+ let(:signed_request) do
228
+ ApiAuth.sign!(request, '1044', '123')
229
+ end
230
+
231
+ context 'when getting' do
232
+ let(:method) { 'get' }
233
+
234
+ it 'validates that the signature in the request header matches the way we sign it' do
235
+ expect(ApiAuth.authentic?(signed_request, '123')).to eq true
236
+ end
237
+ end
238
+
239
+ context 'when posting' do
240
+ let(:method) { 'post' }
241
+
242
+ it 'validates that the signature in the request header matches the way we sign it' do
243
+ expect(ApiAuth.authentic?(signed_request, '123')).to eq true
244
+ end
245
+ end
246
+
247
+ context 'when putting' do
248
+ let(:method) { 'put' }
249
+
250
+ let(:signed_request) do
251
+ ApiAuth.sign!(request, '1044', '123')
252
+ end
253
+
254
+ it 'validates that the signature in the request header matches the way we sign it' do
255
+ expect(ApiAuth.authentic?(signed_request, '123')).to eq true
256
+ end
257
+ end
258
+
259
+ context 'when deleting' do
260
+ let(:method) { 'delete' }
261
+
262
+ let(:signed_request) do
263
+ ApiAuth.sign!(request, '1044', '123')
264
+ end
265
+
266
+ it 'validates that the signature in the request header matches the way we sign it' do
267
+ expect(ApiAuth.authentic?(signed_request, '123')).to eq true
268
+ end
269
+ end
270
+ end
271
+
272
+ describe 'fetch_headers' do
273
+ it 'returns request headers' do
274
+ expect(driven_request.fetch_headers).to include(
275
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded'
276
+ )
277
+ end
278
+ end
279
+ end