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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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:
1 2 3 4 5 6 7 8 9 10 | 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:
1 | 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
1 2 3 4 5 6 7 8 9 10 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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.
No Comments
Leave a reply
You must be logged in to post a comment.