Consuming OAuth intelligently in Rails

Posted by Pelle July 21st, 2009 22 comments edit

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_consumer

This 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.rb

Add 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=>:destroy

Now 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.location

Creating 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
end

If 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 [email protected] to let me know what you need help with and I can give you an estimate and my rate.

Posted July 21st, 2009 under:
Comments
ryancacophony@gmail.com
Ryan Clough July 22nd, 2009 destroy

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 :/

pelle@stakeventures.com
Pelle Braendgaard July 22nd, 2009 destroy

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

ryancacophony@gmail.com
Ryan Clough July 23rd, 2009 destroy

Thanks a bunch!

brian@morearty.org
Brian Morearty October 8th, 2009 destroy

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.

pelle@stakeventures.com
Pelle Braendgaard October 8th, 2009 destroy

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

pelle@stakeventures.com
Pelle Braendgaard October 8th, 2009 destroy

Just released 0.3.14 which includes Brians fix.

britt.v.crawford@gmail.com
Britt October 25th, 2009 destroy

Is there somewhere else I can view the tutorial? The continue reading link just links back to this abbreviated version.

britt.v.crawford@gmail.com
Britt October 25th, 2009 destroy

@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.

manoel@lemos.net
Manoel Lemos November 27th, 2009 destroy

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.

philsonite@googlemail.com
phil December 15th, 2009 destroy

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

greg August 22nd, 2010 destroy

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.

topper@toppingdesign.com
Topper Bowers August 27th, 2010 destroy

I’m also getting the undefined method ‘login_required’ problem.

maik@mediavrog.net
mediavrog September 21st, 2010 destroy

Hi Pelle,

thanks for your great work bringing oAuth to Rails in a convenient fashion. There’s one thing i’m not quite sure about i want to ask you: Will this plugin support oAuth 2.0? I know, all the basic features are in your oAuth Gem (including support for oAuth 2.0) and i think of oAuth-Plugin to be a convenience wrapper for even easier handling – is this right?

Thanks in advance for your help.

Kind Reards,
Maik

stephan@eisler.de
Stephan Eisler November 29th, 2010 destroy

Is there a difference between development and production mode?

http://localhost:3000/oauth/request_token

works perfect, but

http://waldstat.com/oauth/request_token

don’t works with the same client code. ( https://github.com/Eisler/WaldstatOauth-Java-Example )

production.log:
Processing OauthController#request_token (for 79.220.200.220 at 2010-11-28 11:17:13) [POST]
Filter chain halted as [#<OAuth::Controllers::ApplicationControllerMethods::Filter:0×7fd37fc67f50 @options={:interactive=>false, :strategies=>:two_legged}, @strategies=[:two_legged]>] rendered_or_redirected.
Completed in 1ms (View: 0, DB: 0) | 401 Unauthorized [http://waldstat.com/oauth/request_token]

rog_21@yahoo.com
Roger February 14th, 2011 destroy

I am running into this issue:

undefined method `callback_oauth_consumer_url’ for #

Any ideas?

aidan.feldman@gmail.com
Aidan Feldman March 1st, 2011 destroy

@Roger You are probably missing the route… add this to config/routes/rb:

resources :oauth_consumers do
  get :callback, :on => :member
end
lists@josf.se
Josef Ottosson March 8th, 2011 destroy

I seem to have issues with the service token not being available for some reason (Although it’s probably in the noob category, being very new to RoR). From I http://0.0.0.0:3001/oauth_consumers/twitter I get

NameError (uninitialized constant TwitterToken):
/var/lib/gems/1.8/gems/oauth-plugin-0.3.14/lib/oauth/controllers/consumer_controller.rb:70:in `load_consumer’

Is the class not created on the fly as described? Or am I losing it somewhere along the way?

lists@josf.se
Josef Ottosson March 8th, 2011 destroy

Fixed it, the solution seems to have been to install portablecontacts.

rockycaam@gmail.com
Rocky March 25th, 2011 destroy

I’m trying to implement an OAuth consumer but I keep getting this error message:

NoMethodError in Oauth consumersController#show

undefined method `new__session_path’ for #

Any ideas on what could be going wrong?

simon@polkaspots.com
Simon M May 2nd, 2011 destroy

Hi,

I have this working OK although I’m getting the following error:

uninitialized constant Twitter::OAuth

Is there something I can do to sort this – have looked all over for the solution..

Simon

mkins45@yahoo.com
Joyeria Contemporanea July 20th, 2011 destroy

The solution seems to have been to install portablecontacts!!

daniel@danielz.nl
DanielZ July 25th, 2011 destroy

I followed your github readme. Created an provider app and a consumer app. but i cant get it to work. When i try to connect to the provider from the consumer app i get a 401 error. I really dont know how to fix this any tips?