oAuth with the Twitter API in Ruby on Rails without a gem

15th December, 2011 - Posted by david

This post is a follow on from my previous post about the oAuth protocol in general in Ruby. Here I detail how to let a user authenticate themselves via Twitter to your web app, request a list of people they’re following (a GET operation) and how to follow a specific user (a POST operation). As this is a follow-on from an earlier post, the following functions used here can be seen there:

  • params(consumer_key)
  • generate_nonce(size)
  • signature_base_string(method, uri, params)
  • url_encode(string)
  • sign(key, base_string)
  • header(params)
  • request_data(header, base_uri, method, post_data=nil)

The Log-in Process

When you sign up for an API key from Twitter, Yelp etc. you’ll be given a Consumer Key and a Consumer Secret. You may also be given an Access Token and an Access Token secret, but that’s for you logging into your own account. If that’s all you wish to do, you can skip this section.

So, if you want to let a user log in to their Twitter (or whatever) account via your site, you need to get an access token. The process for this is as follows:

  1. You request a unique Request Token from Twitter for your ‘Log-in with Twitter’ button
  2. You use this request token, as well as where on your site you want the user to be re-directed back to after they’re authenticated, to build the URL the button points to
  3. They click the ‘Log-in with Twitter’ button on your site
  4. The user is brought to Twitter where they enter their username and password
  5. Twitter re-directs them back to your site, to the URL you gave them in step 2
  6. Back at your site, you’ll now have an oAuth verifier
  7. This can be used to get the user’s Twitter user info, which has been authenticated by Twitter

Step 1: Getting an oAuth request token for your current session

Twitter’s URL for getting a request token from is https://api.twitter.com/oauth/request_token. The request for the token contains a number of parameters, which are then combined with the URL you’re going to be sending the data to and the method of your request (i.e. GET, POST etc.) to generate what’s called a base signature. At this point you don’t have an access token, so your signing key is simply your consumer secret followed by an ‘&‘. Using the functions I’ve mentioned earlier, this can be done as follows:

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
callback_url = 'http://www.mysite.com/logged-in'
method = 'POST'
uri = 'https://api.twitter.com/oauth/request_token'
params = params(consumer_key)
params['oauth_callback'] = url_encode(callback_url)
params['oauth_signature'] = url_encode(sign(consumer_secret + '&', signature_base_string(method, uri, params)))
token_data = parse_string(request_data(header(params), uri, method))
auth_token, auth_token_secret = [token_data['oauth_token'], token_data['oauth_token_secret']] # save these values, they'll be used again later

# where parse_string is simply
def parse_string(str)
    ret = {}
    str.split('&').each do |pair|
        key_and_val = pair.split('=')
        ret[key_and_val[0]] = key_and_val[1]
    end
    ret
end

Steps 2-5: Authenticating with Twitter

Once you have your request/access token, the URL to direct your user to is simply:

1
login_url = 'https://api.twitter.com/oauth/authorize?oauth_token='+auth_token

You can use standard <%= link_to 'Login', @login_url %> to generate a HTML anchor in your ERB template or whatever you choose. The user will then be directed to Twitter, where they enter their log-in details and get directed back to your callback URL.

Steps 6-7: Getting the user’s info

When the user is directed back to your site, you’ll be sent an oauth_verifier. This can be obtained via Rails in your callback URL’s corresponding controller, via params[:oauth_verifier]. You need to use this to request their user info, as a final check before they’re fully logged in. This results in a new auth token (or what Twitter calls an access token) and auth token secret, which should replace your previous stored values. It is assumed the code below is stored in a model class, where you need to pass the auth verifier from your controller.

1
2
3
4
5
6
7
method = 'POST'
base_uri = 'https://api.twitter.com/oauth/access_token'
params = params() # not to be confused with params in your controller
params['oauth_verifier'] = auth_verifier # this does come from params in the controller
#auth_token_secret here is from above
params['oauth_signature'] = url_encode(sign(consumer_secret + '&' + auth_token_secret, signature_base_string(method, uri, params)))
data = parse_string(request_data(header(params), base_uri, method))

data will now contain an array with things such as screen_name, user_id etc. of the user who just logged in. It’ll also contain a new oauth_token and oauth_token_secret, which should be saved as they’ll be used again.

Now you have a fully validated user, who has authenticated you to access information on Twitter via their Twitter account. So now, let’s access some of that info.

Getting the people a user is following (a GET request)

The process for all GET requests is pretty similar and roughly follows what we’ve done before. We have our array of standard parameters. To this, each of the GET parameters are passed. We use our access token and consumer key & secret to generate our oAuth signature, make the request and parse the response.

1
2
3
4
5
6
7
8
9
method = 'GET'
uri = 'https://api.twitter.com/1/friends/ids.json'
params = params(consumer_key)
# Add the GET parameters here
params['cursor'] = '-1' # start at the beginning
params['user_id'] = user_id # from 'data' array above
params['oauth_signature'] = url_encode(sign(consumer_secret + '&' + auth_token_secret, signature_base_string(method, uri, params)))
uri += '?cursor=-1&user_id=' + user_id # Add in the GET parameters to the URL
followees = JSON.parse(request_data(header(params), uri, method))

Following a specific user (a POST request)

The process for a POST is pretty similar, the only difference being how you handle the parameters to the request. In the example below, I’m assuming you have the user ID of the person you want to follow and that it’s stored in a variable called followee_user_id.

1
2
3
4
5
6
method = 'POST'
uri = 'https://api.twitter.com/1/friendships/create.json'
params = params(consumer_key)
params['user_id'] = followee_user_id
params['oauth_signature'] = url_encode(sign(consumer_secret + '&' + auth_token_secret, signature_base_string(method, uri, params)))
resp = JSON.parse(request_data(header(params), uri, method, 'user_id='+followee_user_id))

So, assuming that was successful, the user should now be following the person with user ID followee_user_id.

Conclusion

Hopefully this will fill in some of the gaps in Twitter’s documentation. When coding this I found plenty of instances where the documentation would say something like “now sign the key”, without actually telling you how to sign it! Very confusing indeed.

Disclaimer: I obviously wrote all this in proper Ruby on Rails classes and controllers, but have extracted the code out here to be presented in modular form. Thus, what’s here is not fully tested, or even elegant, but there should be enough to figure out what you need to do.

Read more...

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:

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 string, as well as indicating you’re using oAuth:

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.

Read more...