0
votes

I am trying (desperately) to setup my Rails 4 app with Devise and Omniauth.

My current efforts are in trying to get this tutorial to work for me.

http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/

Currently, I have:

Gemfile:

gem 'devise', '3.4.1'
gem 'devise_zxcvbn'
gem 'omniauth'
gem 'omniauth-oauth2', '1.3.1'
gem 'omniauth-google-oauth2'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-linkedin-oauth2'
gem 'google-api-client', require: 'google/api_client'
# gem 'oath'

Key differences between my gem file and the tutorial are that the tutorial does not include omniauth-oauth2 gem . I have included this because it is a requirement for google (which the tutorial does not use).

Identity.rb:

class Identity < ActiveRecord::Base


  belongs_to :user
  validates_presence_of :uid, :provider
  validates_uniqueness_of :uid, :scope => :provider


  def self.find_for_oauth(auth)
    find_or_create_by(uid: auth.uid, provider: auth.provider)
  end


end

My devise.rb includes config.omniauth for each of the 4 strategy providers.

My config/environment files include email settings for mailer actions.

routes.rb:

devise_for :users, #class_name: 'FormUser',
             :controllers => {
                # :registrations => "users/registrations",
                # :omniauth_callbacks => "users/authentications"
                :omniauth_callbacks => 'users/omniauth_callbacks'
           }
  match '/users/:id/finish_signup' => 'users#finish_signup', via: [:get, :patch], :as => :finish_signup

Omniauth callbacks controller:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def self.provides_callback_for(provider)
    class_eval %Q{
      def #{provider}
        @user = User.find_for_oauth(env["omniauth.auth"])

        if @user.persisted?
           sign_in_and_redirect @user,  event: :authentication

          set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
        else
          session["devise.#{provider}_data"] = env["omniauth.auth"]
          redirect_to new_user_registration_url
        end
      end
    }
  end



  [:twitter, :facebook, :linkedin, :google_oauth2].each do |provider|
    provides_callback_for provider
  end

  def after_sign_in_path_for(resource)
    if resource.email_verified?
      super resource
    else
      finish_signup_path(resource)
    end
  end 

end

Key differences between tutorial and my code: i have included google strategy

User.rb

class User < ActiveRecord::Base

  TEMP_EMAIL_PREFIX = 'change@me'
  TEMP_EMAIL_REGEX = /\Achange@me/

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable,
          :confirmable, :lockable,
         # :zxcvbnable,
         :omniauthable, :omniauth_providers => [:facebook, :linkedin, :twitter, :google_oauth2 ]



  mount_uploader :image, AvatarUploader

  # --------------- associations

  has_many :articles
  has_many :authentications, :dependent => :delete_all

  has_many :comments

  belongs_to :organisation
  has_one :profile
  has_many :qualifications
  has_many :identities

  has_many :trl_assessments, as: :addressable

  # has_and_belongs_to_many :projects

  # --------------- scopes

  # --------------- validations

   # validates_presence_of :first_name, :last_name
   validates_uniqueness_of :email

  # per sourcey tutorial - how do i confirm email registrations are unique?
  # this is generating an error about the options in the without function -- cant figure out the solution
  validates_format_of :email, :without => TEMP_EMAIL_REGEX, on: :update 

  # --------------- class methods


# sourcey tutorial

 def self.find_for_oauth(auth, signed_in_resource = nil)
    # Get the identity and user if they exist
    identity = Identity.find_for_oauth(auth)

    # If a signed_in_resource is provided it always overrides the existing user
    # to prevent the identity being locked with accidentally created accounts.
    # Note that this may leave zombie accounts (with no associated identity) which
    # can be cleaned up at a later date.
    user = signed_in_resource ? signed_in_resource : identity.user

    # p '11111'

    # Create the user if needed
    if user.nil?
      # p 22222
      # Get the existing user by email if the provider gives us a verified email.
      # If no verified email was provided we assign a temporary email and ask the
      # user to verify it on the next step via UsersController.finish_signup
      email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
      email = auth.info.email if email_is_verified
      user = User.where(:email => email).first if email

      # Create the user if it's a new registration
      if user.nil?
        # p 33333
        user = User.new(
          # at least one problem with this is that each provider uses different terms to desribe first name/last name/email. See notes on linkedin above
          first_name: auth.info.first_name,
          last_name: auth.info.last_name,
          email: email,
          #username: auth.info.nickname || auth.uid,
          password: Devise.friendly_token[0,20])
