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.
- checksums.yaml +5 -5
- data/.github/workflows/main.yml +71 -0
- data/.gitignore +13 -44
- data/.rubocop.yml +39 -0
- data/.rubocop_todo.yml +83 -0
- data/Appraisals +12 -36
- data/CHANGELOG.md +75 -1
- data/README.md +155 -52
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/api_auth.gemspec +35 -23
- data/gemfiles/rails_60.gemfile +9 -0
- data/gemfiles/rails_61.gemfile +9 -0
- data/gemfiles/rails_70.gemfile +9 -0
- data/lib/api-auth.rb +1 -1
- data/lib/api_auth/base.rb +41 -35
- data/lib/api_auth/errors.rb +4 -3
- data/lib/api_auth/headers.rb +38 -42
- data/lib/api_auth/helpers.rb +7 -16
- data/lib/api_auth/railtie.rb +34 -74
- data/lib/api_auth/request_drivers/action_controller.rb +27 -27
- data/lib/api_auth/request_drivers/action_dispatch.rb +0 -6
- data/lib/api_auth/request_drivers/curb.rb +16 -21
- data/lib/api_auth/request_drivers/faraday.rb +25 -34
- data/lib/api_auth/request_drivers/faraday_env.rb +102 -0
- data/lib/api_auth/request_drivers/grape_request.rb +87 -0
- data/lib/api_auth/request_drivers/http.rb +96 -0
- data/lib/api_auth/request_drivers/httpi.rb +22 -27
- data/lib/api_auth/request_drivers/net_http.rb +21 -26
- data/lib/api_auth/request_drivers/rack.rb +23 -28
- data/lib/api_auth/request_drivers/rest_client.rb +24 -29
- data/lib/api_auth.rb +4 -0
- data/lib/faraday/api_auth/middleware.rb +35 -0
- data/lib/faraday/api_auth.rb +8 -0
- data/spec/api_auth_spec.rb +135 -96
- data/spec/faraday_middleware_spec.rb +17 -0
- data/spec/headers_spec.rb +148 -108
- data/spec/helpers_spec.rb +8 -10
- data/spec/railtie_spec.rb +80 -99
- data/spec/request_drivers/action_controller_spec.rb +122 -79
- data/spec/request_drivers/action_dispatch_spec.rb +212 -85
- data/spec/request_drivers/curb_spec.rb +36 -33
- data/spec/request_drivers/faraday_env_spec.rb +188 -0
- data/spec/request_drivers/faraday_spec.rb +87 -83
- data/spec/request_drivers/grape_request_spec.rb +280 -0
- data/spec/request_drivers/http_spec.rb +190 -0
- data/spec/request_drivers/httpi_spec.rb +59 -59
- data/spec/request_drivers/net_http_spec.rb +70 -66
- data/spec/request_drivers/rack_spec.rb +101 -97
- data/spec/request_drivers/rest_client_spec.rb +218 -144
- data/spec/spec_helper.rb +15 -12
- metadata +144 -83
- data/.travis.yml +0 -40
- data/Gemfile.lock +0 -115
- data/gemfiles/rails_23.gemfile +0 -9
- data/gemfiles/rails_23.gemfile.lock +0 -70
- data/gemfiles/rails_30.gemfile +0 -9
- data/gemfiles/rails_30.gemfile.lock +0 -92
- data/gemfiles/rails_31.gemfile +0 -9
- data/gemfiles/rails_31.gemfile.lock +0 -98
- data/gemfiles/rails_32.gemfile +0 -9
- data/gemfiles/rails_32.gemfile.lock +0 -97
- data/gemfiles/rails_4.gemfile +0 -9
- data/gemfiles/rails_4.gemfile.lock +0 -94
- data/gemfiles/rails_41.gemfile +0 -9
- data/gemfiles/rails_41.gemfile.lock +0 -98
- data/gemfiles/rails_42.gemfile +0 -9
- data/gemfiles/rails_42.gemfile.lock +0 -115
data/README.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# ApiAuth
|
|
2
2
|
|
|
3
|
-
[ for security update information
|
|
3
|
+
[](https://github.com/mgomes/api_auth/actions)
|
|
4
|
+
[](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-
|
|
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
|
|
26
|
-
content-
|
|
27
|
-
timestamp isn't present, a valid HTTP date is
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
```ruby
|
|
45
|
+
Authorization = APIAuth "#{client access id}:#{signature from step 2}"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A cURL request would look like:
|
|
38
49
|
|
|
39
|
-
|
|
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](
|
|
50
|
-
* [SHA-
|
|
51
|
-
* [HMAC algorithm](
|
|
52
|
-
* [RFC 2104 (HMAC)](
|
|
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
|
-
|
|
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
|
-
```
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
```
|
|
101
|
-
|
|
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
|
-
```
|
|
115
|
-
|
|
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
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
###
|
|
176
|
+
### Flexirest
|
|
132
177
|
|
|
133
|
-
ApiAuth also works with [
|
|
134
|
-
Simply add this configuration to your
|
|
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
|
-
```
|
|
137
|
-
|
|
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
|
-
```
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
before_action :api_authenticate
|
|
176
268
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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](
|
|
200
|
-
* [Kevin Glowacz](
|
|
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
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2.6.0
|
data/api_auth.gemspec
CHANGED
|
@@ -1,30 +1,42 @@
|
|
|
1
|
-
|
|
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 =
|
|
6
|
-
s.summary =
|
|
7
|
-
s.description =
|
|
8
|
-
s.homepage =
|
|
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 = [
|
|
11
|
-
s.email =
|
|
9
|
+
s.authors = ['Mauricio Gomes']
|
|
10
|
+
s.email = 'mauricio@edge14.com'
|
|
11
|
+
s.license = 'MIT'
|
|
12
12
|
|
|
13
|
-
s.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
s.
|
|
18
|
-
|
|
19
|
-
s.add_development_dependency
|
|
20
|
-
s.add_development_dependency
|
|
21
|
-
s.add_development_dependency
|
|
22
|
-
s.add_development_dependency
|
|
23
|
-
s.add_development_dependency
|
|
24
|
-
s.add_development_dependency
|
|
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.
|
|
28
|
-
s.
|
|
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
|
data/lib/api-auth.rb
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# So you can require "api-auth" instead of "api_auth"
|
|
2
|
-
require
|
|
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 = { :
|
|
23
|
+
options = { override_http_method: nil, digest: 'sha1' }.merge(options)
|
|
27
24
|
headers = Headers.new(request)
|
|
28
|
-
headers.
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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[
|
|
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
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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[
|
|
90
|
-
|
|
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
|
|
95
|
+
secure_equals?(header_sig, calculated_sig, secret_key)
|
|
94
96
|
end
|
|
95
97
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
end # ApiAuth
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/api_auth/errors.rb
CHANGED
|
@@ -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
|