api-auth 2.2.0 → 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
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