4

I am trying to make an app with Rails 4.

I have been trying (for 3+ years) to figure out how to get Devise and Omniauth to works, so that users can add multiple social media accounts to their user profile.

I've read all of the devise and omniauth documentation. The best I can get to with those docs is to add 1 single social media account. That's not what I want.

I've tried this site point tutorial sitepoint.com/rails-authentication-oauth-2-0-omniauth

I've tried this willschenck tutorial http://willschenk.com/setting-up-devise-with-twitter-and-facebook-and-other-omniauth-schemes-without-email-addresses/

I've tried this jorge.caballeromurillo tutorial: http://jorge.caballeromurillo.com/multiple-omniauth-providers-for-same-user-on-ruby-on-rails/

I've also tried this sourcey tutorial: http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/

I've pledged thousands of points in bounties on SO in trying to find help with this problem - but not yet figured it out. I've been to every rails meetup in my area for the last 3 years and wasted $$$ on codementor in trying to find help. Enough time has passed since the most recent frustrating attempt to be ready to give it another go. Please help.

Here's what I have so far:

User.rb

devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable,
          :confirmable, :lockable,
         # :zxcvbnable,
         :omniauthable, :omniauth_providers => [:facebook, :linkedin, :twitter, :google_oauth2 ]

has_many :identities, dependent: :destroy

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 # take out this if stmt for chin yi's solution
      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 ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
          #username: auth.info.nickname || auth.uid,
          password: Devise.friendly_token[0,20])
# fallback for name fields - add nickname to user table
        # 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

Identity.rb

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

Users controller:

class UsersController < ApplicationController

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

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

  end

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

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


  # PATCH/PUT /users/:id.:format
  def update
    # authorize! :update, @user
    respond_to do |format|
      authorize @user
      if @user.update(user_params)
        sign_in(@user == current_user ? @user : current_user, :bypass => true)
        # I'm trying to get the user matched to an organisation after the email address (in finish sign up) updates the user model.
        UserOrganisationMapperService.call(@user)

        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!
        # @user.confirm!

        sign_in(@user, :bypass => true)

        redirect_to root_path#, notice: 'Your profile was successfully updated.'
        # redirect_to [@user, @user.profile || @user.build_profile]
        # sign_in_and_redirect(@user, :bypass => true)
      else
        @show_errors = true
      end
    end
  end

  # DELETE /users/:id.:format
  def destroy
    # authorize! :delete, @user
    @user.destroy
    authorize @user
    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])
      authorize @user
    end

    def user_params
      # params.require(:user).permit(policy(@user).permitted_attributes)
      accessible = [ :first_name, :last_name, :email, :avatar, {role_ids: []} ] # 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

Identities controller

class IdentitiesController < ApplicationController
  before_action :set_identity, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!

  # GET /identities
  # GET /identities.json
  def index
    @identities = Identity.all
  end

  # GET /identities/1
  # GET /identities/1.json
  def show
  end

  # GET /identities/new
  def new
    @identity = Identity.new
  end

  # GET /identities/1/edit
  def edit
  end

  # POST /identities
  # POST /identities.json


