class OAuthenticator::SignableRequest
a request which may be signed with OAuth, generally in order to apply the signature to an outgoing request in the Authorization header.
primarily this is to be used like:
oauthenticator_signable_request = OAuthenticator::SignableRequest.new( :request_method => my_request_method, :uri => my_request_uri, :media_type => my_request_media_type, :body => my_request_body, :signature_method => my_oauth_signature_method, :consumer_key => my_oauth_consumer_key, :consumer_secret => my_oauth_consumer_secret, :token => my_oauth_token, :token_secret => my_oauth_token_secret, :realm => my_authorization_realm ) my_http_request.headers['Authorization'] = oauthenticator_signable_request.authorization
Constants
- BODY_HASH_METHODS
map of oauth signature methods to their body hash instance methods on this class. oauth request body hash section 3.1
- PROTOCOL_PARAM_KEYS
keys of OAuth protocol parameters which form the Authorization header (with an oauth_ prefix). signature is considered separately.
- SIGNATURE_METHODS
map of oauth signature methods to their signature instance methods on this class
Public Class Methods
initialize a signable request with the following attributes (keys may be string or symbol):
-
request_method (required) - get, post, etc. may be string or symbol.
-
uri (required) - request URI. to_s is called so URI or Addressable::URI or whatever may be passed.
-
media_type (required) - the request media type (may be nil if there is no body). note that this may be different than the Content-Type header; other components of that such as encoding must not be included.
-
body (required) - the request body. may be a String or an IO, or nil if no body is present.
-
hash_body
? - whether to add the oauth_body_hash parameter, per the OAuth Request Body Hash specification. defaults to true. not used if the ‘authorization’ parameter is used. -
signature_method
(required*) - oauth signature method (String) -
consumer_key (required*) - oauth consumer key (String)
-
consumer_secret (required*) - oauth consumer secret (String)
-
token (optional*) - oauth token; may be omitted if only using a consumer key (two-legged)
-
token_secret (optional) - must be present if token is present. must be omitted if token is omitted.
-
timestamp (optional*) - if omitted, defaults to the current time. if nil is passed, no oauth_timestamp will be present in the generated authorization.
-
nonce (optional*) - if omitted, defaults to a random string. if nil is passed, no oauth_nonce will be present in the generated authorization.
-
version (optional*) - must be nil or ‘1.0’. defaults to ‘1.0’ if omitted. if nil is passed, no oauth_version will be present in the generated authorization.
-
realm (optional) - authorization realm. if nil is passed, no realm will be present in the generated authorization.
-
authorization - a hash of a received Authorization header, the result of a call to
OAuthenticator.parse_authorization
. it is useful for calculating the signature of a received request, but for fully authenticating a received request it is generally preferable to useOAuthenticator::SignedRequest
. specifying this precludes the requirement to specify any ofPROTOCOL_PARAM_KEYS
.
(*) attributes which are in PROTOCOL_PARAM_KEYS
are unused (and not required) when the ‘authorization’ attribute is given for signature verification. normally, though, they are used and are required or optional as noted.
# File lib/oauthenticator/signable_request.rb, line 63 def initialize(attributes) raise TypeError, "attributes must be a hash" unless attributes.is_a?(Hash) # stringify symbol keys @attributes = attributes.map { |k,v| {k.is_a?(Symbol) ? k.to_s : k => v} }.inject({}, &:update) # validation - presence required = %w(request_method uri media_type body) required += %w(signature_method consumer_key) unless @attributes['authorization'] missing = required - @attributes.keys raise ArgumentError, "missing required attributes: #{missing.inspect}" if missing.any? other_recognized = PROTOCOL_PARAM_KEYS + %w(authorization consumer_secret token_secret realm hash_body?) extra = @attributes.keys - (required + other_recognized) raise ArgumentError, "received unrecognized attributes: #{extra.inspect}" if extra.any? if @attributes['authorization'] # this means we are signing an existing request to validate the received signature. don't use defaults. unless @attributes['authorization'].is_a?(Hash) raise TypeError, "authorization must be a Hash" end # if authorization is specified, protocol params should not be specified in the regular attributes given_protocol_params = @attributes.reject { |k,v| !(PROTOCOL_PARAM_KEYS.include?(k) && v) } if given_protocol_params.any? raise ArgumentError, "an existing authorization was given, but protocol parameters were also " + "given. protocol parameters should not be specified when verifying an existing authorization. " + "given protocol parameters were: #{given_protocol_params.inspect}" end else # defaults defaults = { 'version' => '1.0', } if @attributes['signature_method'] != 'PLAINTEXT' defaults.update({ 'nonce' => OpenSSL::Random.random_bytes(16).unpack('H*')[0], 'timestamp' => Time.now.to_i.to_s, }) end @attributes['authorization'] = PROTOCOL_PARAM_KEYS.map do |key| {"oauth_#{key}" => @attributes.key?(key) ? @attributes[key] : defaults[key]} end.inject({}, &:update).reject {|k,v| v.nil? } @attributes['authorization']['realm'] = @attributes['realm'] unless @attributes['realm'].nil? hash_body end end
Public Instance Methods
the oauth_body_hash calculated for this request, if applicable, per the OAuth Request Body Hash specification.
@return [String, nil] oauth body hash
# File lib/oauthenticator/signable_request.rb, line 130 def body_hash BODY_HASH_METHODS[signature_method] ? BODY_HASH_METHODS[signature_method].bind(self).call : nil end
is the media type application/x-www-form-urlencoded
@return [Boolean]
# File lib/oauthenticator/signable_request.rb, line 158 def form_encoded? media_type = @attributes['media_type'] # media tye is case insensitive per http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 media_type = media_type.downcase if media_type.is_a?(String) media_type == "application/x-www-form-urlencoded" end
protocol params for this request as described in section 3.4.1.3
signature is not calculated for this - use signed_protocol_params
to get protocol params including a signature.
note that if this is a previously-signed request, the oauth_signature attribute returned is the received value, NOT the value calculated by us.
@return [Hash<String, String>] protocol params
# File lib/oauthenticator/signable_request.rb, line 143 def protocol_params @attributes['authorization'].dup end
the oauth_signature calculated for this request.
@return [String] oauth signature
# File lib/oauthenticator/signable_request.rb, line 120 def signature rbmethod = SIGNATURE_METHODS[signature_method] || raise(ArgumentError, "invalid signature method: #{signature_method}") rbmethod.bind(self).call end
protocol params for this request as described in section 3.4.1.3, including our calculated oauth_signature.
@return [Hash<String, String>] signed protocol params
# File lib/oauthenticator/signable_request.rb, line 151 def signed_protocol_params protocol_params.merge('oauth_signature' => signature) end
Private Instance Methods
section 3.4.1.2
@return [String]
# File lib/oauthenticator/signable_request.rb, line 178 def base_string_uri Addressable::URI.parse(@attributes['uri'].to_s).tap do |uri| uri.scheme = uri.scheme.downcase if uri.scheme uri.host = uri.host.downcase if uri.host uri.normalize! uri.fragment = nil uri.query = nil end.to_s end
body hash with a given digest
@param digest_class [Class] the digest class @return [String]
# File lib/oauthenticator/signable_request.rb, line 370 def digest_body_hash(digest_class) Base64.encode64(digest_class.digest(read_body)).gsub(/\n/, '') end
section 3.4.1.3.1
parsed entity params from the body, when the request is form encoded. since keys may appear multiple times, represented as an array of two-element arrays and not a hash
@return [Array<Array<String, nil> (size 2)>]
# File lib/oauthenticator/signable_request.rb, line 225 def entity_params if form_encoded? parse_form_encoded(read_body) else [] end end
set the oauth_body_hash to the hash of the request body
@return [Void]
# File lib/oauthenticator/signable_request.rb, line 273 def hash_body if hash_body? @attributes['authorization']['oauth_body_hash'] = body_hash end end
whether we will hash the body, per oauth request body hash section 4.1, as well as whether the caller said to
@return [Boolean]
# File lib/oauthenticator/signable_request.rb, line 283 def hash_body? BODY_HASH_METHODS.key?(signature_method) && !form_encoded? && (@attributes.key?('hash_body?') ? @attributes['hash_body?'] : true) end
signature with a HMAC digest
@param digest_class [Class] the digest class @return [String]
# File lib/oauthenticator/signable_request.rb, line 330 def hmac_digest_signature(digest_class) # hmac secret is same as plaintext signature secret = plaintext_signature Base64.encode64(OpenSSL::HMAC.digest(digest_class.new, secret, signature_base)).gsub(/\n/, '') end
signature, with method HMAC-SHA1. section 3.4.2
@return [String]
# File lib/oauthenticator/signable_request.rb, line 306 def hmac_sha1_signature hmac_digest_signature(OpenSSL::Digest::SHA1) end
signature, with method HMAC-SHA256. OAuthenticator
extension, outside of spec. do not use. unless you want to.
@return [String]
# File lib/oauthenticator/signable_request.rb, line 314 def hmac_sha256_signature hmac_digest_signature(OpenSSL::Digest::SHA256) end
signature, with method HMAC-SHA512. OAuthenticator
extension, outside of spec. do not use. unless you want to.
@return [String]
# File lib/oauthenticator/signable_request.rb, line 322 def hmac_sha512_signature hmac_digest_signature(OpenSSL::Digest::SHA512) end
string of protocol params including signature, sorted
@return [String]
# File lib/oauthenticator/signable_request.rb, line 246 def normalized_protocol_params_string signed_protocol_params.sort.map { |(k,v)| %Q(#{OAuthenticator.escape(k)}="#{OAuthenticator.escape(v)}") }.join(', ') end
section 3.4.1.1
@return [String]
# File lib/oauthenticator/signable_request.rb, line 191 def normalized_request_method @attributes['request_method'].to_s.upcase end
section 3.4.1.3
@return [Array<Array<String, nil> (size 2)>]
# File lib/oauthenticator/signable_request.rb, line 205 def normalized_request_params query_params + protocol_params.reject { |k,v| %w(realm oauth_signature).include?(k) }.to_a + entity_params end
section 3.4.1.3.2
@return [String]
# File lib/oauthenticator/signable_request.rb, line 198 def normalized_request_params_string normalized_request_params.map { |kv| kv.map { |v| OAuthenticator.escape(v) } }.sort.map { |p| p.join('=') }.join('&') end
like CGI.parse but it keeps keys without any value. doesn’t keep blank keys though.
@return [Array<Array<String, nil> (size 2)>]
# File lib/oauthenticator/signable_request.rb, line 236 def parse_form_encoded(data) data.split(/[&;]/).map do |pair| key, value = pair.split('=', 2).map { |v| CGI::unescape(v) } [key, value] unless [nil, ''].include?(key) end.compact end
signature, with method plaintext. section 3.4.4
@return [String]
# File lib/oauthenticator/signable_request.rb, line 339 def plaintext_signature @attributes.values_at('consumer_secret', 'token_secret').map { |v| OAuthenticator.escape(v) }.join('&') end
section 3.4.1.3.1
parsed query params, extracted from the request URI. since keys may appear multiple times, represented as an array of two-element arrays and not a hash
@return [Array<Array<String, nil> (size 2)>]
# File lib/oauthenticator/signable_request.rb, line 215 def query_params parse_form_encoded(URI.parse(@attributes['uri'].to_s).query || '') end
reads the request body, be it String or IO
@return [String] request body
# File lib/oauthenticator/signable_request.rb, line 253 def read_body body = @attributes['body'] if body.nil? '' elsif body.is_a?(String) body elsif body.respond_to?(:read) && body.respond_to?(:rewind) body.rewind body.read.tap do body.rewind end else raise TypeError, "Body must be a String or something IO-like (responding to #read and #rewind). " + "got body = #{body.inspect}" end end
signature, with method RSA-SHA1. section 3.4.3
@return [String]
# File lib/oauthenticator/signable_request.rb, line 298 def rsa_sha1_signature private_key = OpenSSL::PKey::RSA.new(@attributes['consumer_secret']) Base64.encode64(private_key.sign(OpenSSL::Digest::SHA1.new, signature_base)).gsub(/\n/, '') end
body hash, with a signature method which uses SHA1. oauth request body hash section 3.2
@return [String]
# File lib/oauthenticator/signable_request.rb, line 346 def sha1_body_hash digest_body_hash(OpenSSL::Digest::SHA1) end
body hash, with a signature method which uses SHA256. OAuthenticator
extension, outside of spec. do not use. unless you want to.
@return [String]
# File lib/oauthenticator/signable_request.rb, line 354 def sha256_body_hash digest_body_hash(OpenSSL::Digest::SHA256) end
body hash, with a signature method which uses SHA512. OAuthenticator
extension, outside of spec. do not use. unless you want to.
@return [String]
# File lib/oauthenticator/signable_request.rb, line 362 def sha512_body_hash digest_body_hash(OpenSSL::Digest::SHA512) end
signature base string for signing. section 3.4.1
@return [String]
# File lib/oauthenticator/signable_request.rb, line 170 def signature_base parts = [normalized_request_method, base_string_uri, normalized_request_params_string] parts.map { |v| OAuthenticator.escape(v) }.join('&') end
signature method
@return [String]
# File lib/oauthenticator/signable_request.rb, line 291 def signature_method @attributes['authorization']['oauth_signature_method'] end