Last Updated: February 25, 2016
·
2.11K
· rebyn

Rails authentication using Facebook

Note: This is an updated post based on the awesome one by RailsRumble and Intridea.

Enter OmniAuth

The first step is to add OmniAuth and the oauth strategy you want (in this case, Facebook) to your Gemfile:

# Gemfile
gem 'omniauth'
gem 'omniauth-facebook'

Now we need to create an initializer to make use of the OmniAuth middleware:

# config/intitializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, 'APP_ID', 'APP_SECRET'
end

To separate staging and production logins (also debug), you can list out different credentials for these two environments:

# config/intitializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  if Rails.env.development?
    provider :facebook, 'STAGING_APP_ID', 'STAGING_APP_SECRET'
  elsif Rails.env.production?
    provider :facebook, 'PRODUCTION_APP_ID', 'PRODUCTION_APP_SECRET'
  end
end

You’ve actually already done quite a lot. Try running your application with rails server and navigating to /auth/twitter, /auth/facebook, or /auth/linkedin. You should (assuming you’ve set up applications with the respective providers correctly) be redirected to the appropriate site and asked to login.

Handling the Callback

Upon confirmation, you should be redirected back and get a routing error from Rails from /auth/yourprovider/callback. So let’s add a route! In config/routes.rb add:

# config/routes.rb
match '/auth/:provider/callback', :to => 'sessions#create', via: :get

But of course, this points to a non-existent controller, so let’s create that as well:

$ rails generate controller Sessions create

Now in our sessions_controller.rb lets add a bit of code:

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    render :text => request.env['omniauth.auth'].inspect
  end
end

If you start up the app again and go through the auth process, you should now see a hash that includes a whole lot of information about the user instead of a routing error. We’re on our way!

Authorizations and Users

One of the nice things about OmniAuth is that it doesn’t assume how you want to handle the authentication information, it just goes through the hard parts for you. We want users to be able to log in using one or many external services, so we’re actually going to separate users from authorizations. Let’s create simple Authorization and User models now.

$ rails g model Authorization provider uid user:references
$ rails g model User email

This creates the models we need with appropriate migrations. Notice that the User model doesn’t need to contain any information about authentication providers because we’ll model that through a relationship to the Authorization model. Set up your models like so:

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :authorizations
  validates :email, presence: true
end

# app/models/authorization.rb
class Authorization < ActiveRecord::Base
  belongs_to :user
  validates :user_id, :uid, :provider, presence: true
  validates_uniqueness_of :uid, :scope => :provider
end

Here we’re modeling very simple relationships and making sure that the Authorization has both a provider (“facebook”) and a uid (the Facebook ID, ie: 123456789123456). Next up, we’ll wire these models into our controller to create a real sign in process.

Signing Up/In

One of the nice things about external authentication is you can collapse the sign up and sign in process into a single step. What we’ll do here is:

  1. When a user signs in, look for existing Authorizations for that external account.
  2. Create a user if no authorization is found.
  3. Add an authorization to an existing user if the user is already logged in.

Let’s work backwards for this functionality by adding the code we want to have to the controller. Modify the create action in SessionsController to look like this:

# app/controllers/sessions_controller.rb
def create
  auth = request.env['omniauth.auth']
  unless @auth = Authorization.find_from_hash(auth)
    # Create a new user or add an auth to existing user, depending on
    # whether there is already a user signed in.
    @auth = Authorization.create_from_hash(auth, current_user)
  end
  # Log the authorizing user in.
  self.current_user = @auth.user

  render :text => "Welcome, #{current_user.email}."
end

Now let’s implement some of these methods. First up, adding some class methods to Authorization:

# app/models/authorization.rb
def self.find_from_hash(hash)
  find_by_provider_and_uid(hash['provider'], hash['uid'])
end

def self.create_from_hash(hash, user = nil)
  user ||= User.create_from_hash!(hash)
  Authorization.create(user: user, uid: hash['uid'], provider: hash['provider'])
end

Now we need to add the method referenced above to the User class:

# app/models/user.rb
def self.create_from_hash!(hash)
  create(email: hash.info.email)
end

Finally, we need to add some helpers to ApplicationController to handle user state:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protected

  def current_user
    @current_user ||= User.find_by_id(session[:user_id])
  end

  def signed_in?
    !!current_user
  end

  helper_method :current_user, :signed_in?

  def current_user=(user)
    @current_user = user
    session[:user_id] = user.id
  end
end

Voila! Now a user can sign in using any of their accounts and a User will automatically be fetched or created.