📜 ⬆️ ⬇️

Log in via Facebook, Google, Twitter and Github using Omniauth

Puzzled once by the question of adding registration / logging into the site through third-party services, I began to look for what is already ready, or for descriptions like someone already did. Ready services were thrown away immediately, there was an option to implement yourself. And then Google brought on detailed instructions. After reviewing and inspired by that decision, I made my modification, everything worked, I was just happy.

After some time, I decided to see what else there is on that resource of interest, but to my disappointment the site was not available. Glory to the Yandex cache, from which a copy of the material was pulled out. And so that he does not disappear forever, he decided to make his translation and post it here.

And so let's get started ...
')
This chapter will focus on the famous Omniauth heme. Omniauth is a new identification system over Rack for multi-provider external authentication. It will be used to link CommunityGuides (note: at the moment the resource is not available and it looks like it will not return) with Facebook, Google, Twitter and Github. This chapter will show how to integrate all of this with existing identification through Devise.

Add a login through Facebook


Omniauth is an identification system over Rack for multi-provider external identification.
To begin, we will register our app on Facebook developers.facebook.com/setup . Specify a name (will be displayed to users) and a URL (for example, www.communityguides.eu ). Facebook allows redirection only to a registered site, for development you need to specify a different URL (for example http: // localhost: 3000 / ). Do not specify localhost or 127.0.0.1 in the URL; this will lead to an “invalid redirect_uri” error, which is quite common. Add the omniauth gems to your project, bundle install, create an initializer with your APP_ID / APP_SECRET and restart the server.

Gemfile
gem 'omniauth', '0.1.6' 

config / initializers / omniauth.rb
 Rails.application.config.middleware.use OmniAuth::Builder do provider :facebook, 'APP_ID', 'APP_SECRET' end 

Now we will create a new controller and model that will expand our user with various services and establish a connection between them.

Terminal
 rails generate model service user_id:integer provider:string uid:string uname:string uemail:string rails generate controller services 

app / models / user.rb
 class User < ActiveRecord::Base devise :database_authenticatable, :oauthable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable has_many :services, :dependent => :destroy has_many :articles, :dependent => :destroy has_many :comments, :dependent => :destroy has_many :ratings, :dependent => :destroy belongs_to :country attr_accessible :email, :password, :password_confirmation, :remember_me, :fullname, :shortbio, :weburl validates :weburl, :url => {:allow_blank => true}, :length => { :maximum => 50 } validates :fullname, :length => { :maximum => 40 } validates :shortbio, :length => { :maximum => 500 } end 

app / models / service.rb
 class Service < ActiveRecord::Base belongs_to :user attr_accessible :provider, :uid, :uname, :uemail end 

config / routes.rb
 ... match '/auth/facebook/callback' => 'services#create' resources :services, :only => [:index, :create] ... 

We defined new routes for services (for now only index and create) and added a so-called route for callback. What is it? We make a request for user authentication via http: // localhost: 3000 / auth / facebook . The request is sent to Facabook and then Facebook redirects the request to your page using the path / auth / facebook / callback. We mapped this path to our Services controller, in particular the create method. Now this method returns only the resulting hash.

app / controllers / services_controller.rb
 class ServicesController < ApplicationController def index end def create render :text => request.env["omniauth.auth"].to_yaml end end 

Let's check it out. Let's go to http: // localhost: 3000 / auth / facebook and then we will get to the request for access to your data on Facebook. We accept the offer and return to our application, which will display the data (see the source code of the page for normal formatting).

Page source
 --- user_info: name: Markus Proske urls: Facebook: http://www.facebook.com/profile.php?id=.... Website: nickname: profile.php?id=.... last_name: Proske first_name: Markus uid: "..." credentials: token: ........... extra: user_hash: name: Markus Proske timezone: 1 gender: male id: "...." last_name: Proske updated_time: 2010-11-18T13:43:01+0000 verified: true locale: en_US link: http://www.facebook.com/profile.php?id=........ email: markus.proske@gmail.com first_name: Markus provider: facebook 

We are only interested in the id, provider name and email fields located in extra: user_hash. For verification, replace the create method with the following code:

app / controllers / services_controller.rb
 ... def create omniauth = request.env['omniauth.auth'] if omniauth omniauth['extra']['user_hash']['email'] ? email = omniauth['extra']['user_hash']['email'] : email = '' omniauth['extra']['user_hash']['name'] ? name = omniauth['extra']['user_hash']['name'] : name = '' omniauth['extra']['user_hash']['id'] ? uid = omniauth['extra']['user_hash']['id'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' render :text => uid.to_s + " - " + name + " - " + email + " - " + provider else render :text => 'Error: Omniauth is empty' end end ... 

Great, we managed to authenticate the user through Facebook! There is still a lot to do, we integrate it into our scheme with Devise. There are a few points to look out for:

Omniauth provides the ability to add more services, as we will do. Our authentication is tied to the postal address, so only providers providing it can be used. For example, Github returns the address only if the user specified a public address. Twitter, on the other hand, never shows a mailing address. However, a Github account with an address can be used as an Fb login and registration, and Github without an address or Twitter accounts can be added to an existing local user, or created through another provider.
Each provider returns a hash containing various parameters. Unfortunately, this is not standardized at all and everyone can give different names to the same attributes. This means that we must distinguish the services in the create method. Also note that there is only one callback method. Therefore, what we have to do with the received data (enter or register) depends only on us. We will change our route again for all services, add a parameter to it, in which the name of the used one will be placed: params [: service].

config / routes.rb
 ... match '/auth/:service/callback' => 'services#create' resources :services, :only => [:index, :create, :destroy] ... 


Next, go to the pages for Github and Twitter. We register again at localhost (for Twitter, instead of localhost, you need to use 127.0.0.1). We will receive new routes http: // localhost: 3000 / auth / github / callback / and http://127.0.0.1 天000/auth/twitter/callback . Then change the initializer.


config / initializers / omniauth.rb
 # Do not forget to restart your server after changing this file Rails.application.config.middleware.use OmniAuth::Builder do provider :facebook, 'APP_ID', 'APP_SECRET' provider :twitter, 'CONSUMER_KEY', 'CONSUMER_SECRET' provider :github, 'CLIENT ID', 'SECRET' end 

The created method will check for the presence of a parameter from the path and the Omniauth hash. Further, depending on the authentication service, the necessary values ​​from the hash are transferred to our variables. At the very least, the service provider and the user ID for it must be defined, otherwise stop.
Part one: the user has not yet logged in: First, check to see if there is a provider-id pair in our Service model, which implies that this pair is associated with the user and can be used to log it. If so, then we make the entrance. If not, check the existence of the postal address. Using this address, we can find in the existing user model if it has already been registered with it. When such a user is found, this service will be added to him and in the future he will be able to use it for login. If this is a new email address, then instead create a new user, confirm it and add this authentication service to it.
Part two : if the user has already logged in: We simply add this service to his account if it has not been added before.
Let's look carefully at the Create method below. It contains all the necessary code to handle the various cases described above and provides identification for Facebook, Github and Twitter. Note that only 4 lines of code are needed to add a new provider. There is no interface for this yet, but you can check by clicking on the links yourself:

app / controllers / services_controller.rb
 class ServicesController < ApplicationController before_filter :authenticate_user!, :except => [:create] def index # get all authentication services assigned to the current user @services = current_user.services.all end def destroy # remove an authentication service linked to the current user @service = current_user.services.find(params[:id]) @service.destroy redirect_to services_path end def create # get the service parameter from the Rails router params[:service] ? service_route = params[:service] : service_route = 'no service (invalid callback)' # get the full hash from omniauth omniauth = request.env['omniauth.auth'] # continue only if hash and parameter exist if omniauth and params[:service] # map the returned hashes to our variables first - the hashes differ for every service if service_route == 'facebook' omniauth['extra']['user_hash']['email'] ? email = omniauth['extra']['user_hash']['email'] : email = '' omniauth['extra']['user_hash']['name'] ? name = omniauth['extra']['user_hash']['name'] : name = '' omniauth['extra']['user_hash']['id'] ? uid = omniauth['extra']['user_hash']['id'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' elsif service_route == 'github' omniauth['user_info']['email'] ? email = omniauth['user_info']['email'] : email = '' omniauth['user_info']['name'] ? name = omniauth['user_info']['name'] : name = '' omniauth['extra']['user_hash']['id'] ? uid = omniauth['extra']['user_hash']['id'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' elsif service_route == 'twitter' email = '' # Twitter API never returns the email address omniauth['user_info']['name'] ? name = omniauth['user_info']['name'] : name = '' omniauth['uid'] ? uid = omniauth['uid'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' else # we have an unrecognized service, just output the hash that has been returned render :text => omniauth.to_yaml #render :text => uid.to_s + " - " + name + " - " + email + " - " + provider return end # continue only if provider and uid exist if uid != '' and provider != '' # nobody can sign in twice, nobody can sign up while being signed in (this saves a lot of trouble) if !user_signed_in? # check if user has already signed in using this service provider and continue with sign in process if yes auth = Service.find_by_provider_and_uid(provider, uid) if auth flash[:notice] = 'Signed in successfully via ' + provider.capitalize + '.' sign_in_and_redirect(:user, auth.user) else # check if this user is already registered with this email address; get out if no email has been provided if email != '' # search for a user with this email address existinguser = User.find_by_email(email) if existinguser # map this new login method via a service provider to an existing account if the email address is the same existinguser.services.create(:provider => provider, :uid => uid, :uname => name, :uemail => email) flash[:notice] = 'Sign in via ' + provider.capitalize + ' has been added to your account ' + existinguser.email + '. Signed in successfully!' sign_in_and_redirect(:user, existinguser) else # let's create a new user: register this user and add this authentication method for this user name = name[0, 39] if name.length > 39 # otherwise our user validation will hit us # new user, set email, a random password and take the name from the authentication service user = User.new :email => email, :password => SecureRandom.hex(10), :fullname => name # add this authentication service to our new user user.services.build(:provider => provider, :uid => uid, :uname => name, :uemail => email) # do not send confirmation email, we directly save and confirm the new record user.skip_confirmation! user.save! user.confirm! # flash and sign in flash[:myinfo] = 'Your account on CommunityGuides has been created via ' + provider.capitalize + '. In your profile you can change your personal information and add a local password.' sign_in_and_redirect(:user, user) end else flash[:error] = service_route.capitalize + ' can not be used to sign-up on CommunityGuides as no valid email address has been provided. Please use another authentication provider or use local sign-up. If you already have an account, please sign-in and add ' + service_route.capitalize + ' from your profile.' redirect_to new_user_session_path end end else # the user is currently signed in # check if this service is already linked to his/her account, if not, add it auth = Service.find_by_provider_and_uid(provider, uid) if !auth current_user.services.create(:provider => provider, :uid => uid, :uname => name, :uemail => email) flash[:notice] = 'Sign in via ' + provider.capitalize + ' has been added to your account.' redirect_to services_path else flash[:notice] = service_route.capitalize + ' is already linked to your account.' redirect_to services_path end end else flash[:error] = service_route.capitalize + ' returned invalid data for the user id.' redirect_to new_user_session_path end else flash[:error] = 'Error while authenticating via ' + service_route.capitalize + '.' redirect_to new_user_session_path end end 


Our code is fully functional and right now you can use one local account and three services to log in or register. Despite the fact that entry and registration always follow the same path / auth / service and the callback always goes to / auth / service / callback.
Our example works fine, but there is a flaw that can lead to unwanted accounts: take a user with a local account (email: one@user.com) and a Facebook account (email: two@user.com) that is already linked to a local one. No problems, addresses do not match. If the user has a Google account with mail: three@user.com, then he can be linked without any problems while the session is active. On the other hand, suppose that a user has never linked a Google account and has not logged in yet: if he clicks on “log in through Google” our create method will search for three@user.com, will not find anything and will create a new user.
It's time to add a couple of views, let's start with logging and registration:

app / views / devise / sessions / new.html.erb
 <section id="deviseauth"> <h2>Sign in</h2> <h3>Sign in with your CommunityGuides account -- OR -- use an authentication service</h3> <div id="local" class="box"> <%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %> <p><%= f.label :email %><br /> <%= f.text_field :email %></p> <p><%= f.label :password %><br /> <%= f.password_field :password %></p> <% if devise_mapping.rememberable? %> <p><%= f.check_box :remember_me %> <%= f.label :remember_me %></p> <% end %> <p><%= f.submit "Sign in" %></p> <% end %> </div> <div id="remote"> <div id="terms" class="box"> <%= link_to "Terms of Service", "#" %> </div> <div id="services" class="box"> <a href="/auth/facebook" class="services"><%= image_tag "facebook_64.png", :size => "64x64", :alt => "Facebook" %>Facebook</a> <a href="/auth/google" class="services"><%= image_tag "google_64.png", :size => "64x64", :alt => "Google" %>Google</a> <a href="/auth/github" class="services"><%= image_tag "github_64.png", :size => "64x64", :alt => "Github" %>Github</a> <a href="/auth/twitter" class="services"><%= image_tag "twitter_64.png", :size => "64x64", :alt => "Twitter" %>Twitter</a> </div> </div> <div id="devise_links"> <%= render :partial => "devise/shared/links" %> </div> </section> 

app / views / users / registrations / new.html.erb
 <section id="deviseauth"> <h2>Sign up</h2> <h3>Sign up on CommunityGuides manually -- OR -- or use one of your existing accounts</h3> <div id="local2" class="box"> <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %> <%= devise_error_messages! %> <p><%= f.label :email %><br /> <%= f.text_field :email %></p> <p><%= f.label :password %><br /> <%= f.password_field :password %></p> <p><%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation %></p> <p><%= recaptcha_tags %></p> <p><%= f.submit "Sign up" %></p> <% end %> </div> <div id="remote2"> <div id="terms" class="box"> <%= link_to "Terms of Service", "#" %> </div> <div id="services" class="box"> <a href="/auth/facebook" class="services2"><%= image_tag "facebook_64.png", :size => "64x64", :alt => "Facebook" %>Facebook</a> <a href="/auth/google" class="services2"><%= image_tag "google_64.png", :size => "64x64", :alt => "Google" %>Google</a> <a href="/auth/github" class="services2"><%= image_tag "github_64.png", :size => "64x64", :alt => "Github" %>Github*</a> <div id="footnote_signup">* You can use Github only if you set a public email address</div> </div> </div> <div id="devise_links"> <%= render :partial => "devise/shared/links" %> </div> </section> 


You can download Github: Authbuttons images . Now our users can login or register through a convenient interface. In addition, we need a page with settings where users can manage accounts associated with the local one.

app / views / services / index.html.erb
 <section id="deviseauth"> <h2>Authentication Services - Setting</h2> <div id="currservices"> <h3>The following <%= @services.count == 1 ? 'account is' : 'accounts are' %> connected with your local account at CommunityGuides:</h3> <% @services.each do |service| %> <div class="services_used"> <%= image_tag "#{service.provider}_64.png", :size => "64x64" %> <div class = "user"> <div class="line1">Name: <%= service.uname %> (ID: <%= service.uid %>)</div> <div class="line2">Email: <%= service.uemail != '' ? service.uemail : 'not set' %></div> <div class="line3"> <% @services.count == 1 ? @msg = 'Removing the last account linked might lock you out of your account if you do not know the email/password sign-in of your local account!' : @msg = '' %> <%= link_to "Remove this service", service, :confirm => 'Are you sure you want to remove this authentication service? ' + @msg, :method => :delete, :class => "remove" %> </div> </div> </div> <% end %> </div> <div id="availableservices"> <h3>You can connect more services to your account:</h3> <div id="services"> <a href="/auth/facebook" class="services"><%= image_tag "facebook_64.png", :size => "64x64", :alt => "Facebook" %>Facebook</a> <a href="/auth/google" class="services"><%= image_tag "google_64.png", :size => "64x64", :alt => "Google" %>Google</a> <a href="/auth/github" class="services"><%= image_tag "github_64.png", :size => "64x64", :alt => "Github" %>Github</a> <a href="/auth/twitter" class="services"><%= image_tag "twitter_64.png", :size => "64x64", :alt => "Twitter" %>Twitter</a> </div> <h4>If you signed-up for CommunityGuides via an authentication service a random password has been set for the local password. You can request a new password using the "Forgot your Password?" link on the sign-in page.</h4> </div> </section> 


Adding Google

Finally, let's add Google to our list of service providers. Google (and OpenID in particular) require persistent storage. You can use ActiveRecord or file system as shown below. If you want to deploy to Heroku, remember that you do not have write access to / tmp. Although, as noted in Heroku Docs , you can write in ./tmp.

Two lines of configurations and four to assign values ​​from the hash is all you need to add authorization via Google in your code. Isn't that great? Omniauth is enough for today, but if you want to use it in one of your projects, you can find a lot of resources in the Omniauth Wiki , also Raina Bates made great screencasts on it.

Devise again

There is a small flaw in the profile of our users. The user needs to enter the current password to change the settings. If it is registered through one of the services, then it does not have a password, remember, we installed it into a random string. There is an article in the Devise Wiki with how to completely remove the password. But at home we want to leave the password only for local users. For other users, we will allow you to change your profile without using a password. In addition, they will be able to set a local password if they wish. This is achieved by modifying the update method for the registration controller:

app / controllers / users / registrations_controller.rb
 ... def update # no mass assignment for country_id, we do it manually # check for existence of the country in case a malicious user manipulates the params (fails silently) if params[resource_name][:country_id] resource.country_id = params[resource_name][:country_id] if Country.find_by_id(params[resource_name][:country_id]) end if current_user.haslocalpw super else # this account has been created with a random pw / the user is signed in via an omniauth service # if the user does not want to set a password we remove the params to prevent a validation error if params[resource_name][:password].blank? params[resource_name].delete(:password) params[resource_name].delete(:password_confirmation) if params[resource_name][:password_confirmation].blank? else # if the user wants to set a password we set haslocalpw for the future params[resource_name][:haslocalpw] = true end # this is copied over from the original devise controller, instead of update_with_password we use update_attributes if resource.update_attributes(params[resource_name]) set_flash_message :notice, :updated sign_in resource_name, resource redirect_to after_update_path_for(resource) else clean_up_passwords(resource) render_with_scope :edit end end end ... 


The code uses an additional field in the user model, you can return it and add it to the migration ( t.boolean: haslocalpw,: null => false,: default => true ), change the model to allow mass assignment for this field, change the view to hide field to enter current password if haslocalpw is false and change the create method of our service controller to set this field when creating a user:

app / controllers / services_controller.rb
 ... user = User.new :email => email, :password => SecureRandom.hex(10), :fullname => name, :haslocalpw => false ... 


PS: this is my first big translation, so please error / curves wording in lichku. Thank you very much.

Source: https://habr.com/ru/post/166325/


All Articles