Search code examples
ruby-on-railsdeviseomniauthruby-on-rails-5omniauth-facebook

Devise with OmniAuth Facebook


I'm stuck with integrating devise with omniauth-facebook

My demo app is using:

  • Rails 5
  • Devise
  • Pundit
  • OmniAuth (omniauth-facebook)

I followed the following guide on the Wiki page: https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview but having problems creating the user.

It will sign-in the user and add the app to the user's Facebook (in App Settings) but it does register the account to the User model.

# Gemfile
source 'https://rubygems.org'

gem 'rails', '~> 5.0.0'

gem 'devise'
gem 'pundit'
gem 'omniauth-facebook' 

# Gemfile.lock for the 3 authorization and authentication gems
GEM
  remote: https://rubygems.org/
  remote: https://rails-assets.org/
  specs:

    devise (4.2.0)
      bcrypt (~> 3.0)
      orm_adapter (~> 0.1)
      railties (>= 4.1.0, < 5.1)
      responders

      warden (~> 1.2.3)
    pundit (1.1.0)

    omniauth (1.3.1)
      hashie (>= 1.2, < 4)
      rack (>= 1.0, < 3)
    omniauth-facebook (4.0.0)
      omniauth-oauth2 (~> 1.2)
    omniauth-oauth2 (1.4.0)
      oauth2 (~> 1.0)
      omniauth (~> 1.2)

The schema for the User model.

I added a migration provider:string and uid:string

# == Schema Information
#
# Table name: users
#
#  id                     :integer          not null, primary key
#  email                  :string           default(""), not null
#  encrypted_password     :string           default(""), not null
#  reset_password_token   :string
#  reset_password_sent_at :datetime
#  remember_created_at    :datetime
#  sign_in_count          :integer          default(0), not null
#  current_sign_in_at     :datetime
#  last_sign_in_at        :datetime
#  current_sign_in_ip     :inet
#  last_sign_in_ip        :inet
#  created_at             :datetime         not null
#  updated_at             :datetime         not null
#  name                   :string           default(""), not null
#  provider               :string
#  uid                    :string

The ApplicationController contains pundit and devise code:

class ApplicationController < ActionController::Base
  include Pundit
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?

  private

    def user_not_authorized
      flash[:error] = "You are not authorized to perform this action."
      redirect_to(request.referrer || root_path)
    end

  protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
      devise_parameter_sanitizer.permit(:account_update, keys: [:name])
    end
end

I've configured config/initializers/devise.rb to include :facebook in the omniauth config.

# File: config/initializers/devise.rb
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
  require 'devise/orm/active_record'

  config.case_insensitive_keys = [:email]
  config.strip_whitespace_keys = [:email]
  config.skip_session_storage = [:http_auth]
  config.stretches = Rails.env.test? ? 1 : 11
  config.reconfirmable = true
  config.password_length = 6..128
  config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
  config.sign_out_via = :delete

  # ==> OmniAuth
  # Add a new OmniAuth provider. Check the wiki for more information on setting
  # up on your models and hooks.
  # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
  config.omniauth :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], callback_url: ENV['FACEBOOK_CALLBACK_URL'], scope: 'email', info_fields: 'email,name'
end

I've added omniauthable to the User model.

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :omniauthable, omniauth_providers: [:facebook]

  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.name = auth.info.name
      user.email = auth.info.email
      user.password = Devise.friendly_token[0,20]
    end
  end

  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
      end
    end
  end
end

I've created an OmniauthCallbacksController to handle sign-in requests.

# File: app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, kind: "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

In the Routes I've added the following:

# File: config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }

  root 'pages#index'
end

Added "Sign in with Facebook" link to devise sessions#new view.

# File: app/views/devise/sessions/new.html.erb
<div class="login">
  <%= link_to "Sign in with Facebook", user_facebook_omniauth_authorize_path %>
 # This is the link it creates: <a href="/users/auth/facebook">Sign in with Facebook</a>
</div>