# 
        # debugger

        if email_is_verified
           user.skip_confirmation!
        end
        # user.skip_confirmation! 

        user.save!
      end
    end

    # Associate the identity with the user if needed
    if identity.user != user
      identity.user = user
      identity.save!
    end
    user
  end

  def email_verified?
    self.email && TEMP_EMAIL_REGEX =~ self.email
  end



    def full_name
        [*first_name.capitalize, last_name.capitalize].join(" ")
    end


  def formal_name
      [*self.profile.try(:title), first_name.capitalize, last_name.capitalize].join(" ")
  end


  #----------- user to be approved by admin (for now); later approved by approver


  def disapprove
    self.approved = false
  end

  def approved
    self.approved = true
  end

  def active_for_authentication?
    super && approved?
  end

  def inactive_message
    if !approved?
      :not_approved
    else
      super # Use whatever other message
    end
  end

  def self.send_reset_password_instructions(attributes={})
    recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
    if !recoverable.approved?
      recoverable.errors[:base] << I18n.t("devise.failure.not_approved")
    elsif recoverable.persisted?
      recoverable.send_reset_password_instructions
    end
    recoverable
  end


  # --------------- callbacks

  # after_create :add_default_role

  # --------------- instance methods

  # --------------- private methods

  # def add_default_role
  #   add_role(:pending) if self.roles.blank?
  # end

end

Key differences between my code and tutorial:

  1. changed name to split out between first and last name (to fit my schema) so used auth.info instead of raw info.

  2. In the tutorial, the find_for_oauth method creates a new user with an email attribute that has a question mark and an alternative to email? I deleted the question mark and everything to the right of it because I couldn't get past an error in this line & i couldn't understand what it was trying to do.

  3. I tried to handle confirmation exception with an if email_is_verified method instead of just skipping the confirmation.

  4. I changed the email_verified method to:

    self.email && TEMP_EMAIL_REGEX =~ self.email

I don't know why this change has been made -I tried to get help on codementor.io with these problems and this was one of the changes made (without explanation as to why).

  1. I have an extra validation to check the uniqueness of an email address.

Users controller:

class UsersController < ApplicationController

