metautonomo.us

Confounding URL typists since 2007.

An Alternative to the Facebook Registration Tool

Posted by Ernie on December 21, 2010 at 2:10 pm

With the of the Facebook registration tool, there’s no doubt that you, the intrepid Rails developer, will be looking into it as a possible means of new user signup, either by choice or by client request. My preference has always been to limit an app’s dependence on Facebook as much as possible. I guess it’s a holdover from my hardcore OSS zealot days, or something — though I never had a hacker beard, I can’t help feeling there’s something inherently evil about Facebook (apart from their love affair with PHP ;)). Anyway, I recently implemented something very much like this feature using Devise and OmniAuth, so I thought I’d share.

Is this for me?

Before we get started, a quick pro/con list, because everyone loves pro/con lists.

Registration Tool
Pros:

  • Facebook look and feel
  • Pre-filled responses from FB profile
  • One click
  • Built-in type-ahead field support

Cons:

  • Facebook look and feel
  • Facebook server dependence (unless you code a custom fallback form)
  • (Related to above) slower loading of registration form
  • All registration data (even non-FB prefills) pass through FB servers
  • Limited input types for custom fields
  • Rigid presentation options for custom fields

The alternative
Pros:

  • Complete control over look and feel
  • Pre-filled responses from FB profile
  • Pre-filled responses from any other OmniAuth-supported source
  • Faster load time (for non-FB signups)
  • Don’t feed non-FB info to the Zuck

Cons:

  • Two clicks

Laying the groundwork

So, for the sake of space, we’re going to assume you’ve got some basic understanding of how to set up Devise in your application. If you don’t, the relatively terse instructions that follow may not make much sense.

At the time of this writing, you’ll need the current master branch of Devise from GitHub. Here are the relevant Gemfile lines:

gem "oa-oauth", :require => "omniauth/oauth"
gem 'devise', :git => 'git://github.com/plataformatec/devise.git'

In your user model, you’ll want something like this — note the :omniauthable addition:

  devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable

In your config/initializers/devise.rb, you’ll want the following (you can drop publish_stream if you don’t intend to publish on a user’s behalf):

  config.omniauth :facebook, APP_CONFIG['fb_app_id'],
                             APP_CONFIG['fb_app_secret'],
                  :scope => 'publish_stream,email,offline_access'

And lastly, to lay the groundwork for the various overrides we’ll be doing, you’ll want something like this in your routes.rb:

  devise_for :user, :controllers => {
    :registrations => 'registrations',
    :sessions => 'sessions',
    :omniauth_callbacks => "users/omniauth_callbacks"
  }

Don’t forget — when overriding devise controllers you’ll need to copy the generated views from app/views/devise/* to app/views/*.

The “hard” part

Now, let’s go ahead and get into the custom code required to make this all work.

For starters, we’re going to need a place to store various OmniAuth authorizations. Let’s add an Authorization model.

$ rails g model Authorization user:references provider:string uid:string \
  nickname:string url:string credentials:text
$ rake db:migrate

app/models/authorization.rb

class Authorization < ActiveRecord::Base
  belongs_to :user
  serialize :credentials
 
  validates_uniqueness_of :uid, :scope => :provider
 
  # Maybe we'll cover twitter in another post?
  scope :twitter, where(:provider => 'twitter')
  scope :facebook, where(:provider => 'facebook')
end

We’ll also need to add a few things to our User model:

app/models/user.rb

  [...]
  has_many :authorizations, :dependent => :destroy
  [...]
 
 class << self
    def new_with_session(params, session)
      super.tap do |user|
        if data = session['devise.omniauth_info']
          user.name = data[:name] if user.name.blank?
          user.email = data[:email] if user.email.blank?
          user.gender = data[:gender] if user.gender.blank?
        end
      end
    end
  end
 
  def set_token_from_hash(hash)
    token = self.authorizations.find_or_initialize_by_provider(hash[:provider])
    token.update_attributes(
      :uid         => hash[:uid],
      :nickname    => hash[:nickname],
      :url         => hash[:url],
      :credentials => hash[:credentials]
    )
  end

We’ll be using the two methods we added to User in the next steps. Well, to be honest, Devise uses User.new_with_session on its own. By defining our own new_with_session method, we’re able to pull in the “autofill” values for the new user on our form after we request permissions from Facebook.

Let’s get our OmniAuthCallbacksController set up:

app/controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  before_filter :set_auth
 
  def facebook
 
    if current_user
      if current_user.set_token_from_hash(facebook_authorization_hash)
        flash[:notice] = "Authentication successful"
      else
        flash[:alert] = "This Facebook account is already attached to a user account!"
      end
      redirect_to edit_user_registration_path + '#tabs-3'
    else
 
      authorization = Authorization.find_by_provider_and_uid(['provider'], ['uid'])
 
      if authorization
        authorization.user.set_token_from_hash(facebook_authorization_hash)
        flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => ['provider']
        sign_in_and_redirect(:user, authorization.user)
      else
        session['devise.omniauth_info'] = facebook_authorization_hash.merge(facebook_user_hash)
        redirect_to new_user_registration_url
      end
    end
  end
 
  private
 
  def facebook_authorization_hash
    {
      :provider    => ['provider'],
      :uid         => ['uid'],
      :nickname    => ['user_info']['nickname'],
      :url         => ['user_info']['urls']['Facebook'],
      :credentials => ['credentials']
    }
  end
 
  def facebook_user_hash
    {
      :name        => ['user_info']['name'],
      :email       => ['extra']['user_hash']['email'],
      :gender      => case ['extra']['user_hash']['gender']
        when 'male'
          'M'
        when 'female'
          'F'
        else
          'N'
        end
    }
  end
 
  def set_auth
     = env['omniauth.auth']
  end
end

You can probably see at this point how simply additional services can be added, mapping relevant fields from the OmniAuth hash into our User attributes.

Now, let’s use this data in our custom RegistrationsController — this is Devise customization 101, so not going to go into details here:

app/controllers/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController
 
  def create
    build_resource # Here's where the autofill magic happens
 
    if resource.save
      resource.set_token_from_hash(session['devise.omniauth_info']) if session['devise.omniauth_info'].present?
      if resource.active?
        set_flash_message :notice, :signed_up
        sign_in_and_redirect(resource_name, resource)
      else
        set_flash_message :notice, :inactive_signed_up, :reason => resource.inactive_message.to_s
        expire_session_data_after_sign_in!
        redirect_to after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords(resource)
      render_with_scope :new
    end
  end
end

That’s it! When build_resource is called in the above code, Devise calls User.new_with_session and we pre-fill the fields with the info from OmniAuth.

All that remains is to add a Facebook signup link in our view:

<%= link_to "Sign Up with Facebook", user_omniauth_authorize_path(:facebook) %>

Filed under Blog
Tagged as , , omniauth, ,
You can leave a comment, or trackback from your own site.