warden-hmac-authentication 0.3.0 → 0.4.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/bin/warden-hmac-authentication +3 -3
- data/lib/hmac/signer.rb +245 -0
- data/lib/hmac/strategies/base.rb +178 -0
- data/lib/hmac/strategies/header.rb +98 -0
- data/lib/hmac/strategies/query.rb +57 -0
- data/lib/hmac/string/jruby.rb +33 -0
- metadata +58 -25
- data/lib/hmac_signer.rb +0 -234
- data/lib/strategies/base.rb +0 -173
- data/lib/strategies/hmac_header_strategy.rb +0 -94
- data/lib/strategies/hmac_query_strategy.rb +0 -52
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'trollop'
|
4
|
-
require '
|
4
|
+
require 'hmac/signer'
|
5
5
|
|
6
6
|
opts = Trollop::options do
|
7
7
|
|
@@ -38,7 +38,7 @@ url = ARGV.shift
|
|
38
38
|
secret = opts.delete(:secret)
|
39
39
|
algorithm = opts.delete(:algorithm)
|
40
40
|
|
41
|
-
signer =
|
41
|
+
signer = HMAC::Signer.new(algorithm)
|
42
42
|
|
43
43
|
if "sign" == cmd
|
44
44
|
puts signer.sign_url(url, secret, opts)
|
@@ -51,4 +51,4 @@ else
|
|
51
51
|
puts "URL #{url} does not contain a valid signature"
|
52
52
|
exit 1
|
53
53
|
end
|
54
|
-
end
|
54
|
+
end
|
data/lib/hmac/signer.rb
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'openssl'
|
3
|
+
require 'rack/utils'
|
4
|
+
if defined?(JRUBY_VERSION) && RUBY_VERSION =~ /^1\.9/
|
5
|
+
require 'hmac/string/jruby'
|
6
|
+
end
|
7
|
+
|
8
|
+
module HMAC
|
9
|
+
# Helper class that provides signing capabilites for the hmac strategies.
|
10
|
+
#
|
11
|
+
# @author Felix Gilcher <felix.gilcher@asquera.de>
|
12
|
+
class Signer
|
13
|
+
attr_accessor :secret, :algorithm, :default_opts
|
14
|
+
|
15
|
+
# create a new HMAC instance
|
16
|
+
#
|
17
|
+
# @param [String] algorithm The hashing-algorithm to use. See the openssl documentation for valid values.
|
18
|
+
# @param [Hash] default_opts The default options for all calls that take opts
|
19
|
+
#
|
20
|
+
# @option default_opts [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
|
21
|
+
# @option default_opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
|
22
|
+
# @option default_opts [String] :auth_header ('Authorization') The name of the authorization header to use
|
23
|
+
# @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.
|
24
|
+
# @option default_opts [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
|
25
|
+
# @option default_opts [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
|
26
|
+
# @option default_opts [Bool] :query_based (false) Whether to use query based authentication
|
27
|
+
# @option default_opts [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
|
28
|
+
#
|
29
|
+
def initialize(algorithm = "sha1", default_opts = {})
|
30
|
+
self.algorithm = algorithm
|
31
|
+
self.default_opts = {
|
32
|
+
:auth_scheme => "HMAC",
|
33
|
+
:auth_param => "auth",
|
34
|
+
:auth_header => "Authorization",
|
35
|
+
:auth_header_format => "%{auth_scheme} %{signature}",
|
36
|
+
:nonce_header => "X-%{scheme}-Nonce" % {:scheme => (default_opts[:auth_scheme] || "HMAC")},
|
37
|
+
:alternate_date_header => "X-%{scheme}-Date" % {:scheme => (default_opts[:auth_scheme] || "HMAC")},
|
38
|
+
:query_based => false,
|
39
|
+
:use_alternate_date_header => false
|
40
|
+
}.merge(default_opts)
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
# Generate the signature from a hash representation
|
45
|
+
#
|
46
|
+
# returns nil if no secret or an empty secret was given
|
47
|
+
#
|
48
|
+
# @param [Hash] params the parameters to create the representation with
|
49
|
+
# @option params [String] :secret The secret to generate the signature with
|
50
|
+
# @option params [String] :method The HTTP Verb of the request
|
51
|
+
# @option params [String] :date The date of the request as it was formatted in the request
|
52
|
+
# @option params [String] :nonce ('') The nonce given in the request
|
53
|
+
# @option params [String] :path The path portion of the request
|
54
|
+
# @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param.
|
55
|
+
# @option params [Hash] :headers ({}) All headers given in the request (optional and required)
|
56
|
+
# @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
|
57
|
+
# @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
|
58
|
+
# @option params [String] :auth_header ('Authorization') The name of the authorization header to use
|
59
|
+
# @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.
|
60
|
+
# @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
|
61
|
+
# @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
|
62
|
+
# @option params [Bool] :query_based (false) Whether to use query based authentication
|
63
|
+
# @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
|
64
|
+
#
|
65
|
+
# @return [String] the signature
|
66
|
+
def generate_signature(params)
|
67
|
+
secret = params.delete(:secret)
|
68
|
+
|
69
|
+
# jruby stumbles over empty secrets, we regard them as invalid anyways, so we return an empty digest if no scret was given
|
70
|
+
if '' == secret.to_s
|
71
|
+
nil
|
72
|
+
else
|
73
|
+
OpenSSL::HMAC.hexdigest(algorithm, secret, canonical_representation(params))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# compares the given signature with the signature created from a hash representation
|
78
|
+
#
|
79
|
+
# @param [String] signature the signature to compare with
|
80
|
+
# @param [Hash] params the parameters to create the representation with
|
81
|
+
# @option params [String] :secret The secret to generate the signature with
|
82
|
+
# @option params [String] :method The HTTP Verb of the request
|
83
|
+
# @option params [String] :date The date of the request as it was formatted in the request
|
84
|
+
# @option params [String] :nonce ('') The nonce given in the request
|
85
|
+
# @option params [String] :path The path portion of the request
|
86
|
+
# @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param.
|
87
|
+
# @option params [Hash] :headers ({}) All headers given in the request (optional and required)
|
88
|
+
# @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
|
89
|
+
# @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
|
90
|
+
# @option params [String] :auth_header ('Authorization') The name of the authorization header to use
|
91
|
+
# @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.
|
92
|
+
# @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
|
93
|
+
# @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
|
94
|
+
# @option params [Bool] :query_based (false) Whether to use query based authentication
|
95
|
+
# @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
|
96
|
+
#
|
97
|
+
# @return [Bool] true if the signature matches
|
98
|
+
def validate_signature(signature, params)
|
99
|
+
signature == generate_signature(params)
|
100
|
+
end
|
101
|
+
|
102
|
+
# convienience method to check the signature of a url with query-based authentication
|
103
|
+
#
|
104
|
+
# @param [String] url the url to test
|
105
|
+
# @param [String] secret the secret used to sign the url
|
106
|
+
# @param [Hash] opts Options controlling the singature generation
|
107
|
+
#
|
108
|
+
# @option opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
|
109
|
+
#
|
110
|
+
# @return [Bool] true if the signature is valid
|
111
|
+
def validate_url_signature(url, secret, opts = {})
|
112
|
+
opts = default_opts.merge(opts)
|
113
|
+
opts[:query_based] = true
|
114
|
+
|
115
|
+
uri = Addressable::URI.parse(url)
|
116
|
+
query_values = uri.query_values
|
117
|
+
auth_params = query_values.delete(opts[:auth_param])
|
118
|
+
|
119
|
+
date = auth_params["date"]
|
120
|
+
nonce = auth_params["nonce"]
|
121
|
+
validate_signature(auth_params["signature"], :secret => secret, :method => "GET", :path => uri.path, :date => date, :nonce => nonce, :query => query_values, :headers => {})
|
122
|
+
end
|
123
|
+
|
124
|
+
# generates the canonical representation for a given request
|
125
|
+
#
|
126
|
+
# @param [Hash] params the parameters to create the representation with
|
127
|
+
# @option params [String] :method The HTTP Verb of the request
|
128
|
+
# @option params [String] :date The date of the request as it was formatted in the request
|
129
|
+
# @option params [String] :nonce ('') The nonce given in the request
|
130
|
+
# @option params [String] :path The path portion of the request
|
131
|
+
# @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param.
|
132
|
+
# @option params [Hash] :headers ({}) All headers given in the request (optional and required)
|
133
|
+
# @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
|
134
|
+
# @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
|
135
|
+
# @option params [String] :auth_header ('Authorization') The name of the authorization header to use
|
136
|
+
# @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.
|
137
|
+
# @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
|
138
|
+
# @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
|
139
|
+
# @option params [Bool] :query_based (false) Whether to use query based authentication
|
140
|
+
# @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
|
141
|
+
#
|
142
|
+
# @return [String] the canonical representation
|
143
|
+
def canonical_representation(params)
|
144
|
+
rep = ""
|
145
|
+
|
146
|
+
rep << "#{params[:method].upcase}\n"
|
147
|
+
rep << "date:#{params[:date]}\n"
|
148
|
+
rep << "nonce:#{params[:nonce]}\n"
|
149
|
+
|
150
|
+
(params[:headers] || {}).sort.each do |pair|
|
151
|
+
name,value = *pair
|
152
|
+
rep << "#{name.downcase}:#{value}\n"
|
153
|
+
end
|
154
|
+
|
155
|
+
rep << params[:path]
|
156
|
+
|
157
|
+
p = (params[:query] || {}).dup
|
158
|
+
|
159
|
+
if !p.empty?
|
160
|
+
query = p.sort.map do |key, value|
|
161
|
+
"%{key}=%{value}" % {
|
162
|
+
:key => Rack::Utils.unescape(key.to_s),
|
163
|
+
:value => Rack::Utils.unescape(value.to_s)
|
164
|
+
}
|
165
|
+
end.join("&")
|
166
|
+
rep << "?#{query}"
|
167
|
+
end
|
168
|
+
|
169
|
+
rep
|
170
|
+
end
|
171
|
+
|
172
|
+
# sign the given request
|
173
|
+
#
|
174
|
+
# @param [String] url The url of the request
|
175
|
+
# @param [String] secret The shared secret for the signature
|
176
|
+
# @param [Hash] opts Options for the signature generation
|
177
|
+
#
|
178
|
+
# @option opts [String] :nonce ('') The nonce to use in the signature
|
179
|
+
# @option opts [String, #strftime] :date (Time.now) The date to use in the signature
|
180
|
+
# @option opts [Hash] :headers ({}) A list of optional headers to include in the signature
|
181
|
+
#
|
182
|
+
# @option opts [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names
|
183
|
+
# @option opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
|
184
|
+
# @option opts [String] :auth_header ('Authorization') The name of the authorization header to use
|
185
|
+
# @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.
|
186
|
+
# @option opts [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce
|
187
|
+
# @option opts [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header
|
188
|
+
# @option opts [Bool] :query_based (false) Whether to use query based authentication
|
189
|
+
# @option opts [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`
|
190
|
+
#
|
191
|
+
def sign_request(url, secret, opts = {})
|
192
|
+
opts = default_opts.merge(opts)
|
193
|
+
|
194
|
+
uri = Addressable::URI.parse(url)
|
195
|
+
headers = opts[:headers] || {}
|
196
|
+
|
197
|
+
date = opts[:date] || Time.now.gmtime
|
198
|
+
date = date.gmtime.strftime('%a, %e %b %Y %T GMT') if date.respond_to? :strftime
|
199
|
+
|
200
|
+
signature = generate_signature(:secret => secret, :method => "GET", :path => uri.path, :date => date, :nonce => opts[:nonce], :query => uri.query_values, :headers => opts[:headers])
|
201
|
+
|
202
|
+
if opts[:query_based]
|
203
|
+
auth_params = {
|
204
|
+
"date" => date,
|
205
|
+
"signature" => signature
|
206
|
+
}
|
207
|
+
auth_params[:nonce] = opts[:nonce] unless opts[:nonce].nil?
|
208
|
+
|
209
|
+
query_values = uri.query_values || {}
|
210
|
+
query_values[opts[:auth_param]] = auth_params
|
211
|
+
uri.query_values = query_values
|
212
|
+
else
|
213
|
+
headers[opts[:auth_header]] = opts[:auth_header_format] % opts.merge({:signature => signature})
|
214
|
+
headers[opts[:nonce_header]] = opts[:nonce] unless opts[:nonce].nil?
|
215
|
+
|
216
|
+
if opts[:use_alternate_date_header]
|
217
|
+
headers[opts[:alternate_date_header]] = date
|
218
|
+
else
|
219
|
+
headers["Date"] = date
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
[headers, uri.to_s]
|
224
|
+
end
|
225
|
+
|
226
|
+
# convienience method to sign a url for use with query-based authentication
|
227
|
+
#
|
228
|
+
# @param [String] url the url to sign
|
229
|
+
# @param [String] secret the secret used to sign the url
|
230
|
+
# @param [Hash] opts Options controlling the singature generation
|
231
|
+
#
|
232
|
+
# @option opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication
|
233
|
+
#
|
234
|
+
# @return [String] The signed url
|
235
|
+
def sign_url(url, secret, opts = {})
|
236
|
+
opts = default_opts.merge(opts)
|
237
|
+
opts[:query_based] = true
|
238
|
+
|
239
|
+
headers, url = *sign_request(url, secret, opts)
|
240
|
+
url
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'hmac/signer'
|
2
|
+
require 'warden'
|
3
|
+
|
4
|
+
|
5
|
+
module Warden
|
6
|
+
module Strategies
|
7
|
+
module HMAC
|
8
|
+
# Base class for hmac authentication in warden. Provides shared methods such as config access
|
9
|
+
# and various helpers.
|
10
|
+
#
|
11
|
+
# @author Felix Gilcher <felix.gilcher@asquera.de>
|
12
|
+
class Base < Warden::Strategies::Base
|
13
|
+
|
14
|
+
|
15
|
+
# Performs authentication. Calls success! if authentication was performed successfully and halt!
|
16
|
+
# if the authentication information is invalid.
|
17
|
+
#
|
18
|
+
# Delegates parts of the work to signature_valid? which must be implemented in child-strategies.
|
19
|
+
#
|
20
|
+
# @see https://github.com/hassox/warden/wiki/Strategies
|
21
|
+
def authenticate!
|
22
|
+
if "" == secret.to_s
|
23
|
+
debug("authentication attempt with an empty secret")
|
24
|
+
return fail!("Cannot authenticate with an empty secret")
|
25
|
+
end
|
26
|
+
|
27
|
+
if check_ttl? && !timestamp_valid?
|
28
|
+
debug("authentication attempt with an invalid timestamp. Given was #{timestamp}, expected was #{Time.now.gmtime}")
|
29
|
+
return fail!("Invalid timestamp")
|
30
|
+
end
|
31
|
+
|
32
|
+
if signature_valid?
|
33
|
+
success!(retrieve_user)
|
34
|
+
else
|
35
|
+
debug("authentication attempt with an invalid signature.")
|
36
|
+
fail!("Invalid token passed")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Retrieve the current request method
|
41
|
+
#
|
42
|
+
# @return [String] The request method in capital letters
|
43
|
+
def request_method
|
44
|
+
env['REQUEST_METHOD'].upcase
|
45
|
+
end
|
46
|
+
|
47
|
+
# Retrieve the request query parameters
|
48
|
+
#
|
49
|
+
# @return [Hash] The query parameters
|
50
|
+
def params
|
51
|
+
request.GET
|
52
|
+
end
|
53
|
+
|
54
|
+
# Retrieve the request headers. Header names are normalized by this method by stripping
|
55
|
+
# the `HTTP_`-prefix and replacing underscores with dashes. `HTTP_X_Foo` is normalized to
|
56
|
+
# `X-Foo`.
|
57
|
+
#
|
58
|
+
# @return [Hash] The request headers
|
59
|
+
def headers
|
60
|
+
pairs = env.select {|k,v| k.start_with? 'HTTP_'}
|
61
|
+
.collect {|pair| [pair[0].sub(/^HTTP_/, '').gsub(/_/, '-'), pair[1]]}
|
62
|
+
.sort
|
63
|
+
headers = Hash[*pairs.flatten]
|
64
|
+
headers
|
65
|
+
end
|
66
|
+
|
67
|
+
# Retrieve a user from the database. Stub implementation that just returns true, needed for compat.
|
68
|
+
#
|
69
|
+
# @return [Bool] true
|
70
|
+
def retrieve_user
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
# Log a debug message if a logger is available.
|
75
|
+
#
|
76
|
+
# @param [String] msg The message to log
|
77
|
+
def debug(msg)
|
78
|
+
if logger
|
79
|
+
logger.debug(msg)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Retrieve a logger. Current implementation can
|
84
|
+
# only handle Padrino loggers
|
85
|
+
#
|
86
|
+
# @return [Logger] the logger, nil if none is available
|
87
|
+
def logger
|
88
|
+
if defined? Padrino
|
89
|
+
Padrino.logger
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
def config
|
95
|
+
env["warden"].config[:scope_defaults][scope][:hmac]
|
96
|
+
end
|
97
|
+
|
98
|
+
def auth_param
|
99
|
+
config[:auth_param] || "auth"
|
100
|
+
end
|
101
|
+
|
102
|
+
def auth_header
|
103
|
+
config[:auth_header] || "Authorization"
|
104
|
+
end
|
105
|
+
|
106
|
+
def auth_scheme_name
|
107
|
+
config[:auth_scheme] || "HMAC"
|
108
|
+
end
|
109
|
+
|
110
|
+
def nonce_header_name
|
111
|
+
config[:nonce_header] || "X-#{auth_scheme_name}-Nonce"
|
112
|
+
end
|
113
|
+
|
114
|
+
def alternate_date_header_name
|
115
|
+
config[:alternate_date_header] || "X-#{auth_scheme_name}-Date"
|
116
|
+
end
|
117
|
+
|
118
|
+
def optional_headers
|
119
|
+
(config[:optional_headers] || []) + ["Content-MD5", "Content-Type"]
|
120
|
+
end
|
121
|
+
|
122
|
+
def lowercase_headers
|
123
|
+
|
124
|
+
if @lowercase_headers.nil?
|
125
|
+
tmp = headers.map do |name,value|
|
126
|
+
[name.downcase, value]
|
127
|
+
end
|
128
|
+
@lowercase_headers = Hash[*tmp.flatten]
|
129
|
+
end
|
130
|
+
|
131
|
+
@lowercase_headers
|
132
|
+
end
|
133
|
+
|
134
|
+
def hmac
|
135
|
+
::HMAC::Signer.new(algorithm)
|
136
|
+
end
|
137
|
+
|
138
|
+
def algorithm
|
139
|
+
config[:algorithm] || "sha1"
|
140
|
+
end
|
141
|
+
|
142
|
+
def ttl
|
143
|
+
config[:ttl].to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
def check_ttl?
|
147
|
+
!config[:ttl].nil?
|
148
|
+
end
|
149
|
+
|
150
|
+
def timestamp
|
151
|
+
Time.strptime(request_timestamp, '%a, %e %b %Y %T %z') unless request_timestamp.nil?
|
152
|
+
end
|
153
|
+
|
154
|
+
def has_timestamp?
|
155
|
+
!timestamp.nil?
|
156
|
+
end
|
157
|
+
|
158
|
+
def timestamp_valid?
|
159
|
+
now = Time.now.gmtime.to_i
|
160
|
+
timestamp.to_i <= (now + clockskew) && timestamp.to_i >= (now - ttl)
|
161
|
+
end
|
162
|
+
|
163
|
+
def nonce_required?
|
164
|
+
!!config[:require_nonce]
|
165
|
+
end
|
166
|
+
|
167
|
+
def secret
|
168
|
+
@secret ||= config[:secret].respond_to?(:call) ? config[:secret].call(self) : config[:secret]
|
169
|
+
end
|
170
|
+
|
171
|
+
def clockskew
|
172
|
+
(config[:clockskew] || 5)
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Warden
|
4
|
+
module Strategies
|
5
|
+
module HMAC
|
6
|
+
# Implements header-based hmac authentication for warden. The strategy is registered as
|
7
|
+
# `:hmac_header` in the warden strategy list.
|
8
|
+
#
|
9
|
+
# @author Felix Gilcher <felix.gilcher@asquera.de>
|
10
|
+
class Header < Warden::Strategies::HMAC::Base
|
11
|
+
|
12
|
+
# Checks that this strategy applies. Tests that the required
|
13
|
+
# authentication information was given.
|
14
|
+
#
|
15
|
+
# @return [Bool] true if all required authentication information is available in the request
|
16
|
+
# @see https://github.com/hassox/warden/wiki/Strategies
|
17
|
+
def valid?
|
18
|
+
valid = required_headers.all? { |h| headers.include?(h) } && headers.include?("Authorization") && has_timestamp?
|
19
|
+
valid = valid && scheme_valid?
|
20
|
+
valid
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check that the signature given in the request is valid.
|
24
|
+
#
|
25
|
+
# @return [Bool] true if the request is valid
|
26
|
+
def signature_valid?
|
27
|
+
|
28
|
+
#:method => "GET",
|
29
|
+
#:date => "Mon, 20 Jun 2011 12:06:11 GMT",
|
30
|
+
#:nonce => "TESTNONCE",
|
31
|
+
#:path => "/example",
|
32
|
+
#:query => {
|
33
|
+
# "foo" => "bar",
|
34
|
+
# "baz" => "foobared"
|
35
|
+
#},
|
36
|
+
#:headers => {
|
37
|
+
# "Content-Type" => "application/json;charset=utf8",
|
38
|
+
# "Content-MD5" => "d41d8cd98f00b204e9800998ecf8427e"
|
39
|
+
#}
|
40
|
+
|
41
|
+
hmac.validate_signature(given_signature, {
|
42
|
+
:secret => secret,
|
43
|
+
:method => request_method,
|
44
|
+
:date => request_timestamp,
|
45
|
+
:nonce => nonce,
|
46
|
+
:path => request.path,
|
47
|
+
:query => params,
|
48
|
+
:headers => headers.select {|name, value| optional_headers.include? name}
|
49
|
+
})
|
50
|
+
end
|
51
|
+
|
52
|
+
# retrieve the signature from the request
|
53
|
+
#
|
54
|
+
# @return [String] The signature from the request
|
55
|
+
def given_signature
|
56
|
+
headers[auth_header].split(" ")[1]
|
57
|
+
end
|
58
|
+
|
59
|
+
# retrieve the nonce from the request
|
60
|
+
#
|
61
|
+
# @return [String] The nonce or an empty string if no nonce was given in the request
|
62
|
+
def nonce
|
63
|
+
headers[nonce_header_name]
|
64
|
+
end
|
65
|
+
|
66
|
+
# retrieve the request timestamp as string
|
67
|
+
#
|
68
|
+
# @return [String] The request timestamp or an empty string if no timestamp was given in the request
|
69
|
+
def request_timestamp
|
70
|
+
headers[date_header]
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def required_headers
|
76
|
+
headers = [auth_header]
|
77
|
+
headers += [nonce_header_name] if nonce_required?
|
78
|
+
headers
|
79
|
+
end
|
80
|
+
|
81
|
+
def scheme_valid?
|
82
|
+
headers[auth_header].to_s.split(" ").first == auth_scheme_name
|
83
|
+
end
|
84
|
+
|
85
|
+
def date_header
|
86
|
+
if headers.include? alternate_date_header_name
|
87
|
+
alternate_date_header_name
|
88
|
+
else
|
89
|
+
"Date"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
Warden::Strategies.add(:hmac_header, Warden::Strategies::HMAC::Header)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Warden
|
4
|
+
module Strategies
|
5
|
+
module HMAC
|
6
|
+
# Implements query-based hmac authentication for warden. The strategy is registered as
|
7
|
+
# `:hmac_query` in the warden strategy list.
|
8
|
+
#
|
9
|
+
# @author Felix Gilcher <felix.gilcher@asquera.de>
|
10
|
+
class Warden::Strategies::HMAC::Query < Warden::Strategies::HMAC::Base
|
11
|
+
|
12
|
+
# Checks that this strategy applies. Tests that the required
|
13
|
+
# authentication information was given.
|
14
|
+
#
|
15
|
+
# @return [Bool] true if all required authentication information is available in the request
|
16
|
+
# @see https://github.com/hassox/warden/wiki/Strategies
|
17
|
+
def valid?
|
18
|
+
valid = auth_info.include? "signature"
|
19
|
+
valid = valid && has_timestamp? if check_ttl?
|
20
|
+
valid = valid && has_nonce? if nonce_required?
|
21
|
+
valid
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check that the signature given in the request is valid.
|
25
|
+
#
|
26
|
+
# @return [Bool] true if the request is valid
|
27
|
+
def signature_valid?
|
28
|
+
hmac.validate_url_signature(request.url, secret)
|
29
|
+
end
|
30
|
+
|
31
|
+
# retrieve the authentication information from the request
|
32
|
+
#
|
33
|
+
# @return [Hash] the authentication info in the request
|
34
|
+
def auth_info
|
35
|
+
params[auth_param] || {}
|
36
|
+
end
|
37
|
+
|
38
|
+
# retrieve the nonce from the request
|
39
|
+
#
|
40
|
+
# @return [String] The nonce or an empty string if no nonce was given in the request
|
41
|
+
def nonce
|
42
|
+
auth_info["nonce"] || ""
|
43
|
+
end
|
44
|
+
|
45
|
+
# retrieve the request timestamp as string
|
46
|
+
#
|
47
|
+
# @return [String] The request timestamp or an empty string if no timestamp was given in the request
|
48
|
+
def request_timestamp
|
49
|
+
auth_info["date"] || ""
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Warden::Strategies.add(:hmac_query, Warden::Strategies::HMAC::Query)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# this is a ruby workaround for jruby issue http://jira.codehaus.org/browse/JRUBY-5338
|
2
|
+
|
3
|
+
if defined?(JRUBY_VERSION) && RUBY_VERSION =~ /^1\.9/
|
4
|
+
class KeyError < IndexError
|
5
|
+
end
|
6
|
+
|
7
|
+
class String
|
8
|
+
alias_method :old_format, :%
|
9
|
+
def %(replacements)
|
10
|
+
split_re = /(?<!%)(%{[^}]+})/
|
11
|
+
replace_re = /(?<!%)%{([^}]+)}/
|
12
|
+
if ! replacements.is_a? Hash
|
13
|
+
if split_re.match self
|
14
|
+
raise ArgumentError, "one hash required"
|
15
|
+
else
|
16
|
+
return self.old_format replacements
|
17
|
+
end
|
18
|
+
end
|
19
|
+
segments = self.split split_re
|
20
|
+
segments.each_index do |i; md, key|
|
21
|
+
md = replace_re.match(segments[i])
|
22
|
+
if ! md.nil?
|
23
|
+
key = md.captures[0].to_sym
|
24
|
+
raise KeyError, "key[#{key}] not found" unless replacements.has_key?(key)
|
25
|
+
segments[i] = replacements[key]
|
26
|
+
else
|
27
|
+
segments[i] = segments[i].gsub "%%", "%"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
segments.join
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: warden-hmac-authentication
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,12 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2011-
|
14
|
-
default_executable:
|
13
|
+
date: 2011-12-13 00:00:00.000000000Z
|
15
14
|
dependencies:
|
16
15
|
- !ruby/object:Gem::Dependency
|
17
16
|
name: addressable
|
18
|
-
requirement: &
|
17
|
+
requirement: &2152266240 !ruby/object:Gem::Requirement
|
19
18
|
none: false
|
20
19
|
requirements:
|
21
20
|
- - ! '>='
|
@@ -23,10 +22,10 @@ dependencies:
|
|
23
22
|
version: '0'
|
24
23
|
type: :runtime
|
25
24
|
prerelease: false
|
26
|
-
version_requirements: *
|
25
|
+
version_requirements: *2152266240
|
27
26
|
- !ruby/object:Gem::Dependency
|
28
27
|
name: rack
|
29
|
-
requirement: &
|
28
|
+
requirement: &2152265520 !ruby/object:Gem::Requirement
|
30
29
|
none: false
|
31
30
|
requirements:
|
32
31
|
- - ! '>='
|
@@ -34,10 +33,10 @@ dependencies:
|
|
34
33
|
version: '0'
|
35
34
|
type: :runtime
|
36
35
|
prerelease: false
|
37
|
-
version_requirements: *
|
36
|
+
version_requirements: *2152265520
|
38
37
|
- !ruby/object:Gem::Dependency
|
39
38
|
name: trollop
|
40
|
-
requirement: &
|
39
|
+
requirement: &2152265000 !ruby/object:Gem::Requirement
|
41
40
|
none: false
|
42
41
|
requirements:
|
43
42
|
- - ! '>='
|
@@ -45,10 +44,43 @@ dependencies:
|
|
45
44
|
version: '0'
|
46
45
|
type: :runtime
|
47
46
|
prerelease: false
|
48
|
-
version_requirements: *
|
47
|
+
version_requirements: *2152265000
|
49
48
|
- !ruby/object:Gem::Dependency
|
50
|
-
name:
|
51
|
-
requirement: &
|
49
|
+
name: warden
|
50
|
+
requirement: &2152264180 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *2152264180
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: rake
|
61
|
+
requirement: &2152263320 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *2152263320
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rack-test
|
72
|
+
requirement: &2152262180 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *2152262180
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: riot
|
83
|
+
requirement: &2152261120 !ruby/object:Gem::Requirement
|
52
84
|
none: false
|
53
85
|
requirements:
|
54
86
|
- - ! '>='
|
@@ -56,10 +88,10 @@ dependencies:
|
|
56
88
|
version: '0'
|
57
89
|
type: :development
|
58
90
|
prerelease: false
|
59
|
-
version_requirements: *
|
91
|
+
version_requirements: *2152261120
|
60
92
|
- !ruby/object:Gem::Dependency
|
61
|
-
name:
|
62
|
-
requirement: &
|
93
|
+
name: timecop
|
94
|
+
requirement: &2152228000 !ruby/object:Gem::Requirement
|
63
95
|
none: false
|
64
96
|
requirements:
|
65
97
|
- - ! '>='
|
@@ -67,10 +99,10 @@ dependencies:
|
|
67
99
|
version: '0'
|
68
100
|
type: :development
|
69
101
|
prerelease: false
|
70
|
-
version_requirements: *
|
102
|
+
version_requirements: *2152228000
|
71
103
|
- !ruby/object:Gem::Dependency
|
72
104
|
name: simplecov
|
73
|
-
requirement: &
|
105
|
+
requirement: &2152227480 !ruby/object:Gem::Requirement
|
74
106
|
none: false
|
75
107
|
requirements:
|
76
108
|
- - ! '>='
|
@@ -78,10 +110,10 @@ dependencies:
|
|
78
110
|
version: '0'
|
79
111
|
type: :development
|
80
112
|
prerelease: false
|
81
|
-
version_requirements: *
|
113
|
+
version_requirements: *2152227480
|
82
114
|
- !ruby/object:Gem::Dependency
|
83
115
|
name: simplecov-html
|
84
|
-
requirement: &
|
116
|
+
requirement: &2152226940 !ruby/object:Gem::Requirement
|
85
117
|
none: false
|
86
118
|
requirements:
|
87
119
|
- - ! '>='
|
@@ -89,7 +121,7 @@ dependencies:
|
|
89
121
|
version: '0'
|
90
122
|
type: :development
|
91
123
|
prerelease: false
|
92
|
-
version_requirements: *
|
124
|
+
version_requirements: *2152226940
|
93
125
|
description: ! "This gem provides request authentication via [HMAC](http://en.wikipedia.org/wiki/Hmac).
|
94
126
|
The main usage is request based, noninteractive\n authentication for API implementations.
|
95
127
|
Two strategies are supported that differ mainly in how the authentication information
|
@@ -109,12 +141,12 @@ files:
|
|
109
141
|
- README.md
|
110
142
|
- Rakefile
|
111
143
|
- LICENSE
|
112
|
-
- lib/
|
113
|
-
- lib/strategies/
|
114
|
-
- lib/strategies/
|
115
|
-
- lib/
|
144
|
+
- lib/hmac/signer.rb
|
145
|
+
- lib/hmac/strategies/base.rb
|
146
|
+
- lib/hmac/strategies/header.rb
|
147
|
+
- lib/hmac/strategies/query.rb
|
148
|
+
- lib/hmac/string/jruby.rb
|
116
149
|
- bin/warden-hmac-authentication
|
117
|
-
has_rdoc: true
|
118
150
|
homepage: https://github.com/Asquera/warden-hmac-authentication
|
119
151
|
licenses: []
|
120
152
|
post_install_message:
|
@@ -135,8 +167,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
167
|
version: '0'
|
136
168
|
requirements: []
|
137
169
|
rubyforge_project:
|
138
|
-
rubygems_version: 1.6
|
170
|
+
rubygems_version: 1.8.6
|
139
171
|
signing_key:
|
140
172
|
specification_version: 3
|
141
173
|
summary: Provides request based, non-interactive authentication for APIs
|
142
174
|
test_files: []
|
175
|
+
has_rdoc:
|
data/lib/hmac_signer.rb
DELETED
@@ -1,234 +0,0 @@
|
|
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
|
-
|
data/lib/strategies/base.rb
DELETED
@@ -1,173 +0,0 @@
|
|
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
|
@@ -1,94 +0,0 @@
|
|
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)
|
@@ -1,52 +0,0 @@
|
|
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)
|