a little sinatra oauth ditty

I have a toy Ruby webapp I created using Sinatra that I have hosted on Heroku.  It’s a small little ditty, nothing fancy.  It’s called “Tweet Tools” and I wrote it to make it easier to read tweets from a particular class of Twitter user who don’t seem to understand the point of Twitter: these people insist on making dozens upon dozens of rapid fire and related comments in a row.  Because Twitter and most Twitter clients display the most recent tweets first, their stories or observations or rants or whatever end up reading backwards.

It only took a few hours to write, and I did it a year or so ago and hadn’t messed with it until recently, when it suddenly stopped working.  This is because Twitter went and decided HTTP Basic Auth wasn’t good enough, and now we all have to upgrade to OAuth.  NO problem.  Here’s how you do it with your Ruby webapps in only three simple steps:

1) Go to http://www.twitter.com/apps/ and register your application.  You’ll get a nice little consumer key and a secret.  It really is a secret.  Don’t tell anyone, especially your nosy neighbor.

2) Install the Ruby gem oauth. Unless you want to do everything yourself. In which case, go for broke, but don’t ask me for help. I would rather get real work done.

3) Before every HTTP request, we need to do a few things.

First, we do some basic setup. We clear an area in the session where we intend to hold the OAuth tokens we get, and then construct a Consumer object that identifies who we are using the consumer_key and consumer_secret Twitter gave us, and it identifies Twitter as our OAuth provider.

before do
session[:oauth] ||= {} # we'll store the request and access tokens here
host = request.host
host << ":4567" if request.host == "localhost"
consumer_key = CONSUMER_KEY # what twitter.com/apps says
consumer_secret = CONSUMER_SECRET # shhhh, its a secret
@consumer = OAuth::Consumer.new(consumer_key, consumer_secret, :site => "http://twitter.com")

OK, now we have our Consumer object. That represents our webapp, the OAuth consumer. The ‘host’ variable will come in handy later when we tell Twitter where to call back to. When running Sinatra on my localhost, it uses port :4567 so we append that to the hostname. That way, when we approve Tweet Tools’ permission to use my Twitter account (for browsing other people’s timelines) we’ll get redirected right back to the very same Internet Explorer window I started off in.

Now we need to get a request token. If we asked for one before, we store it in the session so we don’t need to ask for it again.

We will later exchange the request token for an access token by redirecting the user to Twitter and letting them approve our webapp.

  # generate a request token for this user session if we haven't already
request_token = session[:oauth][:request_token]
request_token_secret = session[:oauth][:request_token_secret]
if request_token.nil? || request_token_secret.nil?
# new user? create a request token and stick it in their session
@request_token = @consumer.get_request_token(:oauth_callback => "http://#{host}/auth")
session[:oauth][:request_token] = @request_token.token
session[:oauth][:request_token_secret] = @request_token.secret
else
# we made this user's request token before, so recreate the object
@request_token = OAuth::RequestToken.new(@consumer, request_token, request_token_secret)
end

Awesome. But we might already have an access token, if the user had already logged in and been authenticated by Twitter, so let’s check for that. If we do, we can create our OAuth client object and start perusing timelines.

Astute readers no doubt realize we don’t need to check the request token if we already have an access token. But I’ll leave those optimizations as an exercise for the reader. Mostly because I’m lazy.

  # this is what we came here for...   
access_token = session[:oauth][:access_token]
access_token_secret = session[:oauth][:access_token_secret]
unless access_token.nil? || access_token_secret.nil?
# the ultimate goal is to get here, where we can create our Twitter @client
# object
@access_token = OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
oauth = Twitter::OAuth.new(consumer_key, consumer_secret)
oauth.authorize_from_access(@access_token.token, @access_token.secret)
@client = Twitter::Base.new(oauth)
end
end

OK, now how do we exchange our request token for the access token in the first place? Well, by asking Twitter nicely of course. Set up a route that redirects to the authorization URL. We can get that from the request token.

<a href="/request"><img src="/images/twitter_sign_in.png" />

get "/request" do
redirect @request_token.authorize_url
end

After the user is redirected to Twitter they will see this lovely screen.

Assuming they press “ALLOW”, they will come back to the URL you specified in the oauth_callback earlier.

# remember this code from earlier?  This is why we're here:
@request_token = @consumer.get_request_token(:oauth_callback => "http://#{host}/auth")

get "/auth" do
@access_token = @request_token.get_access_token(:oauth_verifier => params[:oauth_verifier])
session[:oauth][:access_token] = @access_token.token
session[:oauth][:access_token_secret] = @access_token.secret
redirect "/"
end

Hooray! Now we can be all up in the user’s tweets, checking their timelines and whatnot.

@client.user_timeline(:id=>@userid, :count=>35).each do |status|
puts "that's right I be all up in your timeline checking your #{status} and whatnot"
end

If you want to block users from accessing certain parts of the system unless they are logged on, you can simply redirect them to the front page or login page by simply checking for the presence of the @access_token.

redirect '/' unless @access_token

At this point in your webapp, you could implement SSO. When you get redirected to your Twitter landing page (/auth in my case), you could query the Twitter API to find the user’s login and check that against a column on your user table. If you find them, you automatically log them into their account. If no such Twitter user exists, you would redirect them to a page where they are asked to link it to their existing account or register for a new one (it’s easy and only takes… where’d they go?) Or whatever. I don’t have time to walk you through all that right now though, as I have some tweets I need to go read. (Backwards.)