Friday, May 7, 2010

Facebook OAuth on Rails..

So last night I took on trying to integrate Facebook's new OAuth authorization in Ruby On Rails, and in this post I am going to try and give everyone some idea of how to do this.

In the past I have use facebooker to integrate with facebook, so I already had a facebook.yml setup with all the configuration information for my facebook app:



development:
application_id: XXXXXXXXXXXX
api_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
canvas_page_name: blah
callback_url: http://localhost:3000/
pretty_errors: true
set_asset_host_to_callback_url: false
tunnel:
public_host_username:
public_host:
public_port: 4007
local_port: 3000
server_alive_interval: 0

The only real difference between this and the normal facebooker file is the addition of the application_id, which is now used in the OAuth authentication.

The first step is to add an endpoint to start the first leg of the OAuth.

def new_facebook
facebook_settings = YAML::load(File.open("#{RAILS_ROOT}/config/facebooker.yml"))
redirect_to "https://graph.facebook.com/oauth/authorize?client_id=#{facebook_settings[RAILS_ENV]['application_id']}&redirect_uri=#{APP_URL}/facebook_credentials/facebook_oauth_callback&scope=offline_access,publish_stream"
end

This will redirect you to Facebook, it sets the "client_id" to the "application_id" mentioned above. This particular redirect also asks Facebook to ask your user to grant some special permissions, namely "offline_access", and "publish_stream" (allow me to access my users accounts anytime, and let me publish to their streams), there are many other permission detailed here. Also note that we provided a "redirect_url" to Facebook which Facebook will redirect to once the user grants us access to their Facebook account. So now we need to create an endpoint for the "redirect_url"



def facebook_oauth_callback
if not params[:code].nil?
facebook_settings = YAML::load(File.open("#{RAILS_ROOT}/config/facebooker.yml"))
callback = "#{APP_URL}/facebook_credentials/facebook_oauth_callback"
url = URI.parse("https://graph.facebook.com/oauth/access_token?client_id=#{facebook_settings[RAILS_ENV]['application_id']}&redirect_uri=#{callback}&client_secret=#{facebook_settings[RAILS_ENV]['secret_key']}&code=#{CGI::escape(params[:code])}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
tmp_url = url.path+"?"+url.query
request = Net::HTTP::Get.new(tmp_url)
response = http.request(request)
data = response.body
access_token = data.split("=")[1]
url = URI.parse("https://graph.facebook.com/me?access_token=#{CGI::escape(access_token)}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
tmp_url = url.path+"?"+url.query
request = Net::HTTP::Get.new(tmp_url)
response = http.request(request)
user_data = response.body
user_data_obj = JSON.parse(user_data)
flash[:notice] = 'Facebook successfully connected.'
@social_credential = SocialCredential.create_or_find_new_facebook_cred(access_token, session['rsecret'])
end
end
Here we are doing a lot of things. First we make sure that Facebook returns us the "code" parameter, we need this to complete the authentication. Once we have the code, we then need to request an "access_token" from Facebook. This was tricky for me, because the url is an SSL/HTTPS url and normal httparty or net/http tricks didn't work. Eventually I got it worked out as can be seen in the code above. Once we get the "access_token" we are pretty much done, in this case I go ahead and get the current user's Facebook data by hitting "https://graph.facebook.com/me?access_token=XXXXXXXXXXXXX" and then populate an ActiveRecord object with the data I receive (including the "access_token") for later use.

Keeping the "access_token" is important, because in my case it lets me update the users stream whenever I need to (hence the "offline_access" in the initial request).

I know from my Twitter stream that there is work on wrapping the Facebook OpenGraph API in ruby but I don't know where it stands. It is a pretty simple api, and won't require much, but it would be nice to have it abstracted some.

Okay, cheers and jeers in the comments please.

12 comments:

  1. One question if I may - is there a way to ask user to authenticate each time when I need them authorized?

    ReplyDelete
  2. it doesnt work for me. i get this as the response:

    response = http.request(request)
    (rdb:7) n
    warning: peer certificate won't be verified in this SSL session
    data = response.body
    (rdb:7) n
    access_token = data.split("=")[1]
    (rdb:7) data
    "{\"id\":\"https:\\/\\/graph.facebook.com\\/oauth\\/access_token\"}"

    so the data object doesnt have anything at index 1.

    ReplyDelete
  3. @Sam, I think you can check to see if the session is still any good by trying to get the person's feed (and seeing if you get a return, there maybe better endpoints to hit to check to see if the session is still good). If you get a good return (meaning good data), then you don't need to reauth.@Sam, I think you can check to see if the session is still any good by trying to get the person's feed (and seeing if you get a return, there maybe better endpoints to hit to check to see if the session is still good). If you get a good return (meaning good data), then you don't need to reauth.

    ReplyDelete
  4. Guessing that the output format has changed, I'd try data.split("=")[0] or data.split("=")[2]...

    ReplyDelete
  5. "In the past I have use facebooker to integrate with facebooker" - I guess you mean "with facebook" :-)

    ReplyDelete
  6. Indeed I did, updating the post now.

    ReplyDelete
  7. BTW while I am on this post, @Gladiator I am not sure if Facebook was just down, but the code as is does work, I verified it last night.

    ReplyDelete
  8. just getting my head around RoR - may seem silly, but i've put this in a 'services' controller (i am storing multiple social network tokens per person- so the toke will be associated with a user id in the services model) - but can't seem to call it - sure i'm being stupid, but any help for a beginner would be greatly appreciated!

    ReplyDelete
  9. Hi Jamie, 
    thanks a lot for posting this. 

    I don't understand why but what I get for "data = response.body" is 
    {   "error": {      "type": "OAuthException",      "message": "Error validating verification code."   }}

    I don't understand what happens. 
    when I just try the url in my browser with all the parameters until the access code, I can get the access token, but not this way. 

    on the other hand, when I try the same url with a browser with which I'm logged in into Facebook with a different user [but I reuse the code I got for the previous user], then I get the exact same error ["Error validating verification code."]. So I wonder if there's an issue with regards to the server 'not being logged in as the user who got the code'

    I tried hard to make this work and couldn't figure it out. Any help would be super appreciated

    Thanks!

    Pierre

    this is the code I use in my controller [after I logged on the client side - which successfully gives my controller a code]

    ------------------------------------------------------------------------------------------------
     @access_code = params[:code]
     url = URI.parse("https://graph.facebook.com/oauth/access_token?client_id=MYCLIENTID&redirect_uri=MYREDIRECTURI&client_secret=MYAPPSECRET&code=#{CGI::escape(@access_code)}")       http = Net::HTTP.new(url.host, url.port)       http.use_ssl = (url.scheme == 'https')       tmp_url = url.path+"?"+url.query       request = Net::HTTP::Get.new(tmp_url)       response = http.request(request)       data = response.body       @access_token = data.split("=")[1]       @test = data

    ------------------------------------------------------------------------------------------------

    ReplyDelete