before_action :set_user, only: [:index, :show, :edit, :update, :destroy]

  def index
    if params[:approved] == "false"
      @users = User.find_all_by_approved(false)
    else
      @users = User.all
    end

  end

  # GET /users/:id.:format
  def show
    # authorize! :read, @user
  end

  # GET /users/:id/edit
  def edit
    # authorize! :update, @user
  end

  # PATCH/PUT /users/:id.:format
  def update
    # authorize! :update, @user
    respond_to do |format|
      if @user.update(user_params)
        sign_in(@user == current_user ? @user : current_user, :bypass => true)
        format.html { redirect_to @user, notice: 'Your profile was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: 'edit' }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # GET/PATCH /users/:id/finish_signup
  def finish_signup
    # authorize! :update, @user 
    if request.patch? && params[:user] #&& params[:user][:email]
      if @user.update(user_params)
        @user.skip_reconfirmation!
        sign_in(@user, :bypass => true)
        redirect_to @user, notice: 'Your profile was successfully updated.'
      else
        @show_errors = true
      end
    end
  end

  # DELETE /users/:id.:format
  def destroy
    # authorize! :delete, @user
    @user.destroy
    respond_to do |format|
      format.html { redirect_to root_url }
      format.json { head :no_content }
    end
  end

  private
    def set_user
      @user = User.find(params[:id])
    end

    def user_params
      params.require(:user).permit(policy(@user).permitted_attributes)
      # accessible = [ :first_name, :last_name, :email ] # extend with your own params
      # accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
      # accessible << [:approved] if user.admin
      # params.require(:user).permit(accessible)
    end

end

Key changes from tutorial: strong params are now defined in pundit user policy (exactly as per commented out bit above) although I have tried solving this problem with the strong params in the controller. It makes no difference to the current problem.

Finish sign up form:

<%= form_for(current_user, :as => 'user', :url => 
finish_signup_path(current_user), :html => { role: 'form'}) do |f| %>
        <% if @show_errors && current_user.errors.any? %>

          <div id="error_explanation">
          <% current_user.errors.full_messages.each do |msg| %>
            <%= msg %><br>
          <% end %>
          </div>
        <% end %>

    <div class="form-group">
      <!--  f.label :false  -->
      <div class="controls">
        <%= f.text_field :email, :autofocus => true, :value => '', class: 'form-control input-lg', placeholder: 'Example: [email protected] -- use your primary work address' %>
        <p class="help-block">Please confirm your email address. No spam.</p>
      </div>
    </div>
    <div class="actions">
      <%= f.submit 'Continue', :class => 'btn btn-primary' %>

Currently, when I try to register with twitter, I get this error message:

ActiveRecord::StatementInvalid in Users::OmniauthCallbacksController#twitter
PG::NotNullViolation: ERROR: null value in column "email" violates not-null constraint DETAIL: Failing row contains (19, null, null, null, $2a$10$aye71.1oEClA5vsy..8WzeknYl9wxi7W8VDEGlNbzakQQyJqe8i8q, null, null, null, 0, null, null, null, null, 

4e25979ab053eb4a26a6ba451d9, null, 2015-12-14 01:00:08.431268, null, 0, null, null, 2015-12-14 01:00:08.428961, 2015-12-14 01:00:08.428961, null, f). : INSERT INTO "users" ("email", "encrypted_password", "created_at", "updated_at", "confirmation_token", "confirmation_sent_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"

I don't know what this error means, or why I'm getting it. I think it most likely has something to do with the differences between my code and the tutorial outlined above, but I can't see which of them is causing the problem or how to go about solving it.

When I try the same thing with linkedin, I get this error:

NoMethodError (undefined method `approved?' for #<User:0x007fa90c867600>):
2015-12-14T01:02:10.472783+00:00 app[web.1]:   app/models/user.rb:130:in `active_for_authentication?'

I have an approved attribute in my schema. I also added a method called 'approved' to my user model to try and solve this, but it isn't working.

Can anyone see anything that might help. I've been trying to get this set up for the best part of 3 years. I've dumped $$$$ into code mentors, paid various consultants and been to every rails meetup in my area, but I can't find anyone who can help.

Taking Moustafa's suggestions, in relation to the linkedin problem:

I commented out each of these approval methods from my user.rb:

# def disapprove
  #   self.approved = false
  # end

  # def approved?
  #   self.approved = true
  # end

  # def active_for_authentication?
  #   super && approved?
  # end

  # def inactive_message
  #   if !approved?
  #     :not_approved
  #   else
  #     super # Use whatever other message
  #   end
  # end

The first two are there in an attempt to solve this approved? problem and the second two are there because, following this tutorial, I want to allow an admin to approve a newly registered user before they can sign in.

https://github.com/plataformatec/devise/wiki/How-To:-Require-admin-to-activate-account-before-sign_in

Anyway, commenting those out and trying again, I then get this error:

Started PATCH "/users/1/finish_signup" for 49.191.133.120 at 2015-12-14 08:18:49 +0000
2015-12-14T08:18:49.536922+00:00 app[web.1]: 
2015-12-14T08:18:49.536927+00:00 app[web.1]: NameError (undefined local variable or method `user' for #<UsersController:0x007f7215732e48>):
2015-12-14T08:18:49.536929+00:00 app[web.1]:   app/controllers/users_controller.rb:72:in `user_params'
2015-12-14T08:18:49.536930+00:00 app[web.1]:   app/controllers/users_controller.rb:43:in `finish_signup'
2015-12-14T08:18:49.536941+00:00 app[web.1]: 

Line 72 of my users controller has this:

accessible << [:approved] if user.admin

ATTEMPT 2:

I uncommented the last two approved methods (being those suggested in the devise tutorial for admin approval of newly registered users). When I save and restart the server, the home page won't even load. The logs say:

Completed 500 Internal Server Error in 21ms (ActiveRecord: 1.8ms)
2015-12-14T08:45:09.932008+00:00 app[web.1]: 
2015-12-14T08:45:09.932010+00:00 app[web.1]: ActionView::Template::Error (undefined method `approved?' for #<User:0x007f6afe8a2878>):
2015-12-14T08:45:09.932012+00:00 app[web.1]:     12:                 <span class="deviselinks" style="padding-right:30px">
2015-12-14T08:45:09.932011+00:00 app[web.1]:     10:             <div class="col-md-8>"> 
2015-12-14T08:45:09.932012+00:00 app[web.1]:     11:               <ul style="text-align: right">
2015-12-14T08:45:09.932016+00:00 app[web.1]:   app/models/user.rb:130:in `active_for_authentication?'
2015-12-14T08:45:09.932014+00:00 app[web.1]:     14:                     Hi <%= link_to(current_user.first_name.titlecase, profile_path(current_user)) %></span>
2015-12-14T08:45:09.943612+00:00 heroku[router]: at=info method=GET path="/" host=www.[].com request_id=a5646532-14a6-46ed-8dd7-6e4741688cf3 fwd="49.191.133.120" dyno=web.1 connect=1ms service=34ms status=500 bytes=1754

I don't know what full stack trace means - is that a terminal command?

1

1 Answers

0
votes

Here is my input:

in the following code I grabbed from you question you are leaving the email variable = nil if email not verified. but you don't provide any dummy data for email if email not verified. then you try to create a user with email = nil which result exceptions.

  email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
  email = auth.info.email if email_is_verified
  user = User.where(:email => email).first if email

For twitter case:

  • The verified key exist under the extra.raw_info which means for any try to authenticate with twitter will fail because even if the email is verified. You are looking in wrong location and you will always get email with nil, then you try to create a user account with email = nil which is prevented by devise as email is required. here find a link to omniauth-twitter see the hash provided by omniauth.

  • As for the second issue with linkedin apparently the email gets verified user object looks for a method called approved? while you have in user model a method called approved totally different methods.