7
votes

We have a rails app setup that uses devise & omniauth to allow logging in via facebook authentication. We also have a mobile app that is currently using http authentication to login to the rails app either by passing username & password or by passing in the http authentication token. This all works great so far.

The mobile app also has the ability to authenticate with facebook itself and receive the user facebook token directly between itself and facebook.

I would like to bridge this gap so that if the user has logged in from the mobile app via facebook and has their facebook token, allow that facebook token to be used as the authentication on the rails app as if they had received it from facebook via the browser.

The end result would be that the mobile app can log a user in via:

1) username/password or 2) http authentication token or 3) omniauth (facebook) token

Also, in the case of 3), if the user doesn't yet exist on the rails app, would need to create the user - doing that now already with browser side authentication so there may be nothing more to do.

How can I best accomplish this within the devise construct?

2

2 Answers

10
votes

Just did this, but never saw an end-to-end solution.

This addresses point 3. 1 & 2 are easily accomplished with devise and documented elsewhere.

Not too hard to add FB auth to your web app, the instructions are on github for omniauth and omniauth-facebook.

I believe the following stands alone, without doing the omniauth-facebook integration, if you want to do it that way. This is similar to other approaches out there. My idea was to attempt to use the devise model as closely as I could.

You'll need the fb_graph gem.

On the mobile client, you authenticate with FB appropriately and put the returned access token in the header of your http requests. I used the header fb_access_token. Just like basic auth, you'll want to send this over SSL to avoid sniffing of the token. Using the header allows me to interchange basic auth and FB auth without changing the requests, but you could use a parameter if you prefer.

This solution implements a warden strategy which is based off of the devise Authenticatable warden strategy. The difference here is the fact that this strategy utilizes an HTTP header called fb_access_token containing the facebook access token string that was retrieved using the mobile application.

Once you know this, the code is pretty straightforward.

In a file, in the config/initializers directory, add the following. I happened to call mine fb_api_strategy.rb:

# authentication strategy to support API authentication over the webservice
# via facebook

require 'devise/strategies/database_authenticatable'
require 'fb_graph'
require 'warden'

module Devise
  module Strategies
    class FbMobileDatabaseAuthenticatable < Authenticatable
  def valid?
    # if we have headers with the facebook access key
    !!request.headers["fb_access_token"]
  end

  def authenticate!
    token = request.headers["fb_access_token"]
    fbuser = FbGraph::User.me(token)
    fbuser = fbuser.fetch

    user = User.find_for_facebook_mobile_client(fbuser.email)

    # this either creates a new user for the valid FB account, or attaches
    # this session to an existing user that has the same email as the FB account

    if !!user && validate(user) { true }
      user.after_database_authentication
      success!(user)
    elsif !halted? || !user
      fail(:invalid)
        end
      end
    end
  end
end

Warden::Strategies.add(:fb_database_authenticatable,
                       Devise::Strategies::FbMobileDatabaseAuthenticatable)

In config/initializers, add the following to devise.rb:

  config.warden do |manager|
    manager.default_strategies(:scope => :user).unshift :fb_database_authenticatable
  end

To allow you to either create a user or find an existing user based on the FB email, add the following to your user model:

  def self.find_for_facebook_mobile_client(fb_email)
    if user = User.where(:email => fb_email).first
      user
    else
      User.create!(:email => fb_email, :password => Devise.friendly_token[0,20])
    end
  end

I don't think fb_database_authenticatable is an accurate name, but I'll leave that as an exercise for the reader. Another exercise is caching/storing the FB access token, and perhaps avoiding the RT to FB with each call. You should note that the access token from the the mobile app and the rails app will be different if you do FB authentication on both sides, which I suspect most people will want to do. This probably impacts your caching scheme.

I think that does it - happy coding.

0
votes

If you are setting request headers in xcode 4.3.2 you could use:

[request setValue:@"12345" forHTTPHeaderField:@"fbaccesstoken"];

but you cannot use:

[request setValue:@"12345" forHTTPHeaderField:@"fb_access_token"]; // does NOT get set