warden-hmac-authentication 0.2.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.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2011 Florian Gilcher <florian.gilcher@asquera.de>, Felix Gilcher <felix.gilcher@asquera.de>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # HMAC
2
+
3
+ This gem provides request authentication via [HMAC](http://en.wikipedia.org/wiki/Hmac). The main usage is request based, noninteractive
4
+ authentication for API implementations. Two strategies are supported that differ mainly in how the authentication information is
5
+ transferred to the server: One header-based authentication method and one query-based. The authentication scheme is in some parts based
6
+ on ideas laid out in this article and the following discussion:
7
+ http://broadcast.oreilly.com/2009/12/principles-for-standardized-rest-authentication.html
8
+
9
+ The gem also provides a small helper class that can be used to generate request signatures.
10
+
11
+ ## Header-Based authentication
12
+
13
+ The header-based authentication transports the authentication information in the (misnamed) `Authorization` HTTP-Header. The primary
14
+ advantage of header-based authentication is that request urls are stable even if authentication information changes. The improves
15
+ cacheability of the resource.
16
+
17
+ Header-based authentication is supported by the `:hmac_header` strategy.
18
+
19
+ ## Query-Based authentication
20
+
21
+ Query-Based authentication encodes all authentication in the query string. Query-based authentication has unique advantages in
22
+ scenarios with little or no control over the request headers such as pre-generating and embedding a signed URL in a web-page or
23
+ similar cases. However, resources requested using query-based authentication cannot be cached since the request URL changes for
24
+ every request.
25
+ All information related to authentication is passed as a single hash in one single query parameter to minimize collisions with other
26
+ query parameters. The name of the query parameter defaults to `auth` and can be controlled using the `:auth_parameter` config option.
27
+ Query-based authentication takes optional headers into account if they are present in the request.
28
+
29
+ Query-based authentication is supported by the `:hmac_query` strategy.
30
+
31
+ ## Shared secret
32
+
33
+ Both strategies use a secret that is shared between the server and the client to calculate the signature. The secret must be
34
+ configured when registering the strategy. For simple cases a single secret may be sufficient but most real-world scenarios
35
+ will require a different secret for each possible client. Such cases can be managed by passing a Proc as secret. An empty
36
+ secret (empty string or nil) will trigger authentication failure.
37
+
38
+
39
+ ## Warden strategy usage
40
+
41
+ Both strategies can be used at the same time and will not interfere with each other. It is advisable to attempt query-based
42
+ authentication first to reduce the chance that a stray Authorization header triggers header-based authentication. Both strategies
43
+ read additional configuration from a hash named :hmac in the warden scope.
44
+
45
+ Configure the HMAC warden strategy:
46
+
47
+ use Warden::Manager do |manager|
48
+ manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
49
+ # other scopes
50
+ manager.scope_defaults :hmac, :strategies => [:hmac_query, :hmac_header],
51
+ :hmac => {
52
+ :secret => "secrit"
53
+ }
54
+ end
55
+
56
+
57
+
58
+ ### Retrieving the secret from a database or other storage
59
+
60
+ If you want to retrieve the secret and token using a different strategy, either extend the HMAC strategy:
61
+
62
+ class Warden::Strategies::HMACQuery < Warden::Strategies::HMACBase
63
+ def retrieve_user
64
+ User.get(request[:user_id])
65
+ end
66
+
67
+ def secret
68
+ retrieve_user.secret
69
+ end
70
+ end
71
+
72
+ or use a Proc that retrieves the secret.
73
+
74
+ use Warden::Manager do |manager|
75
+ manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
76
+ # other scopes
77
+ manager.scope_defaults :hmac, :strategies => [:hmac_query, :hmac_header],
78
+ :store => false,
79
+ :hmac => {
80
+ :secret => Proc.new {|strategy|
81
+ "secret"
82
+ }
83
+ }
84
+ end
85
+
86
+ ### Controlling the HMAC algorithm
87
+
88
+ The algorithm can be controlled using the `:algorithm` option:
89
+
90
+ use Warden::Manager do |manager|
91
+ manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
92
+ # other scopes
93
+ manager.scope_defaults :hmac, :strategies => [:hmac_query, :hmac_header],
94
+ :hmac => {
95
+ :secret => "secrit",
96
+ :algorithm => "md5"
97
+ }
98
+ end
99
+
100
+ The algorithm defaults to SHA1.
101
+
102
+ ## Auth Scheme Name
103
+
104
+ The name of the authentication scheme is primarily used for header authentication. It is used to construct the `Authorization` header and
105
+ must thus avoid names that are reserved for existing standardized authentication schemes such as `Basic` and `Digest`. The scheme
106
+ name is also used to construct the default values for various header names. The authentication scheme name defaults to `HMAC`
107
+
108
+ use Warden::Manager do |manager|
109
+ manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
110
+ # other scopes
111
+ manager.scope_defaults :hmac, :strategies => [:hmac_query, :hmac_header],
112
+ :hmac => {
113
+ :secret => "secrit",
114
+ :auth_scheme_name => "MyScheme"
115
+ }
116
+ end
117
+
118
+ No authentication attempt is made if the scheme name in the `Authorization` header does not match the configured scheme name.
119
+
120
+ ## Optional nonce
121
+
122
+ An optional nonce can be passed in the request to increase security. The nonce is not limited to digits and can be any string. It's
123
+ advisable to limit the length of the nonce to a reasonable value. If a nonce is used it should be changed with every request. The
124
+ default header for the nonce is `X-#{auth-scheme-name}-Nonce` (`X-HMAC-Nonce`). The header name can be controlled using the `:nonce_header`
125
+ configuration option.
126
+
127
+ The `:require_nonce` configuration can be set to `true` to enforce a nonce. If a nonce is required no authentication attempt will be
128
+ made for requests not providing a nonce.
129
+
130
+ use Warden::Manager do |manager|
131
+ manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
132
+ # other scopes
133
+ manager.scope_defaults :hmac, :strategies => [:hmac_query, :hmac_header],
134
+ :hmac => {
135
+ :secret => "secrit",
136
+ :require_nonce => true
137
+ }
138
+ end
139
+
140
+
141
+ ## Required headers and parameters
142
+
143
+ Required headers and parameters must be present for a successful authentication attempt. The list of required headers defaults to
144
+ the `Authorization` header for header-based authentication and is empty for query-based authentication. The list of required
145
+ parameters defaults to the chosen authentication parameter for query-based authentication and is empty for header-based authentication.
146
+ If a required parameter or header is not included in the request, no authentication attempt will be made for the strategy.
147
+
148
+ ## Other optional headers
149
+
150
+ Some headers are optional but should be included in the signature of the request if present. The default list of optional headers
151
+ includes `Content-MD5` and `Content-Type`. The list of optional headers can be configured using the `:optional_headers` config option.
152
+ Optional headers are always included in the canonical representation if they are found in the request and not blank. Optional headers
153
+ will be included in the canonical representation for query-based authentication if they are present in the request so be careful
154
+ not to include any header that is out of your clients control.
155
+
156
+ ## Date and TTL
157
+
158
+ It is good practice to enforce a max-age for tokens. The hmac strategy allows this via the `ttl` parameter. It controls the max age
159
+ of tokens in seconds and defaults to 900 seconds. Pass `nil` as ttl value to disable TTL checking.
160
+
161
+ The timestamp of the request is usually passed in the `Date` HTTP-Header. However, since some HTTP-Client libraries do not allow
162
+ setting the Date header another header may be used to override the `Date` header. The name of this header can be controlled via the
163
+ `:alternate_date_header` option and defaults to `X-#{auth-scheme-name}-Date` (`X-HMAC-Date`).
164
+
165
+ The date must be formatted as HTTP-Date according to RFC 1123, section 5.2.14 and should be provided in GMT time.
166
+
167
+ Example: Setting the ttl to 300 seconds:
168
+
169
+ use Warden::Manager do |manager|
170
+ manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
171
+ # other scopes
172
+ manager.scope_defaults :token, :strategies => [:hmac_query, :hmac_header],
173
+ :hmac => {
174
+ :secret => "secrit",
175
+ :ttl => 300 # make tokens valid for 5 minutes
176
+ }
177
+ end
178
+
179
+ ### Clock Skew
180
+
181
+ The TTL allows for a little clock skew to accommodate servers that are slightly running off time. The allowed clock skew can be
182
+ controlled with the `:clockskew` option and defaults to 5 seconds.
183
+
184
+
185
+ ## Canonical representation
186
+
187
+ Both request methods use a canonical representation of the request together with the shared secret to calculate a signature
188
+ that authenticates the request. The canonical representation is calculated using the following algorithm:
189
+
190
+ * Start with the empty string ("")
191
+ * Add the HTTP-Verb for the request ("GET", "POST", ...) in capital letters, followed by a single newline (U+000A).
192
+ * Add the date for the request using the form "date:#{date-of-request}" followed by a single newline. The date for the signature must be
193
+ formatted exactly as in the request.
194
+ * Add the nonce for the request in the form "nonce:#{nonce-in-request}" followed by a single newline. If no nonce is passed use the
195
+ empty string as nonce value.
196
+ * Convert all remaining header names to lowercase.
197
+ * Sort the remaining headers lexicographically by header name.
198
+ * Trim header values by removing any whitespace before the first non-whitespace character and after the last non-whitespace character.
199
+ * Combine lowercase header names and header values using a single colon (“:”) as separator. Do not include whitespace characters
200
+ around the separator.
201
+ * Combine all headers using a single newline (U+000A) character and append them to the canonical representation,
202
+ followed by a single newline (U+000A) character.
203
+ * Append the url-decoded query path to the canonical representation
204
+ * URL-decode query parameters if required
205
+ * If using query-based authentication: Remove all authentication-related parameters from the query parameters.
206
+ * Sort all query parameters lexicographically by parameter name and join them, using a single ampersand (“&”) as separator
207
+ * Append the query string using a single question mark (“?”) as separator unless the query string is empty
208
+
209
+ ### Examples
210
+
211
+ Given the following request:
212
+
213
+ GET /example/resource.html?sort=header%20footer&order=ASC HTTP/1.1
214
+ Host: www.example.org
215
+ Date: Mon, 20 Jun 2011 12:06:11 GMT
216
+ User-Agent: curl/7.20.0 (x86_64-pc-linux-gnu) libcurl/7.20.0 OpenSSL/1.0.0a zlib/1.2.3
217
+ X-MAC-Nonce: Thohn2Mohd2zugoo
218
+
219
+ The canonical representation is:
220
+
221
+ GET\n
222
+ date:Mon, 20 Jun 2011 12:06:11 GMT\n
223
+ nonce:Thohn2Mohd2zugo\n
224
+ /example/resource.html?order=ASC&sort=header footer
225
+
226
+
227
+ Given the following request:
228
+
229
+ GET /example/resource.html?sort=header%20footer&order=ASC HTTP/1.1
230
+ Host: www.example.org
231
+ Date: Mon, 20 Jun 2011 12:06:11 GMT
232
+ User-Agent: curl/7.20.0 (x86_64-pc-linux-gnu) libcurl/7.20.0 OpenSSL/1.0.0a zlib/1.2.3
233
+ X-MAC-Nonce: Thohn2Mohd2zugoo
234
+ X-MAC-Date: Mon, 20 Jun 2011 14:06:57 GMT
235
+
236
+ The canonical representation is:
237
+
238
+ GET\n
239
+ date:Mon, 20 Jun 2011 14:06:57 GMT\n
240
+ nonce:Thohn2Mohd2zugo\n
241
+ /example/resource.html?order=ASC&sort=header footer
242
+
243
+
244
+ ### Generating the canonical representation for query-based authentication
245
+
246
+ The canonical representation for query-based authentication is generated using the same algorithm as for header-based authentication, but some
247
+ of the values are retrieved from the query string instead of the respective headers. All query parameters related to authentication
248
+ must be removed from the query string before generating the canonical representation.
249
+
250
+ #### Example
251
+
252
+ Given the following request:
253
+
254
+ GET /example/resource.html?page=3&order=id%2casc&auth%5Bnonce%5D=foLiequei7oosaiWun5aoy8oo&auth%5Bdate%5D=Mon%2C+20+Jun+2011+14%3A06%3A57+GMT HTTP/1.1
255
+ Host: www.example.org
256
+ Date: Mon, 20 Jun 2011 12:06:11 GMT
257
+ User-Agent: curl/7.20.0 (x86_64-pc-linux-gnu) libcurl/7.20.0 OpenSSL/1.0.0a zlib/1.2.3
258
+
259
+ The canonical representation is:
260
+
261
+ GET\n
262
+ date:Mon, 20 Jun 2011 14:06:57 GMT\n
263
+ nonce:foLiequei7oosaiWun5aoy8oo\n
264
+ /example/resource.html?order=id,asc&page=3
265
+
266
+
267
+ ## HMACSigner usage
268
+
269
+ The HMACSigner class can be used to validate and generate signatures for a given request. Most methods accept a hash as an intermediate
270
+ representation of the request but some methods accept and operate on full urls.
271
+
272
+ h = HMACSigner.new
273
+ h.sign_url('http://example.org/example.html', 'secret')
274
+ h.validate_url_signature('http://example.org/example.html?auth[signature]=foo', 'secret')
275
+
276
+ ## Licence
277
+
278
+ Copyright (c) 2011 Florian Gilcher <florian.gilcher@asquera.de>, Felix Gilcher <felix.gilcher@asquera.de>
279
+
280
+ Permission is hereby granted, free of charge, to any person obtaining
281
+ a copy of this software and associated documentation files (the
282
+ "Software"), to deal in the Software without restriction, including
283
+ without limitation the rights to use, copy, modify, merge, publish,
284
+ distribute, sublicense, and/or sell copies of the Software, and to
285
+ permit persons to whom the Software is furnished to do so, subject to
286
+ the following conditions:
287
+
288
+ The above copyright notice and this permission notice shall be
289
+ included in all copies or substantial portions of the Software.
290
+
291
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
292
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
293
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
294
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
295
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
296
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
297
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
298
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new(:test) do |test|
4
+ test.libs << 'test'
5
+ test.pattern = 'test/**/*_test.rb'
6
+ test.verbose = true
7
+ test.ruby_opts = ['-rubygems', '-rtest_helper']
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,234 @@
1
+ require 'addressable/uri'
2
+ require 'openssl'
3
+ require 'rack/utils'
4
+
5
+
6
+ # Helper class that provides signing capabilites for the hmac strategies.
7
+ #
8
+ # @author Felix Gilcher <felix.gilcher@asquera.de>
9
+ class HMACSigner
10
+ attr_accessor :secret, :algorithm, :default_opts
11
+
12
+ # create a new HMAC instance
13
+ #
14
+ # @param [String] algorithm The hashing-algorithm to use. See the openssl documentation for valid values.
15
+ # @param [Hash] default_opts The default options for all calls that take opts
16
+ #
17
+ # @option default_opts [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
18
+ # @option default_opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
19
+ # @option default_opts [String] :auth_header ('Authorization') The name of the authorization header to use
20
+ # @option default_opts [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature.
21
+ # @option default_opts [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
22
+ # @option default_opts [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
23
+ # @option default_opts [Bool] :query_based (false) Whether to use query based authentication
24
+ # @option default_opts [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
25
+ #
26
+ def initialize(algorithm = "sha1", default_opts = {})
27
+ self.algorithm = algorithm
28
+ self.default_opts = {
29
+ :auth_scheme => "HMAC",
30
+ :auth_param => "auth",
31
+ :auth_header => "Authorization",
32
+ :auth_header_format => "%{auth_scheme} %{signature}",
33
+ :nonce_header => "X-%{scheme}-Nonce" % {:scheme => (default_opts[:auth_scheme] || "HMAC")},
34
+ :alternate_date_header => "X-%{scheme}-Date" % {:scheme => (default_opts[:auth_scheme] || "HMAC")},
35
+ :query_based => false,
36
+ :use_alternate_date_header => false
37
+ }.merge(default_opts)
38
+
39
+ end
40
+
41
+ # Generate the signature from a hash representation
42
+ #
43
+ # @param [Hash] params the parameters to create the representation with
44
+ # @option params [String] :secret The secret to generate the signature with
45
+ # @option params [String] :method The HTTP Verb of the request
46
+ # @option params [String] :date The date of the request as it was formatted in the request
47
+ # @option params [String] :nonce ('') The nonce given in the request
48
+ # @option params [String] :path The path portion of the request
49
+ # @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param.
50
+ # @option params [Hash] :headers ({}) All headers given in the request (optional and required)
51
+ # @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
52
+ # @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
53
+ # @option params [String] :auth_header ('Authorization') The name of the authorization header to use
54
+ # @option params [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature.
55
+ # @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
56
+ # @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
57
+ # @option params [Bool] :query_based (false) Whether to use query based authentication
58
+ # @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
59
+ #
60
+ # @return [String] the signature
61
+ def generate_signature(params)
62
+ secret = params.delete(:secret)
63
+ OpenSSL::HMAC.hexdigest(algorithm, secret, canonical_representation(params))
64
+ end
65
+
66
+ # compares the given signature with the signature created from a hash representation
67
+ #
68
+ # @param [String] signature the signature to compare with
69
+ # @param [Hash] params the parameters to create the representation with
70
+ # @option params [String] :secret The secret to generate the signature with
71
+ # @option params [String] :method The HTTP Verb of the request
72
+ # @option params [String] :date The date of the request as it was formatted in the request
73
+ # @option params [String] :nonce ('') The nonce given in the request
74
+ # @option params [String] :path The path portion of the request
75
+ # @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param.
76
+ # @option params [Hash] :headers ({}) All headers given in the request (optional and required)
77
+ # @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
78
+ # @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
79
+ # @option params [String] :auth_header ('Authorization') The name of the authorization header to use
80
+ # @option params [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature.
81
+ # @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
82
+ # @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
83
+ # @option params [Bool] :query_based (false) Whether to use query based authentication
84
+ # @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
85
+ #
86
+ # @return [Bool] true if the signature matches
87
+ def validate_signature(signature, params)
88
+ signature == generate_signature(params)
89
+ end
90
+
91
+ # convienience method to check the signature of a url with query-based authentication
92
+ #
93
+ # @param [String] url the url to test
94
+ # @param [String] secret the secret used to sign the url
95
+ # @param [Hash] opts Options controlling the singature generation
96
+ #
97
+ # @option opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
98
+ #
99
+ # @return [Bool] true if the signature is valid
100
+ def validate_url_signature(url, secret, opts = {})
101
+ opts = default_opts.merge(opts)
102
+ opts[:query_based] = true
103
+
104
+ uri = Addressable::URI.parse(url)
105
+ query_values = uri.query_values
106
+ auth_params = query_values.delete(opts[:auth_param])
107
+
108
+ date = auth_params["date"]
109
+ nonce = auth_params["nonce"]
110
+ validate_signature(auth_params["signature"], :secret => secret, :method => "GET", :path => uri.path, :date => date, :nonce => nonce, :query => query_values, :headers => {})
111
+ end
112
+
113
+ # generates the canonical representation for a given request
114
+ #
115
+ # @param [Hash] params the parameters to create the representation with
116
+ # @option params [String] :method The HTTP Verb of the request
117
+ # @option params [String] :date The date of the request as it was formatted in the request
118
+ # @option params [String] :nonce ('') The nonce given in the request
119
+ # @option params [String] :path The path portion of the request
120
+ # @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param.
121
+ # @option params [Hash] :headers ({}) All headers given in the request (optional and required)
122
+ # @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
123
+ # @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
124
+ # @option params [String] :auth_header ('Authorization') The name of the authorization header to use
125
+ # @option params [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature.
126
+ # @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
127
+ # @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
128
+ # @option params [Bool] :query_based (false) Whether to use query based authentication
129
+ # @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
130
+ #
131
+ # @return [String] the canonical representation
132
+ def canonical_representation(params)
133
+ rep = ""
134
+
135
+ rep << "#{params[:method].upcase}\n"
136
+ rep << "date:#{params[:date]}\n"
137
+ rep << "nonce:#{params[:nonce]}\n"
138
+
139
+ (params[:headers] || {}).sort.each do |pair|
140
+ name,value = *pair
141
+ rep << "#{name.downcase}:#{value}\n"
142
+ end
143
+
144
+ rep << params[:path]
145
+
146
+ p = (params[:query] || {}).dup
147
+
148
+ if !p.empty?
149
+ query = p.sort.map do |key, value|
150
+ "%{key}=%{value}" % {
151
+ :key => Rack::Utils.unescape(key.to_s),
152
+ :value => Rack::Utils.unescape(value.to_s)
153
+ }
154
+ end.join("&")
155
+ rep << "?#{query}"
156
+ end
157
+
158
+ rep
159
+ end
160
+
161
+ # sign the given request
162
+ #
163
+ # @param [String] url The url of the request
164
+ # @param [String] secret The shared secret for the signature
165
+ # @param [Hash] opts Options for the signature generation
166
+ #
167
+ # @option opts [String] :nonce ('') The nonce to use in the signature
168
+ # @option opts [String, #strftime] :date (Time.now) The date to use in the signature
169
+ # @option opts [Hash] :headers ({}) A list of optional headers to include in the signature
170
+ #
171
+ # @option opts [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
172
+ # @option opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
173
+ # @option opts [String] :auth_header ('Authorization') The name of the authorization header to use
174
+ # @option opts [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature.
175
+ # @option opts [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
176
+ # @option opts [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
177
+ # @option opts [Bool] :query_based (false) Whether to use query based authentication
178
+ # @option opts [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
179
+ #
180
+ def sign_request(url, secret, opts = {})
181
+ opts = default_opts.merge(opts)
182
+
183
+ uri = Addressable::URI.parse(url)
184
+ headers = opts[:headers] || {}
185
+
186
+ date = opts[:date] || Time.now.gmtime
187
+ date = date.gmtime.strftime('%a, %e %b %Y %T GMT') if date.respond_to? :strftime
188
+
189
+ signature = generate_signature(:secret => secret, :method => "GET", :path => uri.path, :date => date, :nonce => opts[:nonce], :query => uri.query_values, :headers => opts[:headers])
190
+
191
+ if opts[:query_based]
192
+ auth_params = {
193
+ "date" => date,
194
+ "signature" => signature
195
+ }
196
+ auth_params[:nonce] = opts[:nonce] unless opts[:nonce].nil?
197
+
198
+ query_values = uri.query_values
199
+ query_values[opts[:auth_param]] = auth_params
200
+ uri.query_values = query_values
201
+ else
202
+ headers[opts[:auth_header]] = opts[:auth_header_format] % opts.merge({:signature => signature})
203
+ headers[opts[:nonce_header]] = opts[:nonce] unless opts[:nonce].nil?
204
+
205
+ if opts[:use_alternate_date_header]
206
+ headers[opts[:alternate_date_header]] = date
207
+ else
208
+ headers["Date"] = date
209
+ end
210
+ end
211
+
212
+ [headers, uri.to_s]
213
+ end
214
+
215
+ # convienience method to sign a url for use with query-based authentication
216
+ #
217
+ # @param [String] url the url to sign
218
+ # @param [String] secret the secret used to sign the url
219
+ # @param [Hash] opts Options controlling the singature generation
220
+ #
221
+ # @option opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
222
+ #
223
+ # @return [String] The signed url
224
+ def sign_url(url, secret, opts = {})
225
+ opts = default_opts.merge(opts)
226
+ opts[:query_based] = true
227
+
228
+ headers, url = *sign_request(url, secret, opts)
229
+ url
230
+ end
231
+
232
+
233
+ end
234
+
@@ -0,0 +1,173 @@
1
+ require 'hmac_signer'
2
+ require 'warden'
3
+
4
+
5
+ # Base class for hmac authentication in warden. Provides shared methods such as config access
6
+ # and various helpers.
7
+ #
8
+ # @author Felix Gilcher <felix.gilcher@asquera.de>
9
+ class Warden::Strategies::HMACBase < Warden::Strategies::Base
10
+
11
+
12
+ # Performs authentication. Calls success! if authentication was performed successfully and halt!
13
+ # if the authentication information is invalid.
14
+ #
15
+ # Delegates parts of the work to signature_valid? which must be implemented in child-strategies.
16
+ #
17
+ # @see https://github.com/hassox/warden/wiki/Strategies
18
+ def authenticate!
19
+ if "" == secret.to_s
20
+ debug("authentication attempt with an empty secret")
21
+ return fail!("Cannot authenticate with an empty secret")
22
+ end
23
+
24
+ if check_ttl? && !timestamp_valid?
25
+ debug("authentication attempt with an invalid timestamp. Given was #{timestamp}, expected was #{Time.now.gmtime}")
26
+ return fail!("Invalid timestamp")
27
+ end
28
+
29
+ if signature_valid?
30
+ success!(retrieve_user)
31
+ else
32
+ debug("authentication attempt with an invalid signature.")
33
+ fail!("Invalid token passed")
34
+ end
35
+ end
36
+
37
+ # Retrieve the current request method
38
+ #
39
+ # @return [String] The request method in capital letters
40
+ def request_method
41
+ env['REQUEST_METHOD'].upcase
42
+ end
43
+
44
+ # Retrieve the request query parameters
45
+ #
46
+ # @return [Hash] The query parameters
47
+ def params
48
+ request.GET
49
+ end
50
+
51
+ # Retrieve the request headers. Header names are normalized by this method by stripping
52
+ # the `HTTP_`-prefix and replacing underscores with dashes. `HTTP_X_Foo` is normalized to
53
+ # `X-Foo`.
54
+ #
55
+ # @return [Hash] The request headers
56
+ def headers
57
+ pairs = env.select {|k,v| k.start_with? 'HTTP_'}
58
+ .collect {|pair| [pair[0].sub(/^HTTP_/, '').gsub(/_/, '-'), pair[1]]}
59
+ .sort
60
+ headers = Hash[*pairs.flatten]
61
+ headers
62
+ end
63
+
64
+ # Retrieve a user from the database. Stub implementation that just returns true, needed for compat.
65
+ #
66
+ # @return [Bool] true
67
+ def retrieve_user
68
+ true
69
+ end
70
+
71
+ # Log a debug message if a logger is available.
72
+ #
73
+ # @param [String] msg The message to log
74
+ def debug(msg)
75
+ if logger
76
+ logger.debug(msg)
77
+ end
78
+ end
79
+
80
+ # Retrieve a logger. Current implementation can
81
+ # only handle Padrino loggers
82
+ #
83
+ # @return [Logger] the logger, nil if none is available
84
+ def logger
85
+ if defined? Padrino
86
+ Padrino.logger
87
+ end
88
+ end
89
+
90
+ private
91
+ def config
92
+ env["warden"].config[:scope_defaults][scope][:hmac]
93
+ end
94
+
95
+ def auth_param
96
+ config[:auth_param] || "auth"
97
+ end
98
+
99
+ def auth_header
100
+ config[:auth_header] || "Authorization"
101
+ end
102
+
103
+ def auth_scheme_name
104
+ config[:auth_scheme] || "HMAC"
105
+ end
106
+
107
+ def nonce_header_name
108
+ config[:nonce_header] || "X-#{auth_scheme_name}-Nonce"
109
+ end
110
+
111
+ def alternate_date_header_name
112
+ config[:alternate_date_header] || "X-#{auth_scheme_name}-Date"
113
+ end
114
+
115
+ def optional_headers
116
+ (config[:optional_headers] || []) + ["Content-MD5", "Content-Type"]
117
+ end
118
+
119
+ def lowercase_headers
120
+
121
+ if @lowercase_headers.nil?
122
+ tmp = headers.map do |name,value|
123
+ [name.downcase, value]
124
+ end
125
+ @lowercase_headers = Hash[*tmp.flatten]
126
+ end
127
+
128
+ @lowercase_headers
129
+ end
130
+
131
+ def hmac
132
+ HMACSigner.new(algorithm)
133
+ end
134
+
135
+ def algorithm
136
+ config[:algorithm] || "sha1"
137
+ end
138
+
139
+ def ttl
140
+ config[:ttl].to_i
141
+ end
142
+
143
+ def check_ttl?
144
+ !config[:ttl].nil?
145
+ end
146
+
147
+ def timestamp
148
+ Time.strptime(request_timestamp, '%a, %e %b %Y %T %z') unless request_timestamp.nil?
149
+ end
150
+
151
+ def has_timestamp?
152
+ !timestamp.nil?
153
+ end
154
+
155
+ def timestamp_valid?
156
+ now = Time.now.gmtime.to_i
157
+ timestamp.to_i <= (now + clockskew) && timestamp.to_i >= (now - ttl)
158
+ end
159
+
160
+ def nonce_required?
161
+ !!config[:require_nonce]
162
+ end
163
+
164
+ def secret
165
+ @secret ||= config[:secret].respond_to?(:call) ? config[:secret].call(self) : config[:secret]
166
+ end
167
+
168
+ def clockskew
169
+ (config[:clockskew] || 5)
170
+ end
171
+
172
+
173
+ end
@@ -0,0 +1,94 @@
1
+ require_relative 'base'
2
+
3
+ # Implements header-based hmac authentication for warden. The strategy is registered as
4
+ # `:hmac_header` in the warden strategy list.
5
+ #
6
+ # @author Felix Gilcher <felix.gilcher@asquera.de>
7
+ class Warden::Strategies::HMACHeader < Warden::Strategies::HMACBase
8
+
9
+ # Checks that this strategy applies. Tests that the required
10
+ # authentication information was given.
11
+ #
12
+ # @return [Bool] true if all required authentication information is available in the request
13
+ # @see https://github.com/hassox/warden/wiki/Strategies
14
+ def valid?
15
+ valid = required_headers.all? { |h| headers.include?(h) } && headers.include?("Authorization") && has_timestamp?
16
+ valid = valid && scheme_valid?
17
+ valid
18
+ end
19
+
20
+ # Check that the signature given in the request is valid.
21
+ #
22
+ # @return [Bool] true if the request is valid
23
+ def signature_valid?
24
+
25
+ #:method => "GET",
26
+ #:date => "Mon, 20 Jun 2011 12:06:11 GMT",
27
+ #:nonce => "TESTNONCE",
28
+ #:path => "/example",
29
+ #:query => {
30
+ # "foo" => "bar",
31
+ # "baz" => "foobared"
32
+ #},
33
+ #:headers => {
34
+ # "Content-Type" => "application/json;charset=utf8",
35
+ # "Content-MD5" => "d41d8cd98f00b204e9800998ecf8427e"
36
+ #}
37
+
38
+ hmac.validate_signature(given_signature, {
39
+ :secret => secret,
40
+ :method => request_method,
41
+ :date => request_timestamp,
42
+ :nonce => nonce,
43
+ :path => request.path,
44
+ :query => params,
45
+ :headers => headers.select {|name, value| optional_headers.include? name}
46
+ })
47
+ end
48
+
49
+ # retrieve the signature from the request
50
+ #
51
+ # @return [String] The signature from the request
52
+ def given_signature
53
+ headers[auth_header].split(" ")[1]
54
+ end
55
+
56
+ # retrieve the nonce from the request
57
+ #
58
+ # @return [String] The nonce or an empty string if no nonce was given in the request
59
+ def nonce
60
+ headers[nonce_header_name]
61
+ end
62
+
63
+ # retrieve the request timestamp as string
64
+ #
65
+ # @return [String] The request timestamp or an empty string if no timestamp was given in the request
66
+ def request_timestamp
67
+ headers[date_header]
68
+ end
69
+
70
+ private
71
+
72
+ def required_headers
73
+ headers = [auth_header]
74
+ headers += [nonce_header_name] if nonce_required?
75
+ headers
76
+ end
77
+
78
+ def scheme_valid?
79
+ headers[auth_header].to_s.split(" ").first == auth_scheme_name
80
+ end
81
+
82
+ def date_header
83
+ if headers.include? alternate_date_header_name
84
+ alternate_date_header_name
85
+ else
86
+ "Date"
87
+ end
88
+ end
89
+
90
+
91
+
92
+ end
93
+
94
+ Warden::Strategies.add(:hmac_header, Warden::Strategies::HMACHeader)
@@ -0,0 +1,52 @@
1
+ require_relative 'base'
2
+
3
+
4
+ # Implements query-based hmac authentication for warden. The strategy is registered as
5
+ # `:hmac_query` in the warden strategy list.
6
+ #
7
+ # @author Felix Gilcher <felix.gilcher@asquera.de>
8
+ class Warden::Strategies::HMACQuery < Warden::Strategies::HMACBase
9
+
10
+ # Checks that this strategy applies. Tests that the required
11
+ # authentication information was given.
12
+ #
13
+ # @return [Bool] true if all required authentication information is available in the request
14
+ # @see https://github.com/hassox/warden/wiki/Strategies
15
+ def valid?
16
+ valid = auth_info.include? "signature"
17
+ valid = valid && has_timestamp? if check_ttl?
18
+ valid = valid && has_nonce? if nonce_required?
19
+ valid
20
+ end
21
+
22
+ # Check that the signature given in the request is valid.
23
+ #
24
+ # @return [Bool] true if the request is valid
25
+ def signature_valid?
26
+ hmac.validate_url_signature(request.url, secret)
27
+ end
28
+
29
+ # retrieve the authentication information from the request
30
+ #
31
+ # @return [Hash] the authentication info in the request
32
+ def auth_info
33
+ params[auth_param] || {}
34
+ end
35
+
36
+ # retrieve the nonce from the request
37
+ #
38
+ # @return [String] The nonce or an empty string if no nonce was given in the request
39
+ def nonce
40
+ auth_info["nonce"] || ""
41
+ end
42
+
43
+ # retrieve the request timestamp as string
44
+ #
45
+ # @return [String] The request timestamp or an empty string if no timestamp was given in the request
46
+ def request_timestamp
47
+ auth_info["date"] || ""
48
+ end
49
+
50
+ end
51
+
52
+ Warden::Strategies.add(:hmac_query, Warden::Strategies::HMACQuery)
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: warden-hmac-authentication
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.0
6
+ platform: ruby
7
+ authors:
8
+ - Felix Gilcher
9
+ - Florian Gilcher
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2011-07-16 00:00:00 +02:00
15
+ default_executable:
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: addressable
19
+ prerelease: false
20
+ requirement: &id001 !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: "0"
26
+ type: :runtime
27
+ version_requirements: *id001
28
+ - !ruby/object:Gem::Dependency
29
+ name: rack
30
+ prerelease: false
31
+ requirement: &id002 !ruby/object:Gem::Requirement
32
+ none: false
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: "0"
37
+ type: :runtime
38
+ version_requirements: *id002
39
+ - !ruby/object:Gem::Dependency
40
+ name: yard
41
+ prerelease: false
42
+ requirement: &id003 !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ type: :development
49
+ version_requirements: *id003
50
+ - !ruby/object:Gem::Dependency
51
+ name: rdiscount
52
+ prerelease: false
53
+ requirement: &id004 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ type: :development
60
+ version_requirements: *id004
61
+ - !ruby/object:Gem::Dependency
62
+ name: simplecov
63
+ prerelease: false
64
+ requirement: &id005 !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ type: :development
71
+ version_requirements: *id005
72
+ - !ruby/object:Gem::Dependency
73
+ name: simplecov-html
74
+ prerelease: false
75
+ requirement: &id006 !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ type: :development
82
+ version_requirements: *id006
83
+ description: |-
84
+ This gem provides request authentication via [HMAC](http://en.wikipedia.org/wiki/Hmac). The main usage is request based, noninteractive
85
+ authentication for API implementations. Two strategies are supported that differ mainly in how the authentication information is
86
+ transferred to the server: One header-based authentication method and one query-based. The authentication scheme is in some parts based
87
+ on ideas laid out in this article and the following discussion:
88
+ http://broadcast.oreilly.com/2009/12/principles-for-standardized-rest-authentication.html
89
+
90
+ The gem also provides a small helper class that can be used to generate request signatures.
91
+ email:
92
+ - felix.gilcher@asquera.de
93
+ - florian.gilcher@asquera.de
94
+ executables: []
95
+
96
+ extensions: []
97
+
98
+ extra_rdoc_files: []
99
+
100
+ files:
101
+ - README.md
102
+ - Rakefile
103
+ - LICENSE
104
+ - lib/strategies/hmac_query_strategy.rb
105
+ - lib/strategies/hmac_header_strategy.rb
106
+ - lib/strategies/base.rb
107
+ - lib/hmac_signer.rb
108
+ has_rdoc: true
109
+ homepage: https://github.com/Asquera/warden-hmac-authentication
110
+ licenses: []
111
+
112
+ post_install_message:
113
+ rdoc_options: []
114
+
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: "0"
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: "0"
129
+ requirements: []
130
+
131
+ rubyforge_project:
132
+ rubygems_version: 1.6.2
133
+ signing_key:
134
+ specification_version: 3
135
+ summary: Provides request based, non-interactive authentication for APIs
136
+ test_files: []
137
+