api-auth 1.5.0 → 2.6.0

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 (68) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/main.yml +71 -0
  3. data/.gitignore +13 -44
  4. data/.rubocop.yml +39 -0
  5. data/.rubocop_todo.yml +83 -0
  6. data/Appraisals +12 -36
  7. data/CHANGELOG.md +75 -1
  8. data/README.md +155 -52
  9. data/Rakefile +1 -1
  10. data/VERSION +1 -1
  11. data/api_auth.gemspec +35 -23
  12. data/gemfiles/rails_60.gemfile +9 -0
  13. data/gemfiles/rails_61.gemfile +9 -0
  14. data/gemfiles/rails_70.gemfile +9 -0
  15. data/lib/api-auth.rb +1 -1
  16. data/lib/api_auth/base.rb +41 -35
  17. data/lib/api_auth/errors.rb +4 -3
  18. data/lib/api_auth/headers.rb +38 -42
  19. data/lib/api_auth/helpers.rb +7 -16
  20. data/lib/api_auth/railtie.rb +34 -74
  21. data/lib/api_auth/request_drivers/action_controller.rb +27 -27
  22. data/lib/api_auth/request_drivers/action_dispatch.rb +0 -6
  23. data/lib/api_auth/request_drivers/curb.rb +16 -21
  24. data/lib/api_auth/request_drivers/faraday.rb +25 -34
  25. data/lib/api_auth/request_drivers/faraday_env.rb +102 -0
  26. data/lib/api_auth/request_drivers/grape_request.rb +87 -0
  27. data/lib/api_auth/request_drivers/http.rb +96 -0
  28. data/lib/api_auth/request_drivers/httpi.rb +22 -27
  29. data/lib/api_auth/request_drivers/net_http.rb +21 -26
  30. data/lib/api_auth/request_drivers/rack.rb +23 -28
  31. data/lib/api_auth/request_drivers/rest_client.rb +24 -29
  32. data/lib/api_auth.rb +4 -0
  33. data/lib/faraday/api_auth/middleware.rb +35 -0
  34. data/lib/faraday/api_auth.rb +8 -0
  35. data/spec/api_auth_spec.rb +135 -96
  36. data/spec/faraday_middleware_spec.rb +17 -0
  37. data/spec/headers_spec.rb +148 -108
  38. data/spec/helpers_spec.rb +8 -10
  39. data/spec/railtie_spec.rb +80 -99
  40. data/spec/request_drivers/action_controller_spec.rb +122 -79
  41. data/spec/request_drivers/action_dispatch_spec.rb +212 -85
  42. data/spec/request_drivers/curb_spec.rb +36 -33
  43. data/spec/request_drivers/faraday_env_spec.rb +188 -0
  44. data/spec/request_drivers/faraday_spec.rb +87 -83
  45. data/spec/request_drivers/grape_request_spec.rb +280 -0
  46. data/spec/request_drivers/http_spec.rb +190 -0
  47. data/spec/request_drivers/httpi_spec.rb +59 -59
  48. data/spec/request_drivers/net_http_spec.rb +70 -66
  49. data/spec/request_drivers/rack_spec.rb +101 -97
  50. data/spec/request_drivers/rest_client_spec.rb +218 -144
  51. data/spec/spec_helper.rb +15 -12
  52. metadata +144 -83
  53. data/.travis.yml +0 -40
  54. data/Gemfile.lock +0 -115
  55. data/gemfiles/rails_23.gemfile +0 -9
  56. data/gemfiles/rails_23.gemfile.lock +0 -70
  57. data/gemfiles/rails_30.gemfile +0 -9
  58. data/gemfiles/rails_30.gemfile.lock +0 -92
  59. data/gemfiles/rails_31.gemfile +0 -9
  60. data/gemfiles/rails_31.gemfile.lock +0 -98
  61. data/gemfiles/rails_32.gemfile +0 -9
  62. data/gemfiles/rails_32.gemfile.lock +0 -97
  63. data/gemfiles/rails_4.gemfile +0 -9
  64. data/gemfiles/rails_4.gemfile.lock +0 -94
  65. data/gemfiles/rails_41.gemfile +0 -9
  66. data/gemfiles/rails_41.gemfile.lock +0 -98
  67. data/gemfiles/rails_42.gemfile +0 -9
  68. data/gemfiles/rails_42.gemfile.lock +0 -115
