oauthenticator 0.1.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/README.md +118 -35
- data/Rakefile.rb +11 -0
- data/lib/oauthenticator.rb +5 -6
- data/lib/oauthenticator/config_methods.rb +62 -21
- data/lib/oauthenticator/faraday_signer.rb +66 -0
- data/lib/oauthenticator/parse_authorization.rb +81 -0
- data/lib/oauthenticator/{middleware.rb → rack_authenticator.rb} +37 -9
- data/lib/oauthenticator/signable_request.rb +340 -0
- data/lib/oauthenticator/signed_request.rb +123 -112
- data/lib/oauthenticator/version.rb +3 -1
- data/test/config_methods_test.rb +10 -7
- data/test/faraday_signer_test.rb +65 -0
- data/test/helper.rb +15 -3
- data/test/parse_authorization_test.rb +86 -0
- data/test/{oauthenticator_test.rb → rack_authenticator_test.rb} +264 -136
- data/test/signable_request_test.rb +653 -0
- data/test/signed_request_test.rb +12 -0
- data/test/test_config_methods.rb +67 -0
- metadata +26 -11
@@ -0,0 +1,81 @@
|
|
1
|
+
module OAuthenticator
|
2
|
+
# OAuthenticator::Error represents some problem with authenticating. it has an #errors attribute with error
|
3
|
+
# messages in the form we use.
|
4
|
+
class Error < StandardError
|
5
|
+
# @param message [String]
|
6
|
+
# @param errors [Hash<String, Array<String>>]
|
7
|
+
def initialize(message, errors=nil)
|
8
|
+
super(message)
|
9
|
+
@errors = errors
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [Hash<String, Array<String>>]
|
13
|
+
def errors
|
14
|
+
@errors ||= Hash.new { |h,k| h[k] = [] }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# an error parsing an authorization header in .parse_authorization
|
19
|
+
class ParseError < Error; end
|
20
|
+
|
21
|
+
# an error indicating duplicated paramaters present in an authorization header, in violation of section 3.1
|
22
|
+
# ("Each parameter MUST NOT appear more than once per request.") and 3.2 ("The server SHOULD return a 400
|
23
|
+
# (Bad Request) status code when receiving a request with unsupported parameters, an unsupported signature
|
24
|
+
# method, missing parameters, or duplicated protocol parameters.")
|
25
|
+
class DuplicatedParameters < Error; end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# @param header [String] an Authorization header
|
29
|
+
# @return [Hash<String, String>] parsed authorization parameters
|
30
|
+
# @raise [OAuthenticator::ParseError] if the header is not well-formed and cannot be parsed
|
31
|
+
# @raise [OAuthenticator::DuplicatedParameters] if the header contains multiple instances of the same param
|
32
|
+
def parse_authorization(header)
|
33
|
+
header = header.to_s
|
34
|
+
scanner = StringScanner.new(header)
|
35
|
+
auth_parse_error = proc { |message| raise ParseError.new(message, {'Authorization' => [message]}) }
|
36
|
+
scanner.scan(/OAuth\s*/i) || auth_parse_error.call("Authorization scheme is not OAuth - recieved: #{header}")
|
37
|
+
attributes = Hash.new { |h,k| h[k] = [] }
|
38
|
+
while match = scanner.scan(/(\w+)="([^"]*)"\s*(,?)\s*/)
|
39
|
+
key = scanner[1]
|
40
|
+
value = scanner[2]
|
41
|
+
comma_follows = !scanner[3].empty?
|
42
|
+
if !comma_follows && !scanner.eos?
|
43
|
+
auth_parse_error.call("Could not parse Authorization header: #{header}\naround or after character #{scanner.pos}: #{scanner.rest}")
|
44
|
+
end
|
45
|
+
attributes[unescape(key)] << unescape(value)
|
46
|
+
end
|
47
|
+
unless scanner.eos?
|
48
|
+
auth_parse_error.call("Could not parse Authorization header: #{header}\naround or after character #{scanner.pos}: #{scanner.rest}")
|
49
|
+
end
|
50
|
+
duplicates = attributes.select { |k,v| v.size > 1 }
|
51
|
+
if duplicates.any?
|
52
|
+
errors = duplicates.map do |k,vs|
|
53
|
+
{k => "Received multiple instances of Authorization parameter #{k}. Received values were: #{vs.inspect}"}
|
54
|
+
end.inject({}, &:update)
|
55
|
+
raise DuplicatedParameters.new("Received duplicate parameters: #{duplicates.keys.inspect}", errors)
|
56
|
+
end
|
57
|
+
return attributes.map { |k,v| {k => v.first} }.inject({}, &:update)
|
58
|
+
end
|
59
|
+
|
60
|
+
# escape a value
|
61
|
+
# @param value [String] value
|
62
|
+
# @return [String] escaped value
|
63
|
+
def escape(value)
|
64
|
+
uri_parser.escape(value.to_s, /[^a-z0-9\-\.\_\~]/i)
|
65
|
+
end
|
66
|
+
|
67
|
+
# unescape a value
|
68
|
+
# @param value [String] escaped value
|
69
|
+
# @return [String] unescaped value
|
70
|
+
def unescape(value)
|
71
|
+
uri_parser.unescape(value.to_s)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# @return [Object] a parser that responds to #escape and #unescape
|
77
|
+
def uri_parser
|
78
|
+
@uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'rack'
|
2
|
-
require 'simple_oauth'
|
3
2
|
require 'json'
|
4
3
|
require 'oauthenticator/signed_request'
|
5
4
|
|
@@ -11,16 +10,21 @@ module OAuthenticator
|
|
11
10
|
# structured like rails / ActiveResource:
|
12
11
|
#
|
13
12
|
# {'errors': {'attribute1': ['messageA', 'messageB'], 'attribute2': ['messageC']}}
|
14
|
-
class
|
13
|
+
class RackAuthenticator
|
15
14
|
# options:
|
16
15
|
#
|
17
16
|
# - `:bypass` - a proc which will be called with a Rack::Request, which must have a boolean result.
|
18
|
-
# if the result is true,
|
17
|
+
# if the result is true, authentication checking is bypassed. if false, the request is authenticated
|
19
18
|
# and responds 401 if not authenticated.
|
20
19
|
#
|
21
20
|
# - `:config_methods` - a Module which defines necessary methods for an {OAuthenticator::SignedRequest} to
|
22
21
|
# determine if it is validly signed. See documentation for {OAuthenticator::ConfigMethods}
|
23
22
|
# for details of what this module must implement.
|
23
|
+
#
|
24
|
+
# - `:logger` - a Logger instance to which OAuthenticator::RackAuthenticator will log informative messages
|
25
|
+
#
|
26
|
+
# - `:realm` - 401 responses include a `WWW-Authenticate` with the realm set to the given value. default
|
27
|
+
# is an empty string.
|
24
28
|
def initialize(app, options={})
|
25
29
|
@app=app
|
26
30
|
@options = options
|
@@ -40,22 +44,46 @@ module OAuthenticator
|
|
40
44
|
oauth_signed_request_class = OAuthenticator::SignedRequest.including_config(@options[:config_methods])
|
41
45
|
oauth_request = oauth_signed_request_class.from_rack_request(request)
|
42
46
|
if oauth_request.errors
|
43
|
-
|
47
|
+
log_unauthenticated(env, oauth_request)
|
48
|
+
unauthenticated_response({'errors' => oauth_request.errors})
|
44
49
|
else
|
50
|
+
log_success(env, oauth_request)
|
45
51
|
env["oauth.consumer_key"] = oauth_request.consumer_key
|
46
|
-
env["oauth.
|
52
|
+
env["oauth.token"] = oauth_request.token
|
47
53
|
env["oauth.authenticated"] = true
|
48
54
|
@app.call(env)
|
49
55
|
end
|
50
56
|
end
|
51
57
|
end
|
52
58
|
|
53
|
-
|
54
|
-
|
59
|
+
private
|
60
|
+
|
61
|
+
# the response for an unauthenticated request. the argument will be a hash with the key 'errors', whose
|
62
|
+
# value is a hash with string keys indicating attributes with errors, and values being arrays of strings
|
55
63
|
# indicating error messages on the attribute key..
|
56
|
-
def
|
57
|
-
|
64
|
+
def unauthenticated_response(error_object)
|
65
|
+
# default to a blank realm, I suppose
|
66
|
+
realm = @options[:realm] || ''
|
67
|
+
response_headers = {"WWW-Authenticate" => %Q(OAuth realm="#{realm}"), 'Content-Type' => 'application/json'}
|
58
68
|
[401, response_headers, [JSON.pretty_generate(error_object)]]
|
59
69
|
end
|
70
|
+
|
71
|
+
# write a log entry regarding an unauthenticated request
|
72
|
+
def log_unauthenticated(env, oauth_request)
|
73
|
+
log :warn, "OAuthenticator rejected a request:\n" +
|
74
|
+
"\tAuthorization: #{env['HTTP_AUTHORIZATION']}\n" +
|
75
|
+
"\tErrors: #{JSON.generate(oauth_request.errors)}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# write a log entry for a successfully authenticated request
|
79
|
+
def log_success(env, oauth_request)
|
80
|
+
log :info, "OAuthenticator authenticated an authentic request with Authorization: #{env['HTTP_AUTHORIZATION']}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def log(level, message)
|
84
|
+
if @options[:logger]
|
85
|
+
@options[:logger].send(level, message)
|
86
|
+
end
|
87
|
+
end
|
60
88
|
end
|
61
89
|
end
|
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'uri'
|
3
|
+
require 'base64'
|
4
|
+
require 'cgi'
|
5
|
+
require 'strscan'
|
6
|
+
require 'oauthenticator/parse_authorization'
|
7
|
+
|
8
|
+
module OAuthenticator
|
9
|
+
# a request which may be signed with OAuth, generally in order to apply the signature to an outgoing request
|
10
|
+
# in the Authorization header.
|
11
|
+
#
|
12
|
+
# primarily this is to be used like:
|
13
|
+
#
|
14
|
+
# oauthenticator_signable_request = OAuthenticator::SignableRequest.new(
|
15
|
+
# :request_method => my_request_method,
|
16
|
+
# :uri => my_request_uri,
|
17
|
+
# :media_type => my_request_media_type,
|
18
|
+
# :body => my_request_body,
|
19
|
+
# :signature_method => my_oauth_signature_method,
|
20
|
+
# :consumer_key => my_oauth_consumer_key,
|
21
|
+
# :consumer_secret => my_oauth_consumer_secret,
|
22
|
+
# :token => my_oauth_token,
|
23
|
+
# :token_secret => my_oauth_token_secret,
|
24
|
+
# :realm => my_authorization_realm
|
25
|
+
# )
|
26
|
+
# my_http_request.headers['Authorization'] = oauthenticator_signable_request.authorization
|
27
|
+
class SignableRequest
|
28
|
+
# keys of OAuth protocol parameters which form the Authorization header (with an oauth_ prefix).
|
29
|
+
# signature is considered separately.
|
30
|
+
PROTOCOL_PARAM_KEYS = %w(consumer_key token signature_method timestamp nonce version).map(&:freeze).freeze
|
31
|
+
|
32
|
+
# initialize a signable request with the following attributes (keys may be string or symbol):
|
33
|
+
#
|
34
|
+
# - request_method (required) - get, post, etc. may be string or symbol.
|
35
|
+
# - uri (required) - request URI. to_s is called so URI or Addressable::URI or whatever may be passed.
|
36
|
+
# - media_type (required) - the request media type (may be nil if there is no body). note that this may be
|
37
|
+
# different than the Content-Type header; other components of that such as encoding must not be included.
|
38
|
+
# - body (required) - the request body. may be a String or an IO, or nil if no body is present.
|
39
|
+
# - hash_body? - whether to add the oauth_body_hash parameter, per the OAuth Request Body Hash
|
40
|
+
# specification. defaults to true. not used if the 'authorization' parameter is used.
|
41
|
+
# - signature_method (required*) - oauth signature method (String)
|
42
|
+
# - consumer_key (required*) - oauth consumer key (String)
|
43
|
+
# - consumer_secret (required*) - oauth consumer secret (String)
|
44
|
+
# - token (optional*) - oauth token; may be omitted if only using a consumer key (two-legged)
|
45
|
+
# - token_secret (optional) - must be present if token is present. must be omitted if token is omitted.
|
46
|
+
# - timestamp (optional*) - if omitted, defaults to the current time.
|
47
|
+
# if nil is passed, no oauth_timestamp will be present in the generated authorization.
|
48
|
+
# - nonce (optional*) - if omitted, defaults to a random string.
|
49
|
+
# if nil is passed, no oauth_nonce will be present in the generated authorization.
|
50
|
+
# - version (optional*) - must be nil or '1.0'. defaults to '1.0' if omitted.
|
51
|
+
# if nil is passed, no oauth_version will be present in the generated authorization.
|
52
|
+
# - realm (optional) - authorization realm.
|
53
|
+
# if nil is passed, no realm will be present in the generated authorization.
|
54
|
+
# - authorization - a hash of a received Authorization header, the result of a call to
|
55
|
+
# OAuthenticator.parse_authorization. it is useful for calculating the signature of a received request,
|
56
|
+
# but for fully authenticating a received request it is generally preferable to use
|
57
|
+
# OAuthenticator::SignedRequest. specifying this precludes the requirement to specify any of
|
58
|
+
# PROTOCOL_PARAM_KEYS.
|
59
|
+
#
|
60
|
+
# (*) attributes which are in PROTOCOL_PARAM_KEYS are unused (and not required) when the
|
61
|
+
# 'authorization' attribute is given for signature verification. normally, though, they are used and
|
62
|
+
# are required or optional as noted.
|
63
|
+
def initialize(attributes)
|
64
|
+
raise TypeError, "attributes must be a hash" unless attributes.is_a?(Hash)
|
65
|
+
# stringify symbol keys
|
66
|
+
@attributes = attributes.map { |k,v| {k.is_a?(Symbol) ? k.to_s : k => v} }.inject({}, &:update)
|
67
|
+
|
68
|
+
# validation - presence
|
69
|
+
required = %w(request_method uri media_type body)
|
70
|
+
required += %w(signature_method consumer_key) unless @attributes['authorization']
|
71
|
+
missing = required - @attributes.keys
|
72
|
+
raise ArgumentError, "missing: #{missing.inspect}" if missing.any?
|
73
|
+
other_recognized = PROTOCOL_PARAM_KEYS + %w(authorization consumer_secret token_secret realm hash_body?)
|
74
|
+
extra = @attributes.keys - (required + other_recognized)
|
75
|
+
raise ArgumentError, "received unrecognized @attributes: #{extra.inspect}" if extra.any?
|
76
|
+
|
77
|
+
if @attributes['authorization']
|
78
|
+
# this means we are signing an existing request to validate the received signature. don't use defaults.
|
79
|
+
unless @attributes['authorization'].is_a?(Hash)
|
80
|
+
raise TypeError, "authorization must be a Hash"
|
81
|
+
end
|
82
|
+
|
83
|
+
# if authorization is specified, protocol params should not be specified in the regular attributes
|
84
|
+
given_protocol_params = @attributes.reject { |k,v| !(PROTOCOL_PARAM_KEYS.include?(k) && v) }
|
85
|
+
if given_protocol_params.any?
|
86
|
+
raise ArgumentError, "an existing authorization was given, but protocol parameters were also " +
|
87
|
+
"given. protocol parameters should not be specified when verifying an existing authorization. " +
|
88
|
+
"given protocol parameters were: #{given_protocol_params.inspect}"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
# defaults
|
92
|
+
defaults = {
|
93
|
+
'version' => '1.0',
|
94
|
+
}
|
95
|
+
if @attributes['signature_method'] != 'PLAINTEXT'
|
96
|
+
defaults.update({
|
97
|
+
'nonce' => OpenSSL::Random.random_bytes(16).unpack('H*')[0],
|
98
|
+
'timestamp' => Time.now.to_i.to_s,
|
99
|
+
})
|
100
|
+
end
|
101
|
+
@attributes['authorization'] = PROTOCOL_PARAM_KEYS.map do |key|
|
102
|
+
{"oauth_#{key}" => @attributes.key?(key) ? @attributes[key] : defaults[key]}
|
103
|
+
end.inject({}, &:update).reject {|k,v| v.nil? }
|
104
|
+
@attributes['authorization']['realm'] = @attributes['realm'] unless @attributes['realm'].nil?
|
105
|
+
|
106
|
+
hash_body
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# returns the Authorization header generated for this request.
|
111
|
+
#
|
112
|
+
# @return [String] Authorization header
|
113
|
+
def authorization
|
114
|
+
"OAuth #{normalized_protocol_params_string}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# the oauth_signature calculated for this request.
|
118
|
+
#
|
119
|
+
# @return [String] oauth signature
|
120
|
+
def signature
|
121
|
+
rbmethod = SIGNATURE_METHODS[signature_method] ||
|
122
|
+
raise(ArgumentError, "invalid signature method: #{signature_method}")
|
123
|
+
rbmethod.bind(self).call
|
124
|
+
end
|
125
|
+
|
126
|
+
# the oauth_body_hash calculated for this request, if applicable, per the OAuth Request Body Hash
|
127
|
+
# specification.
|
128
|
+
#
|
129
|
+
# @return [String, nil] oauth body hash
|
130
|
+
def body_hash
|
131
|
+
BODY_HASH_METHODS[signature_method] ? BODY_HASH_METHODS[signature_method].bind(self).call : nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# protocol params for this request as described in section 3.4.1.3
|
135
|
+
#
|
136
|
+
# signature is not calculated for this - use #signed_protocol_params to get protocol params including a
|
137
|
+
# signature.
|
138
|
+
#
|
139
|
+
# note that if this is a previously-signed request, the oauth_signature attribute returned is the
|
140
|
+
# received value, NOT the value calculated by us.
|
141
|
+
#
|
142
|
+
# @return [Hash<String, String>] protocol params
|
143
|
+
def protocol_params
|
144
|
+
@attributes['authorization'].dup
|
145
|
+
end
|
146
|
+
|
147
|
+
# protocol params for this request as described in section 3.4.1.3, including our calculated
|
148
|
+
# oauth_signature.
|
149
|
+
#
|
150
|
+
# @return [Hash<String, String>] signed protocol params
|
151
|
+
def signed_protocol_params
|
152
|
+
protocol_params.merge('oauth_signature' => signature)
|
153
|
+
end
|
154
|
+
|
155
|
+
# is the media type application/x-www-form-urlencoded
|
156
|
+
#
|
157
|
+
# @return [Boolean]
|
158
|
+
def form_encoded?
|
159
|
+
media_type = @attributes['media_type']
|
160
|
+
# media tye is case insensitive per http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
161
|
+
media_type = media_type.downcase if media_type.is_a?(String)
|
162
|
+
media_type == "application/x-www-form-urlencoded"
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
# signature base string for signing. section 3.4.1
|
168
|
+
#
|
169
|
+
# @return [String]
|
170
|
+
def signature_base
|
171
|
+
parts = [normalized_request_method, base_string_uri, normalized_request_params_string]
|
172
|
+
parts.map { |v| OAuthenticator.escape(v) }.join('&')
|
173
|
+
end
|
174
|
+
|
175
|
+
# section 3.4.1.2
|
176
|
+
#
|
177
|
+
# @return [String]
|
178
|
+
def base_string_uri
|
179
|
+
URI.parse(@attributes['uri'].to_s).tap do |uri|
|
180
|
+
uri.scheme = uri.scheme.downcase
|
181
|
+
uri.host = uri.host.downcase
|
182
|
+
uri.normalize!
|
183
|
+
uri.fragment = nil
|
184
|
+
uri.query = nil
|
185
|
+
end.to_s
|
186
|
+
end
|
187
|
+
|
188
|
+
# section 3.4.1.1
|
189
|
+
#
|
190
|
+
# @return [String]
|
191
|
+
def normalized_request_method
|
192
|
+
@attributes['request_method'].to_s.upcase
|
193
|
+
end
|
194
|
+
|
195
|
+
# section 3.4.1.3.2
|
196
|
+
#
|
197
|
+
# @return [String]
|
198
|
+
def normalized_request_params_string
|
199
|
+
normalized_request_params.map { |kv| kv.map { |v| OAuthenticator.escape(v) } }.sort.map { |p| p.join('=') }.join('&')
|
200
|
+
end
|
201
|
+
|
202
|
+
# section 3.4.1.3
|
203
|
+
#
|
204
|
+
# @return [Array<Array<String> (size 2)>]
|
205
|
+
def normalized_request_params
|
206
|
+
query_params + protocol_params.reject { |k,v| %w(realm oauth_signature).include?(k) }.to_a + entity_params
|
207
|
+
end
|
208
|
+
|
209
|
+
# section 3.4.1.3.1
|
210
|
+
#
|
211
|
+
# parsed query params, extracted from the request URI. since keys may appear multiple times, represented
|
212
|
+
# as an array of two-element arrays and not a hash
|
213
|
+
#
|
214
|
+
# @return [Array<Array<String, nil> (size 2)>]
|
215
|
+
def query_params
|
216
|
+
parse_form_encoded(URI.parse(@attributes['uri'].to_s).query || '')
|
217
|
+
end
|
218
|
+
|
219
|
+
# section 3.4.1.3.1
|
220
|
+
#
|
221
|
+
# parsed entity params from the body, when the request is form encoded. since keys may appear multiple
|
222
|
+
# times, represented as an array of two-element arrays and not a hash
|
223
|
+
#
|
224
|
+
# @return [Array<Array<String, nil> (size 2)>]
|
225
|
+
def entity_params
|
226
|
+
if form_encoded?
|
227
|
+
parse_form_encoded(read_body)
|
228
|
+
else
|
229
|
+
[]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# like CGI.parse but it keeps keys without any value. doesn't keep blank keys though.
|
234
|
+
#
|
235
|
+
# @return [Array<Array<String, nil> (size 2)>]
|
236
|
+
def parse_form_encoded(data)
|
237
|
+
data.split(/[&;]/).map do |pair|
|
238
|
+
key, value = pair.split('=', 2).map { |v| CGI::unescape(v) }
|
239
|
+
[key, value] unless [nil, ''].include?(key)
|
240
|
+
end.compact
|
241
|
+
end
|
242
|
+
|
243
|
+
# string of protocol params including signature, sorted
|
244
|
+
#
|
245
|
+
# @return [String]
|
246
|
+
def normalized_protocol_params_string
|
247
|
+
signed_protocol_params.sort.map { |(k,v)| %Q(#{OAuthenticator.escape(k)}="#{OAuthenticator.escape(v)}") }.join(', ')
|
248
|
+
end
|
249
|
+
|
250
|
+
# reads the request body, be it String or IO
|
251
|
+
#
|
252
|
+
# @return [String] request body
|
253
|
+
def read_body
|
254
|
+
body = @attributes['body']
|
255
|
+
if body.nil?
|
256
|
+
''
|
257
|
+
elsif body.is_a?(String)
|
258
|
+
body
|
259
|
+
elsif body.respond_to?(:read) && body.respond_to?(:rewind)
|
260
|
+
body.rewind
|
261
|
+
body.read.tap do
|
262
|
+
body.rewind
|
263
|
+
end
|
264
|
+
else
|
265
|
+
raise TypeError, "Body must be a String or something IO-like (responding to #read and #rewind). " +
|
266
|
+
"got body = #{body.inspect}"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# set the oauth_body_hash to the hash of the request body
|
271
|
+
#
|
272
|
+
# @return [Void]
|
273
|
+
def hash_body
|
274
|
+
if hash_body?
|
275
|
+
@attributes['authorization']['oauth_body_hash'] = body_hash
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# whether we will hash the body, per oauth request body hash section 4.1, as well as whether the caller
|
280
|
+
# said to
|
281
|
+
#
|
282
|
+
# @return [Boolean]
|
283
|
+
def hash_body?
|
284
|
+
BODY_HASH_METHODS[signature_method] && !form_encoded? &&
|
285
|
+
(@attributes.key?('hash_body?') ? @attributes['hash_body?'] : true)
|
286
|
+
end
|
287
|
+
|
288
|
+
# signature method
|
289
|
+
#
|
290
|
+
# @return [String]
|
291
|
+
def signature_method
|
292
|
+
@attributes['authorization']['oauth_signature_method']
|
293
|
+
end
|
294
|
+
|
295
|
+
# signature, with method RSA-SHA1. section 3.4.3
|
296
|
+
#
|
297
|
+
# @return [String]
|
298
|
+
def rsa_sha1_signature
|
299
|
+
private_key = OpenSSL::PKey::RSA.new(@attributes['consumer_secret'])
|
300
|
+
Base64.encode64(private_key.sign(OpenSSL::Digest::SHA1.new, signature_base)).chomp.gsub(/\n/, '')
|
301
|
+
end
|
302
|
+
|
303
|
+
# signature, with method HMAC-SHA1. section 3.4.2
|
304
|
+
#
|
305
|
+
# @return [String]
|
306
|
+
def hmac_sha1_signature
|
307
|
+
# hmac secret is same as plaintext signature
|
308
|
+
secret = plaintext_signature
|
309
|
+
Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, secret, signature_base)).chomp.gsub(/\n/, '')
|
310
|
+
end
|
311
|
+
|
312
|
+
# signature, with method plaintext. section 3.4.4
|
313
|
+
#
|
314
|
+
# @return [String]
|
315
|
+
def plaintext_signature
|
316
|
+
@attributes.values_at('consumer_secret', 'token_secret').map { |v| OAuthenticator.escape(v) }.join('&')
|
317
|
+
end
|
318
|
+
|
319
|
+
# body hash, with a signature method which uses SHA1. oauth request body hash section 3.2
|
320
|
+
#
|
321
|
+
# @return [String]
|
322
|
+
def sha1_body_hash
|
323
|
+
Base64.encode64(OpenSSL::Digest::SHA1.digest(read_body)).chomp.gsub(/\n/, '')
|
324
|
+
end
|
325
|
+
|
326
|
+
# map of oauth signature methods to their signature instance methods on this class
|
327
|
+
SIGNATURE_METHODS = {
|
328
|
+
'RSA-SHA1'.freeze => instance_method(:rsa_sha1_signature),
|
329
|
+
'HMAC-SHA1'.freeze => instance_method(:hmac_sha1_signature),
|
330
|
+
'PLAINTEXT'.freeze => instance_method(:plaintext_signature),
|
331
|
+
}.freeze
|
332
|
+
|
333
|
+
# map of oauth signature methods to their body hash instance methods on this class. oauth request body
|
334
|
+
# hash section 3.1
|
335
|
+
BODY_HASH_METHODS = {
|
336
|
+
'RSA-SHA1'.freeze => instance_method(:sha1_body_hash),
|
337
|
+
'HMAC-SHA1'.freeze => instance_method(:sha1_body_hash),
|
338
|
+
}.freeze
|
339
|
+
end
|
340
|
+
end
|