ruby-on-railsrubyfacebookdeviseomniauth-facebook

Additional User Attribute Validation Failure with Omniauth-Facebook


Hello everyone and thank you in advanced for your expertise and help.

I'm not very skilled in Ruby on Rails (started merely 3 months ago) and am building a rather complex application. I have hit a roadblock concerning Omniauth-Facebook. I have Devise, omniauth-facebook, an omniauth_callbacks Controller, a Devise registrations_controller, a Devise users_controller, and a User Model with validations for a multitude of fields (see all code below). When setting this all up, I more or less followed this Youtube video: https://www.youtube.com/watch?v=VeUX3pWn28w and many other scattered guides when I attempted troubleshooting. Some were outdated and I'm not even sure anymore what parts of my code could be outdated or not.

What's working from my Signup form: A user can fill out my custom signup form and be properly added to the database with all Devise actions functioning correctly and all custom table attributes being recorded. A user can click the "Signup with Facebook" button and be directed to Facebook to login/signup, where Provider and UID is being returned, as well as (it seems) First Name, Email, and Password. I have State and Zip defaulted in my schema, so that doesn't raise an validation exception.

What seems to be failing: When Facebook is sending back the info, it seems to break at Email returning ("User Exists"). Even though I've included "auth.info.last_name, auth.info.image, etc.", the terminal error never shows those additional fields returning from Facebook, but the validations don't throw an exception, including email. In my database, I made sure there is no duplication of that email. Due to my DB and validations, it seems my required Last Name, Gender, DOB, Address and City are not being filled in as well, which is understandable (besides last_name) because I have no way of receiving that from Facebook.

This is what I want: As I mentioned above, Facebook doesn't seem to be returning "last_name", "address" or "image". I also need to include Birth Date, Gender and their City. It seems I cannot do that from omniauth-facebook since Facebook's Auth Hash doesn't include those fields. How can I allow a user to sign up via Facebook with those provided fields, be signed in via Facebook, but then be directed to a page where they can enter the additional information that's missing that my DB requires? So after signing up via facebook, how do I make it so a User is sent to another page (without having those validation exceptions being thrown up?) to then enter that additional information (last name, date of birth, address, address2 [if needed], gender, and city/state/zip, or/else any other fields missing)? And why might "last_name" be failing, but "first_name" is fine?

Below is a picture of everything you guys might need and I think may be required. Again, I understand the basics of Ruby and Rails and would consider myself intermediate, but need help in understanding why/how the answer fixes this issue I am experiencing. I sadly lack knowledge on how advanced Routes and Controllers work so if you have any resource that'll help me sharpen my knowledge of that subject, I'd appreciate that, too.

Users Table (schema.rb):

  create_table "users", force: :cascade do |t|
    t.string "first_name", null: false
    t.string "last_name", null: false
    t.boolean "admin", default: false
    t.boolean "subscribed", default: true
    t.string "zip", default: "44107", null: false
    t.string "phone_number"
    t.string "address", null: false
    t.string "city_name", default: "Lakewood", null: false
    t.string "state", default: "Ohio", null: false
    t.date "dob", null: false
    t.integer "city_id"
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer "sign_in_count", default: 0, null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string "current_sign_in_ip"
    t.string "last_sign_in_ip"
    t.string "confirmation_token"
    t.datetime "confirmed_at"
    t.datetime "confirmation_sent_at"
    t.string "unconfirmed_email"
    t.integer "failed_attempts", default: 0, null: false
    t.string "unlock_token"
    t.datetime "locked_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "address2"
    t.boolean "gender"
    t.string "provider"
    t.string "uid"
    t.string "name"
    t.text "image"
    t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
    t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
  end 

Users Model (user.rb):

