Confounding URL typists since 2007.
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.
Before we get started, a quick pro/con list, because everyone loves pro/con lists.
Registration Tool
Pros:
Cons:
The alternative
Pros:
Cons:
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/*.
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) %>