require 'addressable/uri' begin require 'securerandom' rescue LoadError require 'compat/securerandom' end module Signet #:nodoc: module OAuth1 OUT_OF_BAND = 'oob' ## # Converts a value to a percent-encoded String according to # the rules given in RFC 5849. All non-unreserved characters are # percent-encoded. # # @param [Symbol, #to_str] value The value to be encoded. # # @return [String] The percent-encoded value. def self.encode(value) value = value.to_s if value.kind_of?(Symbol) return Addressable::URI.encode_component( value, Addressable::URI::CharacterClasses::UNRESERVED ) end ## # Converts a percent-encoded String to an unencoded value. # # @param [#to_str] value # The percent-encoded String to be unencoded. # # @return [String] The unencoded value. def self.unencode(value) return Addressable::URI.unencode_component(value) end ## # Returns a timestamp suitable for use as an 'oauth_timestamp' # value. # # @return [String] The current timestamp. def self.generate_timestamp() return Time.now.to_i.to_s end ## # Returns a nonce suitable for use as an 'oauth_nonce' # value. # # @return [String] A random nonce. def self.generate_nonce() return SecureRandom.random_bytes(16).unpack('H*').join('') end ## # Processes an options Hash to find a credential key value. # Allows for greater flexibility in configuration. # # @param [Symbol] credential_type # One of :client, :temporary, # :token, :consumer, :request, # or :access. # # @return [String] The credential key value. def self.extract_credential_key_option(credential_type, options) credential_key_symbol = ("#{credential_type}_credential_key").to_sym credential_symbol = ("#{credential_type}_credential").to_sym if options[credential_key_symbol] credential_key = options[credential_key_symbol] elsif options[credential_symbol] require 'signet/oauth_1/credential' if !options[credential_symbol].respond_to?(:key) raise TypeError, "Expected Signet::OAuth1::Credential, " + "got #{options[credential_symbol].class}." end credential_key = options[credential_symbol].key elsif options[:client] require 'signet/oauth_1/client' if !options[:client].kind_of?(::Signet::OAuth1::Client) raise TypeError, "Expected Signet::OAuth1::Client, got #{options[:client].class}." end credential_key = options[:client].send(credential_key_symbol) else credential_key = nil end if credential_key != nil && !credential_key.kind_of?(String) raise TypeError, "Expected String, got #{credential_key.class}." end return credential_key end ## # Processes an options Hash to find a credential secret value. # Allows for greater flexibility in configuration. # # @param [Symbol] credential_type # One of :client, :temporary, # :token, :consumer, :request, # or :access. # # @return [String] The credential secret value. def self.extract_credential_secret_option(credential_type, options) credential_secret_symbol = ("#{credential_type}_credential_secret").to_sym credential_symbol = ("#{credential_type}_credential").to_sym if options[credential_secret_symbol] credential_secret = options[credential_secret_symbol] elsif options[credential_symbol] require 'signet/oauth_1/credential' if !options[credential_symbol].respond_to?(:secret) raise TypeError, "Expected Signet::OAuth1::Credential, " + "got #{options[credential_symbol].class}." end credential_secret = options[credential_symbol].secret elsif options[:client] require 'signet/oauth_1/client' if !options[:client].kind_of?(::Signet::OAuth1::Client) raise TypeError, "Expected Signet::OAuth1::Client, got #{options[:client].class}." end credential_secret = options[:client].send(credential_secret_symbol) else credential_secret = nil end if credential_secret != nil && !credential_secret.kind_of?(String) raise TypeError, "Expected String, got #{credential_secret.class}." end return credential_secret end ## # Normalizes a set of OAuth parameters according to the algorithm given # in RFC 5849. Sorts key/value pairs lexically by byte order, first by # key, then by value, joins key/value pairs with the '=' character, then # joins the entire parameter list with '&' characters. # # @param [Enumerable] parameters The OAuth parameter list. # # @return [String] The normalized parameter list. def self.normalize_parameters(parameters) if !parameters.kind_of?(Enumerable) raise TypeError, "Expected Enumerable, got #{parameters.class}." end parameter_list = parameters.map do |k, v| next if k == "oauth_signature" # This is probably the wrong place to try to exclude the realm "#{self.encode(k)}=#{self.encode(v)}" end return parameter_list.compact.sort.join("&") end ## # Generates a signature base string according to the algorithm given in # RFC 5849. Joins the method, URI, and normalized parameter string with # '&' characters. # # @param [String] method The HTTP method. # @param [Addressable::URI, String, #to_str] The URI. # @param [Enumerable] parameters The OAuth parameter list. # # @return [String] The signature base string. def self.generate_base_string(method, uri, parameters) if !parameters.kind_of?(Enumerable) raise TypeError, "Expected Enumerable, got #{parameters.class}." end method = method.to_s.upcase uri = Addressable::URI.parse(uri).normalize uri_parameters = uri.query_values.to_a uri = uri.omit(:query, :fragment).to_s merged_parameters = uri_parameters.concat(parameters.map { |k, v| [k, v] }) parameter_string = self.normalize_parameters(merged_parameters) return [ self.encode(method), self.encode(uri), self.encode(parameter_string) ].join('&') end ## # Generates an Authorization header from a parameter list # according to the rules given in RFC 5849. # # @param [Enumerable] parameters The OAuth parameter list. # @param [String] realm # The Authorization realm. See RFC 2617. # # @return [String] The Authorization header. def self.generate_authorization_header(parameters, realm=nil) if !parameters.kind_of?(Enumerable) || parameters.kind_of?(String) raise TypeError, "Expected Enumerable, got #{parameters.class}." end parameter_list = parameters.map do |k, v| if k == 'realm' raise ArgumentError, 'The "realm" parameter must be specified as a separate argument.' end "#{self.encode(k)}=\"#{self.encode(v)}\"" end if realm realm = realm.gsub('"', '\"') parameter_list.unshift("realm=\"#{realm}\"") end return 'OAuth ' + parameter_list.join(", ") end ## # Parses an Authorization header into its component # parameters. Parameter keys and values are decoded according to the # rules given in RFC 5849. def self.parse_authorization_header(header) if !header.kind_of?(String) raise TypeError, "Expected String, got #{header.class}." end unless header[0...6] == 'OAuth ' raise ArgumentError, 'Parsing non-OAuth Authorization headers is out of scope.' end header = header.gsub(/^OAuth /, '') return header.split(/,\s*/).inject([]) do |accu, pair| k = pair[/^(.*?)=\"[^\"]*\"/, 1] v = pair[/^.*?=\"([^\"]*)\"/, 1] if k != 'realm' k = self.unencode(k) v = self.unencode(v) else v = v.gsub('\"', '"') end accu << [k, v] accu end end ## # Parses an application/x-www-form-urlencoded HTTP response # body into an OAuth key/secret pair. # # @param [String] body The response body. # # @return [Signet::OAuth1::Credential] The OAuth credentials. def self.parse_form_encoded_credentials(body) if !body.kind_of?(String) raise TypeError, "Expected String, got #{body.class}." end return Signet::OAuth1::Credential.new( Addressable::URI.form_unencode(body) ) end ## # Generates an OAuth signature using the signature method indicated in the # parameter list. Unsupported signature methods will result in a # NotImplementedError exception being raised. # # @param [String] method The HTTP method. # @param [Addressable::URI, String, #to_str] The URI. # @param [Enumerable] parameters The OAuth parameter list. # @param [String] client_credential_secret The client credential secret. # @param [String] token_credential_secret # The token credential secret. Omitted when unavailable. # # @return [String] The signature. def self.sign_parameters(method, uri, parameters, client_credential_secret, token_credential_secret=nil) # Technically, the token_credential_secret parameter here may actually # be a temporary credential secret when obtaining a token credential # for the first time base_string = self.generate_base_string(method, uri, parameters) signature_method = Hash[parameters]['oauth_signature_method'] case signature_method when 'HMAC-SHA1' require 'signet/oauth_1/signature_methods/hmac_sha1' return Signet::OAuth1::HMACSHA1.generate_signature( base_string, client_credential_secret, token_credential_secret ) else raise NotImplementedError, "Unsupported signature method: #{signature_method}" end end ## # Generates an OAuth parameter list to be used when obtaining a set of # temporary credentials. # # @param [Hash] options # The configuration parameters for the request. # - :client_credential_key — # The client credential key. # - :callback — # The OAuth callback. Defaults to {Signet::OAuth1::OUT_OF_BAND}. # - :signature_method — # The signature method. Defaults to 'HMAC-SHA1'. # - :additional_parameters — # Non-standard additional parameters. # # @return [Array] # The parameter list as an Array of key/value pairs. def self.unsigned_temporary_credential_parameters(options={}) options = { :callback => ::Signet::OAuth1::OUT_OF_BAND, :signature_method => 'HMAC-SHA1', :additional_parameters => [] }.merge(options) client_credential_key = self.extract_credential_key_option(:client, options) if client_credential_key == nil raise ArgumentError, "Missing :client_credential_key parameter." end parameters = [ ["oauth_consumer_key", client_credential_key], ["oauth_signature_method", options[:signature_method]], ["oauth_timestamp", self.generate_timestamp()], ["oauth_nonce", self.generate_nonce()], ["oauth_version", "1.0"], ["oauth_callback", options[:callback]] ] # Works for any Enumerable options[:additional_parameters].each do |key, value| parameters << [key, value] end return parameters end ## # Appends the optional 'oauth_token' and 'oauth_callback' parameters to # the base authorization URI. # # @param [Addressable::URI, String, #to_str] authorization_uri # The base authorization URI. # # @return [String] The authorization URI to redirect the user to. def self.generate_authorization_uri(authorization_uri, options={}) options = { :callback => nil, :additional_parameters => {} }.merge(options) temporary_credential_key = self.extract_credential_key_option(:temporary, options) parsed_uri = Addressable::URI.parse(authorization_uri) query_values = parsed_uri.query_values || {} if options[:additional_parameters] query_values = query_values.merge(Hash[options[:additional_parameters]]) end if temporary_credential_key query_values['oauth_token'] = temporary_credential_key end if options[:callback] query_values['oauth_callback'] = options[:callback] end parsed_uri.query_values = query_values return parsed_uri.normalize.to_s end ## # Generates an OAuth parameter list to be used when obtaining a set of # token credentials. # # @param [Hash] options # The configuration parameters for the request. # - :client_credential_key — # The client credential key. # - :temporary_credential_key — # The temporary credential key. # - :verifier — # The OAuth verifier. # - :signature_method — # The signature method. Defaults to 'HMAC-SHA1'. # # @return [Array] # The parameter list as an Array of key/value pairs. def self.unsigned_token_credential_parameters(options={}) options = { :signature_method => 'HMAC-SHA1', :verifier => nil }.merge(options) client_credential_key = self.extract_credential_key_option(:client, options) temporary_credential_key = self.extract_credential_key_option(:temporary, options) if client_credential_key == nil raise ArgumentError, "Missing :client_credential_key parameter." end if temporary_credential_key == nil raise ArgumentError, "Missing :temporary_credential_key parameter." end if options[:verifier] == nil raise ArgumentError, "Missing :verifier parameter." end parameters = [ ["oauth_consumer_key", client_credential_key], ["oauth_token", temporary_credential_key], ["oauth_signature_method", options[:signature_method]], ["oauth_timestamp", self.generate_timestamp()], ["oauth_nonce", self.generate_nonce()], ["oauth_verifier", options[:verifier]], ["oauth_version", "1.0"] ] # No additional parameters allowed here return parameters end ## # Generates an OAuth parameter list to be used when requesting a # protected resource. # # @param [Hash] options # The configuration parameters for the request. # - :client_credential_key — # The client credential key. # - :token_credential_key — # The token credential key. # - :signature_method — # The signature method. Defaults to 'HMAC-SHA1'. # # @return [Array] # The parameter list as an Array of key/value pairs. def self.unsigned_resource_parameters(options={}) options = { :signature_method => 'HMAC-SHA1' }.merge(options) client_credential_key = self.extract_credential_key_option(:client, options) token_credential_key = self.extract_credential_key_option(:token, options) if client_credential_key == nil raise ArgumentError, "Missing :client_credential_key parameter." end if token_credential_key == nil raise ArgumentError, "Missing :token_credential_key parameter." end parameters = [ ["oauth_consumer_key", client_credential_key], ["oauth_token", token_credential_key], ["oauth_signature_method", options[:signature_method]], ["oauth_timestamp", self.generate_timestamp()], ["oauth_nonce", self.generate_nonce()], ["oauth_version", "1.0"] ] # No additional parameters allowed here return parameters end end end