oauthenticator 0.1.4 → 1.0.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.
- 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
|