Twitter Gem Reborn with Fewer Features and 100% More OAuth

The Twitter gem is kind of my baby. It was pretty much the first gem I created and as such I care for it a little more. When I originally created it, I knew nothing. I sucked. Now over time, I suck less and Twitter’s addition of OAuth seemed like the perfect time to completely rewrite it. Yay for rewrites!

The gem is now leaner, meaner and works swell with OAuth. I would imagine as I use OAuth a bit more, I’ll figure out some ways to make it even easier in the twitter gem but it works for now.

The Cuts

One of the biggest headaches and coolest things about the Twitter gem was the command line interface. It allowed for multiple accounts and was SQLite database backed. That was cool and all but it is ridiculous to have main and active record as dependencies for someone who just wanted the API wrapping aspects of the gem.

Knowing this, I have finally killed the CLI. Fear not, CLI fans, it will make a triumphant return, but that return will be in a separate gem that merely relies on the Twitter gem. The Twitter gem itself will be only a wrapper for the REST and Search APIs that Twitter provides. No more extra cruft.

OAuth

As I said, the main reason for the rewrite was that Twitter now supports OAuth. I thought about it long and hard and decided to not continue support for HTTP Authentication. Twitter has not discontinued support for HTTP Authentication, but they will in the future and I didn’t want to add a bunch of hours to releasing the updated gem simply to support something that is going to be deprecated.

So how does one use the OAuth stuff in the Twitter gem? First off, familiarize yourself with OAuth. Then, you can check out the example below pulled straight from the examples directory included in the gem (or over here on github).

require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
require File.join(File.dirname(__FILE__), 'helpers', 'config_store')
require 'pp'

config = ConfigStore.new("#{ENV['HOME']}/.twitter")
oauth = Twitter::OAuth.new(config['token'], config['secret'])

if config['atoken'] && config['asecret']
  oauth.authorize_from_access(config['atoken'], config['asecret'])
  twitter = Twitter::Base.new(oauth)
  pp twitter.friends_timeline
  
elsif config['rtoken'] && config['rsecret']  
  oauth.authorize_from_request(config['rtoken'], config['rsecret'])
  twitter = Twitter::Base.new(oauth)
  pp twitter.friends_timeline
  
  config.update({
    'atoken'  => oauth.access_token.token,
    'asecret' => oauth.access_token.secret,
  }).delete('rtoken', 'rsecret')