def create

    @identity = Identity.new(identity_params)

    respond_to do |format|
      if @identity.save
        format.html { redirect_to @identity, notice: 'Identity was successfully created.' }
        format.json { render :show, status: :created, location: @identity }
      else
        format.html { render :new }
        format.json { render json: @identity.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /identities/1
  # PATCH/PUT /identities/1.json

create alternative that I have also tried

def create
  auth = request.env['omniauth.auth']
  # Find an identity here
  @identity = Identity.find_with_omniauth(auth)

  if @identity.nil?
    # If no identity was found, create a brand new one here
    @identity = Identity.create_with_omniauth(auth)
  end

  if signed_in?
    if @identity.user == current_user
      # User is signed in so they are trying to link an identity with their
      # account. But we found the identity and the user associated with it 
      # is the current user. So the identity is already associated with 
      # this user. So let's display an error message.
      redirect_to root_url, notice: "Already linked that account!"
    else
      # The identity is not associated with the current_user so lets 
      # associate the identity
      @identity.user = current_user
      @identity.save
      redirect_to root_url, notice: "Successfully linked that account!"
    end
  else
    if @identity.user.present?
      # The identity we found had a user associated with it so let's 
      # just log them in here
      self.current_user = @identity.user
      redirect_to root_url, notice: "Signed in!"
    else
      # No user associated with the identity so we need to create a new one
      redirect_to new_registration_path, notice: "Please finish registering"
    end
  end
end

def update

    respond_to do |format|
      if @identity.update(identity_params)
        format.html { redirect_to @identity, notice: 'Identity was successfully updated.' }
        format.json { render :show, status: :ok, location: @identity }
      else
        format.html { render :edit }
        format.json { render json: @identity.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /identities/1
  # DELETE /identities/1.json
  def destroy
    @identity.destroy
    respond_to do |format|
      format.html { redirect_to identities_url, notice: 'Identity was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_identity
      @identity = Identity.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def identity_params
      params[:identity]
    end
end

registrations controller

class Users::RegistrationsController < Devise::RegistrationsController

  before_action :configure_permitted_parameters, if: :devise_controller?

  def create
    super do |resource|
      UserOrganisationMapperService.call(resource)
    end
  end




  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:email, :password, :first_name, :last_name) }
  end


  private

end

omniauth callbacks controller

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  #sourcey tutorial ------------------

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

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


        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



end

users/finish sign up view

 <div class="container-fluid">
   <div class="row">
    <div class="col-xs-8 col-xs-offset-2">
     <h1 class="formheader">Complete your registration</h1>

        <%= 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">

        <% if current_user.first_name.blank? %>  
            <%= f.text_field :first_name,  :value => '', class: 'form-control input-lg', placeholder: 'First name' %>
            <p class="help-block">Hi there, what is your first name?.</p>
        <% end %>

        <% if current_user.last_name.blank? %>  
            <%= f.text_field :last_name,  :value => '', class: 'form-control input-lg', placeholder: 'Last name (surname)' %>
            <p class="help-block">Add your last name</p>
        <% end %>    


        <% if !current_user.email_verified? %> 
          <%= f.text_field :email,  :value => '', class: 'form-control input-lg', placeholder: 'Example: email@me.com -- use your primary work or university address' %>
           <p class="help-block">Please confirm your email address. No spam.</p>
        <% end %>   


      </div>
    </div>
    <div class="actions">
      <%= f.submit 'Continue', :class => 'btn btn-primary' %>
    </div>
    <% end %>
    </div>
  </div>
</div>

users/authentications view

<div class="container-fluid">
   <div class="row">
        <div class="col-xs-8 col-xs-offset-2">
            <div class="table-responsive" style="margin-left:30px; margin-top:15px">
                <table class="table table-bordered">

                    <tr>
                      <td><i class="fa fa-facebook"></i></td>   
                      <td>

                        <% if @user.identities.map(&:provider).include?('facebook') %>
                            <span class="glyphicon glyphicon-ok"</span>
                        <% else %>  
                            <%= link_to icon('Connect Facebook', id: 'facebookauth'), user_omniauth_authorize_path(:facebook) %>
                        <% end %>   

                      </td>
                    </tr>

                    <tr>
                      <td><i class="fa fa-google"></i></td> 
                      <td>
                        <% if @user.identities.map(&:provider).include?('googleauth') %>
                            <span class="glyphicon glyphicon-ok"</span>
                        <% else %>  
                            <%= link_to icon('Connect Google', id: 'googleauth'), user_omniauth_authorize_path(:google_oauth2) %>
                        <% end %>   

                      </td>
                    </tr>

                    <tr>
                      <td><i class="fa fa-linkedin"></i></td>   
                      <td>
                        <% if @user.identities.map(&:provider).include?('linkedin') %>
                            <span class="glyphicon glyphicon-ok"</span>
                        <% else %>  
                            <%= link_to icon('Connect Linkedin', id: 'linkedinauth'), user_omniauth_authorize_path(:linkedin) %>
                        <% end %>

                      </td>
                    </tr>


                    <tr>
                      <td><i class="fa fa-twitter"></i></td>    
                      <td>
                        <% if @user.identities.map(&:provider).include?('twitter') %>
å                           <span class="glyphicon glyphicon-ok"</span>
                        <% else %>  
                            <%= link_to icon('Connect Twitter', id: 'twitterauth'), user_omniauth_authorize_path(:twitter) %>
                        <% end %>

                      </td>
                    </tr>

                    <tr>
                      <td>Password</td> 
                      <td>
                        <% if @user.encrypted_password.present? %>
                            <span class="glyphicon glyphicon-ok"</span>
                        <% else %>  
                             <%= form_for(current_user, :as => '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>
                                <div class="form-group">
                                    <div class="controls">  
                                        <%= f.input :password,  hint: ("#{@minimum_password_length} characters minimum" if @validatable), :input_html => { class: 'estimate-password'} %>
                                    </div>
                                </div>      
                            <% end %>
                            <div class="actions">
                                <%= f.submit 'Continue', :class => 'btn btn-primary' %>
                            </div>
                        <% end %>


                      </td>
                    </tr>
                </table>
            </div>  
        </div>
   </div>
</div>    

routes

devise_for :users, #class_name: 'FormUser',
             :controllers => {
                :registrations => "users/registrations",
                # :omniauth_callbacks => "users/authentications"
                :omniauth_callbacks => 'users/omniauth_callbacks'
           }


  # PER SOURCEY TUTORIAL ----------
  match '/users/:id/finish_signup' => 'users#finish_signup', via: [:get, :patch], :as => :finish_signup

None of this works. I don't know how to plug it in. I'm not sure whether I'm supposed to include the attributes stored in my identities table in the permitted params in the controller??

The attributes are:

t.integer  "user_id"
    t.string   "provider"
    t.string   "accesstoken"
    t.string   "refreshtoken"
    t.string   "uid"
    t.string   "name"
    t.string   "email"
    t.string   "nickname"
    t.string   "image"
    t.string   "phone"
    t.string   "urls"

I have got this working so a user can authenticate with one method only. I don't know how to get this working. I've tried every resource I can find to figure this out but I'm stuck.

I have this all working with each individual social plugin and email, but what I don't have is the ability to add identities to an existing user (in a current session) so that the next time they login they can use any acceptable identity.

Can anyone help?

Mel
  • 2,481
  • 26
  • 113
  • 273
  • 1
    Did you see [the doc](https://github.com/plataformatec/devise/wiki/OmniAuth-with-multiple-models)? Because your code doesn't follow it. [This tut](https://rlafranchi.github.io/2015/03/25/multiple-social-identities-rails-project/) seems to be compatible with the doc. – rdupz Apr 15 '16 at 10:00
  • This question would require an extremely large answer for which I have no time... But maybe you should check this repo of mine: https://github.com/guilhermesimoes/omniauth-popup. It doesn't use devise but maybe if you read the code you'll understand how to integrate it with devise (it's perfectly doable, I've done it in the past). – Ashitaka Apr 15 '16 at 16:50
  • Thanks both. Yes - I have read the docs (many times). The omniauth docs are full of errors, which I have highlighted in the notes to my code. Thanks for the new tutorial link and for the repo. I'll try and figure them both out today. – Mel Apr 15 '16 at 22:32
  • @Ashitaka - I've been through your repo. Thanks for the suggestion, but I can't see how it solves my problem (or any of them). I can't see how you have handled a user having a Facebook account authentication the first time and a twitter authentication the 2nd time, each of which takes them to the same account. For me, I require email authentication as part of the sign up - regardless of which method the register with (so that I can verify their source organisation). I then allow them to store each of their social media accounts to their user account. At least, that's what I'm trying to achieve – Mel Apr 15 '16 at 23:19
  • @standelaune - not sure what you're seeing in the Omniauth doc you linked that is relevant. I'm only trying to use omniauth on one model (user). In any event, is the deviation from that file that you're thinking about the inclusion of 'omniauthable' in my user model? Why is removing it relevant when I only use it on one model (user)? – Mel Apr 15 '16 at 23:23
  • @standelaune I tried the tut you suggested. Thanks for suggesting it but I can't figure out how to fill in the gaps, including how to associate other social media accounts as identities in a users account. Thanks anyway, but I can't see where in that tutorial this problem is solved. – Mel Apr 16 '16 at 01:11
  • Here is how I understand. The docs say you can't do what you want with the devise gem, because the omniauth support included in devise does not allow you to manage multiple social account. Basically devise enhance your User model with new fields as `provider`, which is not a list of providers but a single one. So you need to : 1) Rely on Omniauth only. 2) Associate each user with multiple Identity model. An Identity corresponds to a provider. The links I provided do not fully solve the problem; you need some extra work. This is why its only a comment and not an answer ;). – rdupz Apr 16 '16 at 10:32
  • @standelaune. Thanks for bearing with me while I try to understand your thought. Do you think devise does not support multiple social media providers for social authentication? I don't think that's right (see this devise doc): https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview. "Here we'll use Facebook as an example, but you are free to use whatever and as many OmniAuth gems as you'd like." Thanks anyway for the thoughts. – Mel Apr 16 '16 at 21:57
  • You can read my answer to similar question [here](http://stackoverflow.com/questions/21249749/rails-4-devise-omniauth-with-multiple-providers/22186061#22186061) – Alex Tonkonozhenko Apr 17 '16 at 23:52
  • Hi Alex, that question is actually one I asked. I tried your method and couldn't get it to work. You can see from my question history, I have asked more than 50 questions in trying to solve this problem (most of them with bounties. But I'm not finding help to solve this. Thanks anyway for the suggestion. – Mel Apr 17 '16 at 23:56
  • Where are you providing the keys that are required in the initializer? Here is a full tutorial I found that implements facebook, twitter, and linkedin: http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/ There are also a lot of comments on the bottom helping people work through some issues they had. – trueinViso Apr 18 '16 at 19:00
  • Yep - I tried the sourcey tutorial. I'm putting the keys in config.omniauth in devise.rb. They are then defined in my application.yml. The source tutorial is the closest thing I can find to help. I'm stuck at the part where I add identities to user accounts. – Mel Apr 18 '16 at 23:46
  • I created a project 2 years ago allowing multiple social media logins (Devise, OmniAuth2 with Facebook, Twitter, and Google+) but not all connected to same profile (Github Link: https://github.com/ltfschoen/littlehumans). Have you tried polymorphic associations between the Devise User and each Socia Media provider? – Luke Schoen Apr 19 '16 at 07:35
  • Hi - no I have a one to many relationship between my user and identity models. – Mel Apr 20 '16 at 02:18

1 Answers1

3

Without being able to see all of your code, I just created a shell app that runs with multiple providers. I just followed the steps from the tutorial you mentioned at sourcey. Here is the link to my repo.

You should be able to just clone it and run it by entering your app's keys and secret tokens from facebook, twitter, and linkedin in the devise.rb initializer. To get this to work locally you will need to make sure that the callback url on twitter is set to http://127.0.0.1:3000/.

If you wanted to give a user the option to add their own omniauth account (identity) instead of having it done automatically through app authorization you could just make a form for the user to enter the numeric uid and create the identity your self in the controller or back end like this:

new_identity = Identity.new
new_identity.user_id = "current user's id"
new_identity.provider = "facebook"
new_identity.uid = "0123456789"
new_identity.save!

The user would have to get their numeric uid from the site and enter it themselves.

trueinViso
  • 1,354
  • 3
  • 18
  • 30
  • thanks so much for taking the time to try to help. I can't see from your repo where you add identities to a user's account. I am trying to use this so that when a user is in a session, they can add other identities to their account. I can't see how your code does this? – Mel Apr 22 '16 at 01:16
  • I don't understand the last paragraph of your answer. I think it might have something to do with the point Im trying to resolve though. When a user is logged in, they should be able to add new identities to their existing user account. I can't figure out how to do that. I want them to go through the same authorisations in the social media apps as they would if they were registering as a new user with those channels, just that they are added to an existing user id, once confirmed. – Mel Apr 22 '16 at 04:23
  • Hi - I don't want the user to get their uid themselves, I want to write something to match the new authentication identity to the user account.Wouldnt the users current session give a uid that could be used? – Mel Apr 22 '16 at 04:37
  • You can see in the Users model that the `find_for_oauth` method is already checking if the user exists and if it does it associates the new identity with it. The email on the new account has to be the same as the one they register with on the site. – trueinViso Apr 22 '16 at 16:36
  • Also in the tutorial on sourcey it says: Therefore, to link accounts with multiple providers the current_user session must be already set when the OAuth callback returns, and passed to `User.find_for_oauth`. This might sound complicated, but all thats required to link a different provider, Facebook for example, is to `redirect_to user_omniauth_authorize_path(:facebook)` while the user is already logged in. So this would take care of accounts with different emails. – trueinViso Apr 22 '16 at 16:41
  • This solution actually already does this if the user is signed in. I signed in with an existing user that has a twitter identity. I then visited this link `http://localhost:3000/my/users/auth/linkedin` and it associated my linked in to the same user even though they have different emails. – trueinViso Apr 22 '16 at 16:54
  • how did you set this part up? – Mel Apr 22 '16 at 22:18
  • The omniauth_callbacks_controller calls `User.find_for_oauth(env["omniauth.auth"], current_user)`. When a user is signed in it will pass the current user. In User model `find_for_oauth` it will find or create an identity. If the user exists already it will associate the current user with the identity. – trueinViso Apr 22 '16 at 22:36
  • but do you make a new identities form, nested inside user, to add these additional authentication methods? I made users/authentications view (shown above) to try to do this. Have you tried something different? – Mel Apr 22 '16 at 23:12
  • I'm sure there are unlimited ways to achieve this but I would just have a few buttons that say "Add Facebook Login" or something like that and the url would be the omniauth links that devise generates in the `app/views/devise/shared/_links.html.erb` view. When a user visits those links when logged in it will automatically add that identity to their user id. – trueinViso Apr 23 '16 at 00:42
  • I think this is the part I'm struggling with. Ill post a new question to try to narrow down the scope of this problem – Mel Apr 23 '16 at 00:53
  • It might help to look at devise Wiki because it is doing some behind the scenes things that you don't really see. – trueinViso Apr 23 '16 at 02:37
  • I've been through the wiki so many times. I can't find which bit of it helps to show how to add additional authentication methods to an existing user's account. Please can you show me which link in the wiki is helpful? – Mel Apr 23 '16 at 05:37
  • The straight devise way is laid out [here](https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview). It does not use a identity model for multiple providers. This [digital ocean tutorial](https://www.digitalocean.com/community/tutorials/how-to-configure-devise-and-omniauth-for-your-rails-application) uses the devise wiki method and walks you through the process step by step and shows you that you just have to add separate methods for more omniauth providers. – trueinViso Apr 25 '16 at 18:42
  • thanks but neither of those articles address what I'm trying to achieve. I want to add multiple authentication strategies to a single user account (as distinct from providing a range of authentication methods - which allow a user to pick one as an authentication method). – Mel Apr 25 '16 at 22:46