class User < ApplicationRecord
      # Below - Devise Modules. We have not used = :timeoutable 
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :trackable, 
             :validatable, :confirmable, 
             :omniauthable, omniauth_providers: [:facebook]

      # Below - Additional validations of DB field entries presence at Model-level.
      validates :first_name, presence: true, length: { maximum: 30 }
      validates :last_name, presence: true, length: { maximum: 30 }
      validates :dob, presence: { message: "(Date of Birth) must be entered" } 
      validates :zip, presence: true, length: { maximum: 11 }
      validates :city_name, presence: true   
      validates :state, presence: true, length: { maximum: 15 }
      validates :address, presence: true
      validates :gender, presence: { message: "must be selected" } 

       # Below - Associates Users into a One to Many relationship with Cities.
      belongs_to :city


      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


      def self.from_omniauth(auth)
        where(provider: auth.provider, uid: auth.uid).first_or_create! do |user|
          user.provider       = auth.provider
          user.uid            = auth.uid
          user.email          = auth.info.email
          user.first_name     = auth.info.first_name
          user.password       = Devise.friendly_token[0,20] 
          user.last_name      = auth.info.last_name
          user.image          = auth.info.image
          user.address        = auth.info.location
          user.skip_confirmation!

          user.save

      end


      end

      # Below - Turns emails into downcase when saved from the controller into DB.      
      before_save { self.email = email.downcase }

    end

Sessions Controller (controllers/users/sessions_controller.rb):

class Users::SessionsController < Devise::SessionsController
  # before_action :configure_sign_in_params, only: [:create]
  class SessionsController < ApplicationController
    def create
      @user = User.find_or_create_from_auth_hash(auth_hash)
      self.current_user = @user
      redirect_to '/'
    end

    protected

    def auth_hash
      request.env['omniauth.auth']
    end
  end

Registrations Controller (controllers/users/registrations_controller.rb):

class Users::RegistrationsController < Devise::RegistrationsController
   before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
   def new
     @user = User.new
   end

  # POST /resource
  def create
    super
  end

  # GET /resource/edit
   def edit
     super
   end

  # PUT /resource
   def update
     super
   end

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :gender, :dob, :admin, :phone_number, :address, :address2, :city_name, :state, :zip, :subscribed, :city_id, :name, :image, :uid, :provider])
  end

end 

Users Controller (controllers/users_controller.rb):

class UsersController < ApplicationController

    # Main Users Controller

      # Index Action for all Users
      def index
        @users = User.all
      end

      # Show Action for individual user ID
      def show 
        @user = User.find(params[:id])
      end 
    end

Omniauth Callback Controller (controllers/user/omniauth_callbacks_controller.rb):

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Main MOdel for Facebook OmniAuth User Login/Signup

  def facebook
    #raise request.env["omniauth.auth"].to_yaml
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, :event => :authentication
      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

Application Controller (controllers/application_controller.rb):

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?

# Below, permits strong params from signup process, sign_in, and Account_update
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:login, keys: [:email, :password])
    devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :address, :address2, :dob, :city_name, :state, :zip, :gender, :phone_number, :subscribed, :email, :password, :password_confirmation, :city_id, :name, :image, :uid, :provider]) 
    devise_parameter_sanitizer.permit(:account_update, keys: [:email, :address, :address2, :city_name, :state, :zip, :phone_number, :subscribed, :password, :password_confirmation, :current_password])
  end

end

Devise Initializer (config/initializers/devise.rb):

 # ==> OmniAuth
  # Add a new OmniAuth provider. Check the wiki for more information on setting
  # up on your models and hooks.
  config.omniauth :facebook, 'BLANK', 'BLANK', callback_url: 'MYWEBADRESS/users/auth/facebook/callback'

Routes (routes.rb):

Rails.application.routes.draw do
# Main Routes file for all route, URL names and Action Methods. 

  # Below - Sets up custom login and log out path names for routes for user model.
  devise_for :users, path_names: { sign_in: "login", sign_out: "logout" }, controllers: {
                                    sessions: 'users/sessions', 
                                    :omniauth_callbacks => "users/omniauth_callbacks"   }

  # Not logged in visitors will be greeted by Index page
  root 'visitors#index'

  get 'cities/index'

  # Callback Route after OmniAuth redirects back to Ossemble for User Signup
  get '/auth/facebook/callback', to: 'sessions#create'

  # Below - Sets up routes for admin model.
  devise_for :admins

  # Below - Creates all routes for City model.
  resources :cities, except: [:destroy]

  # Below - Creates show route for Users & Model
  resources :users, only: [:show, :index]