User flow:

  1. Clicks the "Sign in with Facebook"
  2. User gets forwarded to Facebook's Oauth url with 'Continue as User' prompt where user clicks "OK"
  3. User is returned to the app but user isn't registered in the database
  4. The url is a long string of params code and state (check *NB

*NB Return url from Facebook:

http://localhost:3000/?code=REALLYLONGHASHOFCHARACTERS&state=ANOTHERSETOFREALLYLONGHASHOFCHARACTERS

The development.log show:

Started GET "/users/auth/facebook" for ::1 at 2016-08-10 18:43:26 +1000
I, [2016-08-10T18:43:26.084371 #2292]  INFO -- omniauth: (facebook) Request phase initiated.
Started GET "/users/auth/facebook" for ::1 at 2016-08-10 18:43:26 +1000
I, [2016-08-10T18:43:26.521627 #2292]  INFO -- omniauth: (facebook) Request phase initiated.
Started GET "/?code=REALLYLONGHASHOFCHARACTERS&state=ANOTHERSETOFREALLYLONGHASHOFCHARACTERS" for ::1 at 2016-08-10 18:44:19 +1000
Processing by ListingsController#index as HTML
  Parameters: {"code"=>"REALLYLONGHASHOFCHARACTERS", "state"=>"ANOTHERSETOFREALLYLONGHASHOFCHARACTERS"}

I don't think it's hitting Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController because I put a raise "Error" on the facebook action but it doesn't raise any errors. And I can't see the omniauth_callbacks: "users/omniauth_callbacks" when running Rails routes

Checking the routes show:

# Routes

||                           Prefix Verb     URI Pattern                             Controller#Action
||                 new_user_session GET      /users/sign_in(.:format)                devise/sessions#new
||                     user_session POST     /users/sign_in(.:format)                devise/sessions#create
||             destroy_user_session DELETE   /users/sign_out(.:format)               devise/sessions#destroy
|| user_facebook_omniauth_authorize GET|POST /users/auth/facebook(.:format)          users/omniauth_callbacks#passthru
||  user_facebook_omniauth_callback GET|POST /users/auth/facebook/callback(.:format) users/omniauth_callbacks#facebook
||                    user_password POST     /users/password(.:format)               devise/passwords#create
||                new_user_password GET      /users/password/new(.:format)           devise/passwords#new
||               edit_user_password GET      /users/password/edit(.:format)          devise/passwords#edit
||                                  PATCH    /users/password(.:format)               devise/passwords#update
||                                  PUT      /users/password(.:format)               devise/passwords#update
||         cancel_user_registration GET      /users/cancel(.:format)                 devise/registrations#cancel
||                user_registration POST     /users(.:format)                        devise/registrations#create
||            new_user_registration GET      /users/sign_up(.:format)                devise/registrations#new
||           edit_user_registration GET      /users/edit(.:format)                   devise/registrations#edit
||                                  PATCH    /users(.:format)                        devise/registrations#update
||                                  PUT      /users(.:format)                        devise/registrations#update
||                                  DELETE   /users(.:format)                        devise/registrations#destroy
||                             root GET      /                                       pages#index

I'm not sure what to do from here. Any help and insight is much appreciated.


Solution

  • All working now.

    It has to do with the incorrect callback_url.

    From the logs:

    Started GET "/users/auth/facebook" for ::1 at 2016-08-10 18:43:26 +1000
    I, [2016-08-10T18:43:26.084371 #2292]  INFO -- omniauth: (facebook) Request phase initiated.
    Started GET "/users/auth/facebook" for ::1 at 2016-08-10 18:43:26 +1000
    I, [2016-08-10T18:43:26.521627 #2292]  INFO -- omniauth: (facebook) Request phase initiated.
    Started GET "/?code=REALLYLONGHASHOFCHARACTERS&state=ANOTHERSETOFREALLYLONGHASHOFCHARACTERS" for ::1 at 2016-08-10 18:44:19 +1000
    

    The last line of the log file shows it's not going to the correct url:

    Started GET "/?code=REALLYLONGHASHOFCHARACTERS&state=ANOTHERSETOFREALLYLONGHASHOFCHARACTERS" for ::1 at 2016-08-10 18:44:19 +1000

    The initializer config shows:

    config.omniauth :facebook, 
       ENV['FACEBOOK_APP_ID'],
       ENV['FACEBOOK_APP_SECRET'],
       callback_url: ENV['FACEBOOK_CALLBACK_URL'],
       scope: 'email', info_fields: 'email,name'
    

    The callback_url was http://localhost:3000 which is incorrect.

    I changed the callback_url from http://localhost:3000 to http://localhost:3000/users/auth/facebook/callback

    It is now working and I hope this helps someone in the future.