else
  config.update({
    'rtoken'  => oauth.request_token.token,
    'rsecret' => oauth.request_token.secret,
  })
  
  # authorize in browser
  %x(open #{oauth.request_token.authorize_url})
end

The ConfigStore is a simple class that allows me to save the request and access token and secrets as I’m completing the handshakes between the script and Twitter.

As you can see, it is pretty simple. Get a request token, then use that to create an access token. From there, you can reuse the access token to authorize future requests.

An Example App

Now one might think, based on that previous example, “Thanks so much John! I really appreciate you getting the ball rolling for me with a simple example.” Friends do I have news for you. I didn’t stop there. I whipped together a little sample app, that I have been using for a few days now. Eventually, it will become a full Twitter client, but for now it is pretty basic and dirty.

It uses Clearance for authentication for authentication and automatically sends you to Twitter to authorize after firing it up (if you haven’t previously). I used Rails rescue_from to automatically send you to Twitter if any of the authorization stuff gets messed up at a later point to.

rescue_from Twitter::Unauthorized, :with => :twitter_unauthorized  
private
  def twitter_unauthorized(exception)
    redirect_to new_authorization_url
  end

Anytime the Twitter gem throws the Twitter::Unauthorized exception, you are redirected to new_authorization_url, which is powered by the AuthorizationsController, which looks like this.

class AuthorizationsController < ApplicationController
  def new
    oauth = current_user.oauth
    session['rtoken'] = oauth.request_token.token
    session['rsecret'] = oauth.request_token.secret
    redirect_to oauth.request_token.authorize_url
  end
  
  def show
    oauth = current_user.oauth
    oauth.authorize_from_request(session['rtoken'], session['rsecret'])
    
    session['rtoken'] = nil
    session['rsecret'] = nil
    
    current_user.update_attributes({
      :atoken => oauth.access_token.token, 
      :asecret => oauth.access_token.secret,
    })
    
    redirect_to root_path
  end
end

The new action stores the request token and secret and then redirects you to Twitter to authorize the app. Over at Twitter, I have registered an application and set the redirect url to go to /authorization in the app, which as you can see above, obtains the access token and secret and stores them with the current user.

You’ll probably notice that I am calling current_user.oauth and current_user.client throughout the app. These are helper methods I added to the user model to shorten up what was happening in the controller.

class User < ActiveRecord::Base
  include Clearance::App::Models::User
  
  attr_accessible :atoken, :asecret
  
  def authorized?
    !atoken.blank? && !asecret.blank?
  end
  
  def oauth
    @oauth ||= Twitter::OAuth.new(ConsumerConfig['token'], ConsumerConfig['secret'])
  end
  
  def client
    @client ||= begin
      oauth.authorize_from_access(atoken, asecret)
      Twitter::Base.new(oauth)
    end
  end
end

I could shorten up the controllers even more (and will at some point) by delegating several methods on user to user.client. I’ll post more on that later when I get around to it. Also, at some point, I’m going to remove the atoken and asecret from the user and allow support for multiple Twitter accounts for one user in the app. This should be pretty easy, but I wanted to get something working first before I complicated things.

Conclusion

From a user standpoint, OAuth is really nice. I love not having to put in my username and password. From a developer standpoint, OAuth is also pretty nice. Basically, you register your application with Twitter, provide the consumer token and key in a config file and this example app is ready to go.

As with any rewrite there are bound to be bugs. I have fixed one already that someone caught. If you catch any weird bugs using the rewritten Twitter gem, there is group and ticket information on the Twitter gem website.

17 Comments

  1. Awesome! I can’t wait to implement this in http://twitterless.com which is in dire need of some attention and OAuth. Thanks, John!

  2. Sweet, glad to see the oauth support!

    Is it too much trouble to maintain the HTTP auth code as well? I’m working on an XMPP microblogging interface and liked that I can use the same library to support Twitter and Laconica, but now it looks like I’ll have to implement my own Laconica support since it doesn’t yet support oauth. If it’s too complicated to maintain both authentication types then by all means drop HTTP auth, but if there is any way to continue supporting Laconica’s mostly compatible API then that’d rock. Either way, thanks for a cool library.

  3. @Mark Coolio. Let me know if you run into problems.

    @Nolan I thought about it a bit and it probably wouldn’t be more than an hour or two of hacking but I make no promises. :) Basically, I would need to implement Twitter::HTTPAuth and then allow Twitter::Base to take an instance of that or of Twitter::OAuth.

  4. Added HTTP Authentication back in for those that want to use it until it is deprecated by Twitter. Only took an hour or two.

  5. Hey John, the move over to the new gem has been smooth so far but I ran into one issue. It seems Twitter has changed their api so that GET methods can no longer be sent as POST methods (verify_credentials is a GET method). I’m getting the “Woah There!” page is not valid screen when trying to authorize accounts, so I thought I’d pass that along to you. Not sure if it’s something that needs to be changed in your gem or not, or if it’s something I need to do when using it.

  6. Hey, update: I found out my issue was due to a mistake on my end, unrelated to the API changes. Everything seems to be working fine. I’ll let you know if I run into any other actual issues.

  7. Stephen Hood Stephen Hood

    Apr 23, 2009

    Does anyone know if/how yesterday’s revelation regarding the OAuth exploit affect this gem? Will it need to be updated?

  8. I can’t imagine that it would. I believe the session fixation security issue is more of a provider issue than consumer, meaning it affects twitter, not clients consuming twitter’s oauth.

    I just did a quick checked and the oauth part seems to be working fine.

  9. Stephen Hood Stephen Hood

    Apr 23, 2009

    Makes sense, thanks. And thanks for the useful resource!

  10. Thanks, John, for your work on this. I am trying to implement the sample app you’ve set up here and it worked once without a hitch this morning and now I am getting an error on the show action:

    OAuth::Unauthorized (401 Unauthorized):

    oauth (0.3.2) lib/oauth/consumer.rb:166:in `token_request’
    oauth (0.3.2) lib/oauth/tokens/request_token.rb:14:in `get_access_token’
    twitter (0.6.8) lib/twitter/oauth.rb:22:in `authorize_from_request’

    Have you ever seen this before? Is it possible that twitter changed their API in the intervening hours?

    Thanks!

  11. Ah! Nevermind, I figured it out. The problem was with the version of the oauth gem on production. Thanks, John.

  12. Dylan, what exactly did you fix? I’m getting this…

    OAuth::Unauthorized (401 Unauthorized):
    oauth (0.3.2) lib/oauth/consumer.rb:166:in `token_request’
    oauth (0.3.2) lib/oauth/consumer.rb:104:in `get_request_token’
    /usr/lib/ruby/gems/1.8/gems/twitter-auth-0.1.17/app/controllers/sessions_controller.rb:4:in `new’

    But only on my home machine, not my work box.

  13. Everything is working except that I can’t figure out what to use for the “Callback URL:” on the twitter oauth site. I’m running the app from my local machine but twitter doesn’t accept http://localhost:3000. Any suggestions?

  14. @Chris – Yeah, I had the same problem. Just setup a vhost on your local machine such as twitter.local. Twitter will accept URLs like that. I started using the passenger prefpane which makes that really easy to do.

  15. Thanks John. I got it working using bit.ly to trick out the Twitter API. I’ll also check out passenger prefane. You’ve done an awesome job on the Twitter Gem.

  16. I’m trying to get the Clearance Cucumber scenarios working with twitter-app and the sign in step fails with “(401): Unauthorized – (Twitter::Unauthorized)”. I understand that the twitter app-depends on Rails “rescue_from Twitter::Unauthorized” to send get redirected to new_authorization_url. Any ideas how to get Cucumber to rescue from the Twitter::Unauthorized error and continue stepping through the scenario?

  17. @Chris You’ll have to stub out the Twitter gem stuff. In my example twitter app there is an example of how to do this with mocha that should get you going.

    If you have more questions, it might be easier to handle on the mailing list .

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.