end

And lastly my Signup Form (views/devise/registrations/new.html.erb):

<%= form_for(resource, as: resource_name, url: registration_path(resource_name),
      :html => {class: "form", role: "form"}) do |f| %>
    <div class="container alert"> <!-- Begin - Error Message(s) Wrapper for User Signup -->
      <%= devise_error_messages! %>
    </div>                        <!-- End - Error Message(s) Wrapper for User Signup -->
    <div class="container"> <!-- Begin -- Signup Form -->
      <div class="form-row"> <!-- Begin - First, Last Name, Gender, & Date of Birth Form Row -->
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :first_name, "First Name" %> 
          </div>
            <%= f.text_field :first_name, autofocus: true, class: "form-control", placeholder: "Enter First Name" %>
        </div>
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :last_name, "Last Name" %> 
          </div>
            <%= f.text_field :last_name, class: "form-control", placeholder: "Enter Last Name" %>
        </div>
       <div class="form-group col-md-2">
          <div class="control-label">
            <label class="control-label"> 
            Gender
            </label>
          </div>   
            <div class="gender_form form-control center" >
              <%= f.label :gender, "Male", id: "male_text", class: "gender_text" %>
              <%= f.radio_button :gender, true,  class: "gender_box"%>
              <%= f.label :gender, "Female", id: "female_text", class: "gender_text" %>
              <%= f.radio_button :gender, false, class: "gender_box" %>
            </div>
        </div>
        <div class="form-group col-md-2">
          <div class="control-label">
            <%= f.label :dob, "Birth Date" %> 
          </div>
            <%= f.date_field :dob, class: "form-control" %>
        </div>
      </div> 
      <br> <!-- End - of Name, Gender & DOB form row -->

    <!-- Begin - Email & Password Form Row -->
      <div class="form-row">
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :email, "Email Address" %>
          </div>
            <%= f.email_field :email, type:"text", class: "form-control", placeholder: "Enter Email Address: youremail@example.com" %>
        </div>  
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :password, "Password" %>
            <!-- Begin - If statement checks for password minimum character length  -->
            <small>
              (Minimum: <%= @minimum_password_length %> characters) 
            </small>
          </div>
            <%= f.password_field :password, autocomplete: "off", id:"inputPassword", class: "form-control", placeholder: "Create a Password" %>
          <!-- End - password character length if statement -->
        </div>
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :password_confirmation, "Confirm Password" %> 
          </div>  
            <%= f.password_field :password_confirmation, autocomplete: "off", id:"inputPasswordConfirm", class: "form-control", placeholder: "Re-enter Password" %>
        </div>
      </div> 
      <br> <!-- End - Email & Password Form Row -->

    <!-- Begin -  Address & Phone Number Form Row--> 
      <div class="form-row">
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :address, "Primary Address" %> 
            </div>
            <%= f.text_field :address, autocomplete: "on", class: "form-control", placeholder: "Enter Address: 1234 Main Street" %>
        </div>
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :address2, "Secondary Address" %> <small> (optional) </small>
          </div>
            <%= f.text_field :address2, autocomplete: "on", class: "form-control", placeholder: "Apartment, Suite #" %>
        </div>
        <!--  Phonne number currently hideen on form due to "style-" and "disabled :true" -->
        <div class="form-group col-md-3" style="display: none">
          <div class="control-label">
            <%= f.label :phone_number, "Phone Number" %> 
          </div>
            <%= f.phone_field :phone_number, disabled: true, class: "form-control", placeholder: "Enter Phone #: 555-555-5555" %>
        </div>
      </div> <br> <!-- End - Address & Phone Number Row -->

  <!--  Beginning - Location Form Row  -->
      <div class="form-row">
        <div class="form-group col-md-4">
          <div class="control-label">
            <%= f.label :city_name, "City" %> 
              <small>
                (Currently Lakewood, OH Supported)
              </small>
          </div> 
