April 05, 2009
Older: Crack, The Easiest Way to Parse XML and JSON
Newer: How to Add Simple Permissions into Your Simple App. Also, Thoughtbot Rules!
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
Apr 05, 2009
Awesome! I can’t wait to implement this in http://twitterless.com which is in dire need of some attention and OAuth. Thanks, John!
Apr 05, 2009
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.
Apr 05, 2009
@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.
Apr 11, 2009
Added HTTP Authentication back in for those that want to use it until it is deprecated by Twitter. Only took an hour or two.
Apr 14, 2009
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.
Apr 15, 2009
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.
Apr 23, 2009
Does anyone know if/how yesterday’s revelation regarding the OAuth exploit affect this gem? Will it need to be updated?
Apr 23, 2009
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.
Apr 23, 2009
Makes sense, thanks. And thanks for the useful resource!
Apr 26, 2009
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!
Apr 26, 2009
Ah! Nevermind, I figured it out. The problem was with the version of the oauth gem on production. Thanks, John.
Apr 29, 2009
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.
May 02, 2009
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?
May 02, 2009
@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.
May 02, 2009
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.
May 03, 2009
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?
May 03, 2009
@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.