Friday, April 22, 2016

Rails 4 - Devise Omniauth and allowing a single user to authenticate with multiple social media strategies

Leave a Comment

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?

1 Answers

Answers 1

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.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment