warden-hmac-authentication 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|