<!-- Below, we list all cities to be selected and we set the user's "city_id" field to the corresponding City table's City ID, completing the User's Association to City  -->
            <%= f.collection_select :city_id, City.all, :id, :name, {prompt: "Choose a City"}, class: "select_form form-control" %>
        </div>
        <div class="form-group col-md-2 ">
          <div class="control-label">
            <%= f.label :state, "State" %> 
              <small>
                (Currently Ohio)
              </small>
          </div>
            <%= f.collection_select :state, City.all, :state, :state, {prompt: "Choose a State"}, class: "select_form form-control" %>
        </div>
        <div class="form-group col-md-2">
          <div class="control-label">
            <%= f.label :zip, "Zip Code" %> <small> (5-Digit Zip)</small>
          </div>
            <%= f.collection_select :zip, City.all, :zip, :zip, {prompt: "Choose Zip-Code"}, maxlength: "5", class: "select_form form-control" %>
        </div>
      </div> <!--  End - Location Form Row -->

    <!-- Begin - Subscribe Option -->
      <div class="form-row">
        <div class="form-group col-md-12 pull-left">
          <div class="form-check">
            <div class="control-label">
              <%= f.label :subscribed, "Subscribe?" %> 
                <small>
                  <em>
                  Stay up to date with Ossemble!
                  </em>
                </small> 
              <br>
              <div class="form-row pull-left">  
                <div class="col-md-12">
                  <div>
                    <%= f.check_box :subscribed, class: "check_box"  %>
                    <%= f.label "Yes!", class: "check_box_label"  %>
                  </div>
                </div>
              </div>
            </div> 
          </div> 
        </div>
      </div> <!-- End - Subscribe Option -->
      <div class="form-row"> <!-- Begin - Create Account Button Row (FB) -->  
        <div class="form-group col-md-2">
          <div class="actions">
            <%= f.submit "Create Account", class: "form_btn btn btn-success btn-lg btn-xlarge" %>
          </div>
        </div> 
        <div class="form-group col-md-4"> <!-- Begin - Signup With Facebook Button -->
          <%- if devise_mapping.omniauthable? %> <!-- Begin - OmniAuth If Statement -->
              <!-- Below - OmniAuth & FB Signup Button -->
              **<%= link_to '<i class="fa fa-facebook fa-lg fa-fw fb_log_sign_icon" aria-hidden="true"></i> Facebook Signup'.html_safe, 
                                     user_facebook_omniauth_authorize_path, 
                                     class: "form_btn btn btn-primary btn-lg btn-xlarge" %> <br />
          <% end -%>**                  <!-- End - OmniAuth If Statement -->
        </div>                        <!-- End - Signup with Facebook Button -->
      </div>                 <!-- End - Create Account Button Row (FB) -->
    </div>                  <!-- End - Signup Form -->
<% end %> 

And my Errors from Browser and Terminal: enter image description here enter image description here


Solution

  • In your Devise initializer (config/initializers/devise.rb) you need to use the info_fields parameter to add the items you want Facebook to return. Facebook only returns name, email and id by default. For example:

    # Add a new OmniAuth provider. Check the wiki for more information on setting
    # up on your models and hooks.
    config.omniauth :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], 
       callback_url: 'https://ossemble-workspace-twistedben.c9users.io/users/auth/facebook/callback',
       info_fields: 'email,name,birthday,locale,gender'
    

    You can get a list of the items available on the Facebook user object here. Some of the items (such as birthday) require the permission of the user and special approval of your app by Facebook. You'll need to thoroughly explore this in the documentation.

    Also, a Facebook user can deny your app access to any part of his/her profile. Just because your request the email from Facebook, does not guarantee the call back will include the email. Your app will need to gracefully handle those cases.

    In your case, you may want to create a new user record but not persist it right away. Instead, pass that object to a registration form that lets the user fill in the blanks in his/her profile. Or you can save the new record without validation (user.save(validate:false)).

    Also, I recommend storing your app id and app secret in an environment variable (ENV[...]) and not checking it into source code or posting it online. You will want to get a new Facebook secret immediately.