Consuming OAuth intelligently in Rails
It has been fairly easy to provide OAuth services in your web application (see How to turn your Rails Site into an OAuth Provider), but to actually consume Twitter, FireEagle, my own Agree2 and other OAuth services has been a fairly manual affair.
There are great gems out there that wrap up the process for the above mentioned services. So it hasn’t been too hard to support one of them. But what to do if you want to support 5 different services today and more in the future?
I knew there should be some generic approach to handle the OAuth authorization process, but had not spent too much time thinking about it until we actually needed to consume external web services for Agree2.
Well I think I’ve cracked it in a a nice Dont Repeat Yourself fashion.
OAuth Consumer Generator
I have now added a oauth_consumer generator to the Rails OAuth Plugin.
The oauth_consumer generator creates a controller to manage the authentication flow between your application and any number of external OAuth secured applications that you wish to connect to.
To run it simply run:
./script/generate oauth_consumerThis generates the OauthConsumerController as well as the ConsumerToken model.
OAuth Authentication Flow
For each service you support the consumer presents the following 2 urls:
- http://yoursite.com/oauth_consumers/twitter
- http://yoursite.com/oauth_consumers/twitter/callback
To connect to a users twitter account just add a link somewhere to go to the first URL and the plugin takes care of the rest. The callback is used internally and you don’t really need to worry about it.
If you send the user back the first URL he/she will have the option of removing or reconnecting. A http DELETE at that URL will remove an existing token.
Configuring services
You can configure any amount of services to support by editing:
All configuration of applications is done in
config/initializers/oauth_consumers.rbAdd entries to OAUTH_CREDENTIALS for all OAuth Applications you wish to connect to. Get this information by registering your application at the particular applications developer page. I decided to use Ruby rather than yaml here so heroku users can Config Values here.
OAUTH_CREDENTIALS={
:twitter=>{
:key=>"key",
:secret=>"secret"
},
:fireeagle=>{
:key=>"key",
:secret=>"ssh"
},
:agree2=>{
:key=>"key",
:secret=>"secret"
},
:hour_feed=>{
:key=>"bb",
:secret=>"aa",
:options={
:site=>"http://hourfeed.com"
}
}
}For generic OAuth services you need to add key, secret and an options hash with information of which URLs to use. See OAuth::Consumer for valid values. For services with wrapper classess (I’ll get into those shortly) you only need to specify key and secret as a wrapper class handles the rest.
For every service you add here you get the before mentioned 2 urls. So to link to Agree2 in the above example have a user follow a link to http://yoursite.com/oauth_consumers/agree2.
Using the OAuth Services
Every time a user is linked to one of these services it creates a new ConsumerToken. It actually uses STI and I dynamically create a new Subclass of ConsumerToken for each service listed in OAUTH_CREDENTIALS.
Using the above example you get 3 new classes:
- TwitterToken
- FireeagleToken
- Agree2Token
- HourFeedToken
This makes it easy to use from your user object. Add the following in your user model:
has_one :twitter_token,:class_name=>"TwitterToken", :dependent=>:destroy
has_one :fire_eagle,:class_name=>"FireEagleToken", :dependent=>:destroy
has_one :agree2,:class_name=>"Agree2Token", :dependent=>:destroy
has_one :hour_feed,:class_name=>"HourFeedToken", :dependent=>:destroyNow you can access each of these directly in your own code. Each of these tokens implement a client method which will allow you to do stuff on the web service
current_user.hour_feed.client.get("/report")By default this is an instance of OAuth::AccessToken but you can write wrapper classes to use a higher level library.
Wrapper Tokens
Wrapper tokens are classes written using to create a high level approach to working with a service such as Twitter. The plugin comes with wrapper classes for Twitter, Agree2 and FireEagle but it is fairly easy to write your own.
Here is an example using Twitter. It uses the Twitter Gem to provide a high level api:
current_user.twitter.client.update("Just had fantastic Jerk Pork on Boston Beach. Need beer!!")This example uses FireEagle. It uses the Fireagle Gem to provide a high level api:
@location=current_user.fireeagle.locationCreating your own wrapper tokens
If you have a service in CONSUMER_CREDENTIALS called :my_service just add a class to your models folder called MyServiceToken and it will be used instead of the auto generated one. In this example we are imagining a gem called ‘myservice’ that wraps around the web services actual api.
# Use a gem to do the heavy stuff
require 'myservice'
class MyServiceToken < ConsumerToken
MY_SERVICE_SETTINGS={:site=>"http://my_service.com"}
# override self.consumer so we don't have to specify url's in options
def self.consumer
@consumer||=OAuth::Consumer.new credentials[:key],credentials[:secret],MY_SERVICE_SETTINGS
end
# overide client to return a high level client object
def client
@client||=MyService::Client.new( MyServiceToken.consumer.key,MyServiceToken.consumer.secret, token, secret)
end
# optionally add various often used methods here
def shout(message)
client.shout(message)
end
endIf there isn’t a wrapper gem for the service you want to use. You can always add methods to MyServiceToken to do the parsing etc.
I hope this will get you started implementing OAuth. There no longer is an excuse not to.
Do you need advise or help with implementing OAuth in your Rails application?
I am available as a consultant and would be glad to help your company out. Whether you need help in developing an OAuth strategy or in practical hands on implementation help. Send me an email at pelle@stakeventures.com to let me know what you need help with and I can give you an estimate and my rate.
Follow me on Twitter
So weird you posted this literally 16 minutes before I was looking for how to do it, but I noticed a dependency issue- oauth-plugin uses oauth 3.5 and twitter uses 3.4; how would you resolve this/ I’m pretty new with ruby :/
Hi Ryan,
You should install Paul Singh’s version of the Twitter gem. He just bumped the OAuth version number and there aren’t any other changes.
sudo gem install paulsingh-twitter —source http://gems.github.com
Thanks a bunch!
Hi,
This tutorial is great. Thanks.
One thing that took me a few hours to figure out, though: you wrote “If you have a service in CONSUMER_CREDENTIALS called :my_service just add a class to your models folder called MyServiceToken and it will be used instead of the auto generated one.”
But it doesn’t work—the oauth plugin never looks for a subclass in the models folder.
I’ve just created a fork at http://github.com/BMorearty/oauth-plugin that fixes this problem. I’ve sent you a pull request.
Thanks for the great gem.
Thanks Brian,
I was just hit by this myself and was going to start investigating it.
I’ll have a look at your fix.
P
Just released 0.3.14 which includes Brians fix.
Is there somewhere else I can view the tutorial? The continue reading link just links back to this abbreviated version.
@pelle Disregard my last comment. I checked backa few minutes later to see if you’d responded and everything was fine. That’s what I get for using Google Chrome for OS X.
Hi @pelle, I really need your help with a roadblock…. in my app, user accounts are represented by a model called Account (and all related methods does the same).
How can I make oauth-plugin to handle that instead of User ?
Thanks in advance and congrats for such useful post and plugin.
Thanx for that great resources (gem and articles).
I would be interested, how you configure the oauth consumer to be able to login via one of the consumers e.g. with twitter.
in your approach you need a current_user to have access to the oauth_consumer_token.
thanx in advance
phil
I’m trying to implement an OAuth consumer and after following the instructions above, I keep getting this error message:
NoMethodError in Oauth consumersController#show
undefined method `login_required’
Any ideas on what could be going wrong? I googled around and saw people running into the issue when creating OAuth providers, but couldn’t find similar cases/fixes for an OAuth consumer.
I’m also getting the undefined method ‘login_required’ problem.