data/README.md CHANGED
@@ -1,14 +1,13 @@
1
1
  # ApiAuth
2
2
 
3
- [![Build Status](https://travis-ci.org/mgomes/api_auth.png?branch=master)](https://travis-ci.org/mgomes/api_auth)
4
-
5
- ## IMPORTANT: See [CHANGELOG.md](/CHANGELOG.md) for security update information
3
+ [![Build Status](https://github.com/mgomes/api_auth/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/mgomes/api_auth/actions)
4
+ [![Gem Version](https://badge.fury.io/rb/api-auth.svg)](https://badge.fury.io/rb/api-auth)
6
5
 
7
6
  Logins and passwords are for humans. Communication between applications need to
8
7
  be protected through different means.
9
8
 
10
9
  ApiAuth is a Ruby gem designed to be used both in your client and server
11
- HTTP-based applications. It implements the same authentication methods (HMAC-SHA1)
10
+ HTTP-based applications. It implements the same authentication methods (HMAC-SHA2)
12
11
  used by Amazon Web Services.
13
12
 
14
13
  The gem will sign your requests on the client side and authenticate that
@@ -22,41 +21,62 @@ have to be written in the same language as the clients.
22
21
  ## How it works
23
22
 
24
23
  1. A canonical string is first created using your HTTP headers containing the
25
- content-type, content-MD5, request URI and the timestamp. If content-type or
26
- content-MD5 are not present, then a blank string is used in their place. If the
27
- timestamp isn't present, a valid HTTP date is automatically added to the
28
- request. The canonical string is computed as follows:
24
+ `content-type`, `X-Authorization-Content-SHA256`, request path and the date/time stamp.
25
+ If `content-type` or `X-Authorization-Content-SHA256` are not present, then a blank
26
+ string is used in their place. If the timestamp isn't present, a valid HTTP date is
27
+ automatically added to the request. The canonical string is computed as follows:
28
+
29
+ ```ruby
30
+ canonical_string = "#{http method},#{content-type},#{X-Authorization-Content-SHA256},#{request URI},#{timestamp}"
31
+ ```
29
32
 
30
- canonical_string = 'http method,content-type,content-MD5,request URI,timestamp'
33
+ e.g.,
34
+
35
+ ```ruby
36
+ canonical_string = 'POST,application/json,,request_path,Tue, 30 May 2017 03:51:43 GMT'
37
+ ```
31
38
 
32
39
  2. This string is then used to create the signature which is a Base64 encoded
33
40
  SHA1 HMAC, using the client's private secret key.
34
41
 
35
42
  3. This signature is then added as the `Authorization` HTTP header in the form:
36
43
 
37
- Authorization = APIAuth 'client access id':'signature from step 2'
44
+ ```ruby
45
+ Authorization = APIAuth "#{client access id}:#{signature from step 2}"
46
+ ```
47
+
48
+ A cURL request would look like:
38
49
 
39
- 5. On the server side, the SHA1 HMAC is computed in the same way using the
50
+ ```sh
51
+ curl -X POST --header 'Content-Type: application/json' --header "Date: Tue, 30 May 2017 03:51:43 GMT" --header "Authorization: ${AUTHORIZATION}" https://my-app.com/request_path`
52
+ ```
53
+
54
+ 5. On the server side, the SHA2 HMAC is computed in the same way using the
40
55
  request headers and the client's secret key, which is known to only
41
56
  the client and the server but can be looked up on the server using the client's
42
57
  access id that was attached in the header. The access id can be any integer or
43
58
  string that uniquely identifies the client. The signed request expires after 15
44
59
  minutes in order to avoid replay attacks.
45
60
 
46
-
47
61
  ## References
48
62
 
49
- * [Hash functions](http://en.wikipedia.org/wiki/Cryptographic_hash_function)
50
- * [SHA-1 Hash function](http://en.wikipedia.org/wiki/SHA-1)
51
- * [HMAC algorithm](http://en.wikipedia.org/wiki/HMAC)
52
- * [RFC 2104 (HMAC)](http://tools.ietf.org/html/rfc2104)
63
+ * [Hash functions](https://en.wikipedia.org/wiki/Cryptographic_hash_function)
64
+ * [SHA-2 Hash function](https://en.wikipedia.org/wiki/SHA-2)
65
+ * [HMAC algorithm](https://en.wikipedia.org/wiki/HMAC)
66
+ * [RFC 2104 (HMAC)](https://tools.ietf.org/html/rfc2104)
67
+
68
+ ## Requirement
69
+
70
+ This gem require Ruby >= 2.6 and Rails >= 6.0 if you use rails.
53
71
 
54
72
  ## Install
55
73
 
56
74
  The gem doesn't have any dependencies outside of having a working OpenSSL
57
75
  configuration for your Ruby VM. To install:
58
76
 
59
- [sudo] gem install api-auth
77
+ ```sh
78
+ [sudo] gem install api-auth
79
+ ```
60
80
 
61
81
  Please note the dash in the name versus the underscore.
62
82
 
@@ -72,6 +92,8 @@ Here is the current list of supported request objects:
72
92
  * Curb (Curl::Easy)
73
93
  * RestClient
74
94
  * Faraday
95
+ * HTTPI
96
+ * HTTP
75
97
 
76
98
  ### HTTP Client Objects
77
99
 
@@ -79,26 +101,30 @@ Here's a sample implementation of signing a request created with RestClient.
79
101
 
80
102
  Assuming you have a client access id and secret as follows:
81
103
 
82
- ``` ruby
83
- @access_id = "1044"
84
- @secret_key = ApiAuth.generate_secret_key
104
+ ```ruby
105
+ @access_id = "1044"
106
+ @secret_key = ApiAuth.generate_secret_key
85
107
  ```
86
108
 
87
109
  A typical RestClient PUT request may look like:
88
110
 
89
- ``` ruby
90
- headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
91
- 'Content-Type' => "text/plain",
92
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
93
- @request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
94
- :headers => headers,
95
- :method => :put)
111
+ ```ruby
112
+ headers = { 'X-Authorization-Content-SHA256' => "dWiCWEMZWMxeKM8W8Yuh/TbI29Hw5xUSXZWXEJv63+Y=",
113
+ 'Content-Type' => "text/plain",
114
+ 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT"
115
+ }
116
+
117
+ @request = RestClient::Request.new(
118
+ url: "/resource.xml?foo=bar&bar=foo",
119
+ headers: headers,
120
+ method: :put
121
+ )
96
122
  ```
97
123
 
98
124
  To sign that request, simply call the `sign!` method as follows:
99
125
 
100
- ``` ruby
101
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
126
+ ```ruby
127
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
102
128
  ```
103
129
 
104
130
  The proper `Authorization` request header has now been added to that request
@@ -111,8 +137,27 @@ If you are signing a request for a driver that doesn't support automatic http
111
137
  method detection (like Curb or httpi), you can pass the http method as an option
112
138
  into the sign! method like so:
113
139
 
114
- ``` ruby
115
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key, :override_http_method => "PUT")
140
+ ```ruby
141
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key, :override_http_method => "PUT")
142
+ ```
143
+
144
+ If you want to use another digest existing in `OpenSSL::Digest`,
145
+ you can pass the http method as an option into the sign! method like so:
146
+
147
+ ```ruby
148
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key, :digest => 'sha256')
149
+ ```
150
+
151
+ With the `digest` option, the `Authorization` header will be change from:
152
+
153
+ ```sh
154
+ Authorization = APIAuth 'client access id':'signature'
155
+ ```
156
+
157
+ to:
158
+
159
+ ```sh
160
+ Authorization = APIAuth-HMAC-DIGEST_NAME 'client access id':'signature'
116
161
  ```
117
162
 
118
163
  ### ActiveResource Clients
@@ -120,23 +165,36 @@ into the sign! method like so:
120
165
  ApiAuth can transparently protect your ActiveResource communications with a
121
166
  single configuration line:
122
167
 
123
- ``` ruby
124
- class MyResource < ActiveResource::Base
125
- with_api_auth(access_id, secret_key)
126
- end
168
+ ```ruby
169
+ class MyResource < ActiveResource::Base
170
+ with_api_auth(access_id, secret_key)
171
+ end
127
172
  ```
128
173
 
129
174
  This will automatically sign all outgoing ActiveResource requests from your app.
130
175
 
131
- ### Active Rest Client
176
+ ### Flexirest
132
177
 
133
- ApiAuth also works with [ActiveRestClient](https://github.com/whichdigital/active-rest-client) in a very similar way.
134
- Simply add this configuration to your ActiveRestClient initializer in your app and it will automatically sign all outgoing requests.
178
+ ApiAuth also works with [Flexirest](https://github.com/andyjeffries/flexirest) (used to be ActiveRestClient, but that is now unsupported) in a very similar way.
179
+ Simply add this configuration to your Flexirest initializer in your app and it will automatically sign all outgoing requests.
135
180
 
136
- ``` ruby
137
- ActiveRestClient::Base.api_auth_credentials(@access_id, @secret_key)
181
+ ```ruby
182
+ Flexirest::Base.api_auth_credentials(@access_id, @secret_key)
138
183
  ```
139
184
 
185
+ ### Faraday
186
+
187
+ ApiAuth provides a middleware for adding authentication to a Faraday connection:
188
+
189
+ ```ruby
190
+ require 'faraday/api_auth'
191
+ Faraday.new do |f|
192
+ f.request :api_auth, @access_id, @secret_key
193
+ end
194
+ ```
195
+
196
+ The order of middlewares is important. You should make sure api_auth is last.
197
+
140
198
  ## Server
141
199
 
142
200
  ApiAuth provides some built in methods to help you generate API keys for your
@@ -144,23 +202,57 @@ clients as well as verifying incoming API requests.
144
202
 
145
203
  To generate a Base64 encoded API key for a client:
146
204
 
147
- ``` ruby
148
- ApiAuth.generate_secret_key
205
+ ```ruby
206
+ ApiAuth.generate_secret_key
149
207
  ```
150
208
 
151
209
  To validate whether or not a request is authentic:
152
210
 
211
+ ```ruby
212
+ ApiAuth.authentic?(signed_request, secret_key)
213
+ ```
214
+
215
+ The `authentic?` method uses the digest specified in the `Authorization` header.
216
+ For example SHA256 for:
217
+
218
+ ```sh
219
+ Authorization = APIAuth-HMAC-SHA256 'client access id':'signature'
220
+ ```
221
+
222
+ And by default SHA1 if the HMAC-DIGEST is not specified.
223
+
224
+ If you want to force the usage of another digest method, you should pass it as an option parameter:
225
+
226
+ ```ruby
227
+ ApiAuth.authentic?(signed_request, secret_key, :digest => 'sha256')
228
+ ```
229
+
230
+ For security, requests dated older or newer than a certain timespan are considered inauthentic.
231
+
232
+ This prevents old requests from being reused in replay attacks, and also ensures requests
233
+ can't be dated into the far future.
234
+
235
+ The default span is 15 minutes, but you can override this:
236
+
237
+ ```ruby
238
+ ApiAuth.authentic?(signed_request, secret_key, :clock_skew => 60) # or 1.minute in ActiveSupport
239
+ ```
240
+
241
+ If you want to sign custom headers, you can pass them as an array of strings in the options like so:
242
+
153
243
  ``` ruby
154
- ApiAuth.authentic?(signed_request, secret_key)
244
+ ApiAuth.authentic?(signed_request, secret_key, headers_to_sign: %w[HTTP_HEADER_NAME])
155
245
  ```
156
246
 
247
+ With the specified headers values being at the end of the canonical string in the same order.
248
+
157
249
  If your server is a Rails app, the signed request will be the `request` object.
158
250
 
159
251
  In order to obtain the secret key for the client, you first need to look up the
160
252
  client's access_id. ApiAuth can pull that from the request headers for you:
161
253
 
162
254
  ``` ruby
163
- ApiAuth.access_id(signed_request)
255
+ ApiAuth.access_id(signed_request)
164
256
  ```
165
257
 
166
258
  Once you've looked up the client's record via the access id, you can then verify
@@ -172,12 +264,12 @@ Here's a sample method that can be used in a `before_action` if your server is a
172
264
  Rails app:
173
265
 
174
266
  ``` ruby
175
- before_action :api_authenticate
267
+ before_action :api_authenticate
176
268
 
177
- def api_authenticate
178
- @current_account = Account.find_by_access_id(ApiAuth.access_id(request))
179
- head(:unauthorized) unless @current_account && ApiAuth.authentic?(request, @current_account.secret_key)
180
- end
269
+ def api_authenticate
270
+ @current_account = Account.find_by_access_id(ApiAuth.access_id(request))
271
+ head(:unauthorized) unless @current_account && ApiAuth.authentic?(request, @current_account.secret_key)
272
+ end
181
273
  ```
182
274
 
183
275
  ## Development
@@ -188,7 +280,17 @@ take care of all that for you.
188
280
 
189
281
  To run the tests:
190
282
 
191
- rake spec
283
+ Install the dependencies for a particular Rails version by specifying a gemfile in `gemfiles` directory:
284
+
285
+ ```sh
286
+ BUNDLE_GEMFILE=gemfiles/rails_5.gemfile bundle install
287
+ ```
288
+
289
+ Run the tests with those dependencies:
290
+
291
+ ```sh
292
+ BUNDLE_GEMFILE=gemfiles/rails_5.gemfile bundle exec rake
293
+ ```
192
294
 
193
295
  If you'd like to add support for additional HTTP clients, check out the already
194
296
  implemented drivers in `lib/api_auth/request_drivers` for reference. All of
@@ -196,8 +298,9 @@ the public methods for each driver are required to be implemented by your driver
196
298
 
197
299
  ## Authors
198
300
 
199
- * [Mauricio Gomes](http://github.com/mgomes)
200
- * [Kevin Glowacz](http://github.com/kjg)
301
+ * [Mauricio Gomes](https://github.com/mgomes)
302
+ * [Kevin Glowacz](https://github.com/kjg)
303
+ * [Florian Wininger](https://github.com/fwininger)
201
304
 
202
305
  ## Copyright
203
306
 
data/Rakefile CHANGED
@@ -10,4 +10,4 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
10
10
  spec.pattern = FileList['spec/**/*_spec.rb']
11
11
  end
12
12
 
13
- task :default => :spec
13
+ task default: :spec
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.0
1
+ 2.6.0
data/api_auth.gemspec CHANGED
@@ -1,30 +1,42 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
1
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
3
2
 
4
3
  Gem::Specification.new do |s|
5
- s.name = %q{api-auth}
6
- s.summary = %q{Simple HMAC authentication for your APIs}
7
- s.description = %q{Full HMAC auth implementation for use in your gems and Rails apps.}
8
- s.homepage = %q{https://github.com/mgomes/api_auth}
4
+ s.name = 'api-auth'
5
+ s.summary = 'Simple HMAC authentication for your APIs'
6
+ s.description = 'Full HMAC auth implementation for use in your gems and Rails apps.'
7
+ s.homepage = 'https://github.com/mgomes/api_auth'
9
8
  s.version = File.read(File.join(File.dirname(__FILE__), 'VERSION'))
10
- s.authors = ["Mauricio Gomes"]
11
- s.email = "mauricio@edge14.com"
9
+ s.authors = ['Mauricio Gomes']
10
+ s.email = 'mauricio@edge14.com'
11
+ s.license = 'MIT'
12
12
 
13
- s.add_development_dependency "appraisal"
14
- s.add_development_dependency "rake"
15
- s.add_development_dependency "amatch"
16
- s.add_development_dependency "rspec", "~> 3.4"
17
- s.add_development_dependency "actionpack", "< 5.0", "> 2.3.2"
18
- s.add_development_dependency "activesupport", "< 5.0", "> 2.3.2"
19
- s.add_development_dependency "activeresource", "~> 4.0"
20
- s.add_development_dependency "rest-client", "~> 1.6.0"
21
- s.add_development_dependency "curb", "~> 0.8.1"
22
- s.add_development_dependency "httpi"
23
- s.add_development_dependency "faraday"
24
- s.add_development_dependency "multipart-post", "~> 2.0"
13
+ s.metadata = {
14
+ 'rubygems_mfa_required' => 'true'
15
+ }
16
+
17
+ s.required_ruby_version = '>= 2.6.0'
18
+
19
+ s.add_development_dependency 'actionpack', '>= 6.0'
20
+ s.add_development_dependency 'activeresource', '>= 4.0'
21
+ s.add_development_dependency 'activesupport', '>= 6.0'
22
+ s.add_development_dependency 'amatch'
23
+ s.add_development_dependency 'appraisal'
24
+ s.add_development_dependency 'curb', '~> 1.0'
25
+ # DRb is required for Ruby 3.4+ but must avoid 2.0.6 which breaks Ruby 2.6
26
+ s.add_development_dependency 'drb', '>= 2.0.4', '< 2.0.6'
27
+ s.add_development_dependency 'faraday', '>= 1.1.0'
28
+ s.add_development_dependency 'grape', '~> 2.0'
29
+ s.add_development_dependency 'http'
30
+ s.add_development_dependency 'httpi'
31
+ s.add_development_dependency 'multipart-post', '~> 2.0'
32
+ s.add_development_dependency 'pry'
33
+ s.add_development_dependency 'rake'
34
+ s.add_development_dependency 'rest-client', '~> 2.0'
35
+ s.add_development_dependency 'rexml'
36
+ s.add_development_dependency 'rspec', '~> 3.4'
37
+ s.add_development_dependency 'rubocop', '~> 1.50'
25
38
 
26
39
  s.files = `git ls-files`.split("\n")
27
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
- s.require_paths = ["lib"]
40
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
41
+ s.require_paths = ['lib']
30
42
  end
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "actionpack", "~> 6.0"
6
+ gem "activeresource", "~> 5.1"
7
+ gem "activesupport", "~> 6.0"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "actionpack", "~> 6.1"
6
+ gem "activeresource", "~> 5.1"
7
+ gem "activesupport", "~> 6.1"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "actionpack", "~> 7.0"
6
+ gem "activeresource", "~> 6.0"
7
+ gem "activesupport", "~> 7.0"
8
+
9
+ gemspec path: "../"
data/lib/api-auth.rb CHANGED
@@ -1,2 +1,2 @@
1
1
  # So you can require "api-auth" instead of "api_auth"
2
- require "api_auth"
2
+ require 'api_auth'
data/lib/api_auth/base.rb CHANGED
@@ -1,4 +1,3 @@
1
- # encoding: UTF-8
2
1
  # api-auth is a Ruby gem designed to be used both in your client and server
3
2
  # HTTP-based applications. It implements the same authentication methods (HMAC)
4
3
  # used by Amazon Web Services.
@@ -8,9 +7,7 @@
8
7
  # Rails ActiveResource, it will integrate with that. It will even generate the
9
8
  # secret keys necessary for your clients to sign their requests.
10
9
  module ApiAuth
11
-
12
10
  class << self
13
-
14
11
  include Helpers
15
12
 
16
13
  # Signs an HTTP request using the client's access id and secret key.
@@ -23,9 +20,9 @@ module ApiAuth
23
20
  #
24
21
  # secret_key: assigned secret key that is known to both parties
25
22
  def sign!(request, access_id, secret_key, options = {})
26
- options = { :override_http_method => nil, :with_http_method => false }.merge(options)
23
+ options = { override_http_method: nil, digest: 'sha1' }.merge(options)
27
24
  headers = Headers.new(request)
28
- headers.calculate_md5
25
+ headers.calculate_hash
29
26
  headers.set_date
30
27
  headers.sign_header auth_header(headers, access_id, secret_key, options)
31
28
  end
@@ -34,14 +31,19 @@ module ApiAuth
34
31
  # secret key. Returns true if the request is authentic and false otherwise.
35
32
  def authentic?(request, secret_key, options = {})
36
33
  return false if secret_key.nil?
37
- options = { :override_http_method => nil }.merge(options)
38
34
 
39
- headers = Headers.new(request)
40
- if headers.md5_mismatch?
35
+ options = { override_http_method: nil, authorize_md5: false }.merge(options)
36
+
37
+ headers = Headers.new(request, authorize_md5: options[:authorize_md5])
38
+
39
+ # 900 seconds is 15 minutes
40
+ clock_skew = options.fetch(:clock_skew, 900)
41
+
42
+ if headers.content_hash_mismatch?
41
43
  false
42
44
  elsif !signatures_match?(headers, secret_key, options)
43
45
  false
44
- elsif request_too_old?(headers)
46
+ elsif !request_within_time_window?(headers, clock_skew)
45
47
  false
46
48
  else
47
49
  true
@@ -52,7 +54,7 @@ module ApiAuth
52
54
  def access_id(request)
53
55
  headers = Headers.new(request)
54
56
  if match_data = parse_auth_header(headers.authorization_header)
55
- return match_data[1]
57
+ return match_data[2]
56
58
  end
57
59
 
58
60
  nil
@@ -67,50 +69,54 @@ module ApiAuth
67
69
  b64_encode(Digest::SHA2.new(512).digest(random_bytes))
68
70
  end
69
71
 
70
- private
72
+ private
71
73
 
72
- AUTH_HEADER_PATTERN = /APIAuth ([^:]+):(.+)$/
74
+ AUTH_HEADER_PATTERN = /APIAuth(?:-HMAC-(MD5|SHA(?:1|224|256|384|512)?))? ([^:]+):(.+)$/.freeze
73
75
 
74
- def request_too_old?(headers)
75
- # 900 seconds is 15 minutes
76
- begin
77
- Time.httpdate(headers.timestamp).utc < (Time.now.utc - 900)
78
- rescue ArgumentError
79
- true
80
- end
76
+ def request_within_time_window?(headers, clock_skew)
77
+ Time.httpdate(headers.timestamp).utc > (Time.now.utc - clock_skew) &&
78
+ Time.httpdate(headers.timestamp).utc < (Time.now.utc + clock_skew)
79
+ rescue ArgumentError
80
+ false
81
81
  end
82
82
 
83
83
  def signatures_match?(headers, secret_key, options)
84
84
  match_data = parse_auth_header(headers.authorization_header)
85
85
  return false unless match_data
86
86
 
87
- options = options.merge(:with_http_method => true)
87
+ digest = match_data[1].nil? ? 'SHA1' : match_data[1].upcase
88
+ raise InvalidRequestDigest if !options[:digest].nil? && !options[:digest].casecmp(digest).zero?
89
+
90
+ options = { digest: digest }.merge(options)
88
91
 
89
- header_sig = match_data[2]
90
- calculated_sig_no_http = hmac_signature(headers, secret_key, {})
91
- calculated_sig_with_http = hmac_signature(headers, secret_key, options)
92
+ header_sig = match_data[3]
93
+ calculated_sig = hmac_signature(headers, secret_key, options)
92
94
 
93
- header_sig == calculated_sig_with_http || header_sig == calculated_sig_no_http
95
+ secure_equals?(header_sig, calculated_sig, secret_key)
94
96
  end
95
97
 
96
- def hmac_signature(headers, secret_key, options)
97
- if options[:with_http_method]
98
- canonical_string = headers.canonical_string_with_http_method(options[:override_http_method])
99
- else
100
- canonical_string = headers.canonical_string
101
- end
98
+ def secure_equals?(m1, m2, key)
99
+ sha1_hmac(key, m1) == sha1_hmac(key, m2)
100
+ end
101
+
102
+ def sha1_hmac(key, message)
102
103
  digest = OpenSSL::Digest.new('sha1')
104
+ OpenSSL::HMAC.digest(digest, key, message)
105
+ end
106
+
107
+ def hmac_signature(headers, secret_key, options)
108
+ canonical_string = headers.canonical_string(options[:override_http_method], options[:headers_to_sign])
109
+ digest = OpenSSL::Digest.new(options[:digest])
103
110
  b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
104
111
  end
105
112
 
106
113
  def auth_header(headers, access_id, secret_key, options)
107
- "APIAuth #{access_id}:#{hmac_signature(headers, secret_key, options)}"
114
+ hmac_string = "-HMAC-#{options[:digest].upcase}" unless options[:digest] == 'sha1'
115
+ "APIAuth#{hmac_string} #{access_id}:#{hmac_signature(headers, secret_key, options)}"
108
116
  end
109
117
 
110
118
  def parse_auth_header(auth_header)
111
119
  AUTH_HEADER_PATTERN.match(auth_header)
112
120
  end
113
-
114
- end # class methods
115
-
116
- end # ApiAuth
121
+ end
122
+ end
@@ -1,9 +1,10 @@
1
1
  module ApiAuth
2
-
3
2
  # :nodoc:
4
3
  class ApiAuthError < StandardError; end
5
-
4
+
6
5
  # Raised when the HTTP request object passed is not supported
7
6
  class UnknownHTTPRequest < ApiAuthError; end
8
-
7
+
8
+ # Raised when the client request digest is not the same as the server
9
+ class InvalidRequestDigest < ApiAuthError; end
9
10
  end