oAuth 1.0 in Ruby without a gem

10th December, 2011 - Posted by david

Recently I decided to figure out what the oAuth 1.0 protocol was all about and try to implement it in Ruby, as part of a way a) to practice by Ruby on Rails, b) have a look at the Twitter API and c) use both to get an understanding of how websites let you log in/comment via your Facebook/Twitter/Google etc. account, for potential use in future web projects. Sure there’s an oAuth gem out there, and a Twitter gem and probably a generic login gem (or if not, there’s an idea!) but I thought I’d get more out of the process by coding everything from scratch. So, first up is a generic overview of the oAuth protocol.

Each request will have a method (i.e. GET, POST etc.), a base URL to handle the request at the source site (Twitter, Yelp etc.) and a certain set of parameters. Every request I’ve dealt with has had the same 5 parameters, along with various other ones specific to the request you’re making. So, in Ruby, I’d have something like:

consumer_key = 'abcdefghijklmnop' # Obtainable from your destination site's API admin panel
consumer_secret = 'zyxwvutsrqponm' # As above
method = 'GET'
uri = 'https://api.site.com/resource/section.format'
params = params(consumer_key)

# These 5 parameters are common to all calls
def params(consumer_key)
  params = {
    'oauth_consumer_key' => consumer_key, # Your consumer key
    'oauth_nonce' => generate_nonce, # A random string, see below for function
    'oauth_signature_method' => 'HMAC-SHA1', # How you'll be signing (see later)
    'oauth_timestamp' => Time.now.getutc.to_i.to_s, # Timestamp
    'oauth_version' => '1.0' # oAuth version
  }
end

def generate_nonce(size=7)
  Base64.encode64(OpenSSL::Random.random_bytes(size)).gsub(/\W/, '')
end

Next, you’ll need to add in any extra parameters to your params hash, e.g. your access token if you have it, and then combine all the above to generate a base string:

params['abc'] = 'xyz'
signature_base_string = signature_base_string(method, uri, params)

#where signature_base_string function is:

def signature_base_string(method, uri, params)
  # Join up the parameters into one long URL-safe string of key value pairs
  encoded_params = params.sort.collect{ |k, v| url_encode("#{k}=#{v}") }.join('%26')
  # Join the above with your method and URL-safe destination URL
  method + '&' + url_encode(uri) + '&' + encoded_params
end

# I'm a PHP developer primarily, hence the name of this function!
def url_encode(string)
 CGI::escape(string)
end

Next up, you need to generate a signing key, which is a combination of your consumer secret and your access token for the current session, if you have one at this stage (you may not, if the user still hasn’t logged in yet: in that case, a blank string will suffice). With this signing key, you sign your signature base string to get your oauth signature:

access_token ||= '' # if not set, blank string
signing_key = consumer_secret + '&' + access_token
params['oauth_signature'] = url_encode(sign(signing_key, signature_base_string))

# where sign is:
def sign(key, base_string)
  digest = OpenSSL::Digest::Digest.new('sha1')
  hmac = OpenSSL::HMAC.digest(digest, key, base_string)
  Base64.encode64(hmac).chomp.gsub(/\n/, '')
end

At this point, you’ve all your info nicely encoded in the oauth_signature using your private consumer secret. So, in a kind of public/private key partnership, you need to give the service your public consumer key, so it can validate the encoding of the oauth_signature at the destination:

params['oauth_consumer_key'] = consumer_key # from above

So, you’re nearly ready to make your oAuth request. One final thing: all these parameters need to go into the Authorization line in your HTTP header, which is simply a matter of generating another string, as well as indicating you’re using oAuth:

header_string = header(params)

 # where header is:
def header(params)
  header = "OAuth "
  params.each do |k, v|
    header += "#{k}=\"#{v}\", "
  end
  header.slice(0..-3) # chop off last ", "
end

So, to make your HTTP request, I wrote a generic function (request_data) that will do either a GET or a POST, make the request and return the response body:

response = request_data(header_string, uri, method)

# where request_data is
def request_data(header, base_uri, method, post_data=nil)
  url = URI.parse(base_uri)
  http = Net::HTTP.new(url.host, 443) # set to 80 if not using HTTPS
  http.use_ssl = true # ignore if not using HTTPS
  if method == 'POST'
    # post_data here should be your encoded POST string, NOT an array
    resp, data = http.post(url.path, post_data, { 'Authorization' => header })
  else
    resp, data = http.get(url.to_s, { 'Authorization' => header })
  end
  resp.body
end

And there you go. You should have a response from the API in whatever format you requested, be it JSON, XML or whatever. In my next post, I’ll explain how to use the above specifically for the Twitter API.

Tags: api oauth ruby twitter | david | 10th Dec, 2011 at 22:56pm | No Comments

No Comments

Leave a reply

You must be logged in to post a comment.