warden-hmac-authentication 0.2.0

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