Thiết lập gem Devise và OmniAuth trên ứng dụng Rails

Nguồn : http://willschenk.com/setting-up-devise-with-twitter-and-facebook-and-other-omniauth-schemes-without-email-addresses/

Demo with facebook login: https://github.com/duongichi/study06

Bài viết này sẽ hướng dẫn các bạn có thể thiết lập chức năng sign up bằng mạng xã hội vào website.

Cài gem devise và omniauth

Trước hết mình cần phải cài đặt 2 gem là devise và omniauth.

Gemfile

gem 'devise', '~> 3.4'
gem 'omniauth'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
gem 'omniauth-instagram'
gem 'twitter'
gem 'instagram'
gem 'omniauth-google-oauth2'
gem 'google-api-client', require: 'google/api_client'

Tiếp đến bạn cài đặt devise

$ rails generate devise:install

Sau đấy là tạo model User và cấu hình devise route để sử dụng model này

$ rails generate devise User

Tiếp tục là tạo file views

$ rails generate devise:view

Devise

Test thử chức năng login của Devise

Tạo controller cở bản để check xem có login được ko

$ rails g controller welcome index
$ rake db:migrate
$ rails s

Chỉnh file routes.rb :

get 'welcome/index'
root 'welcome#index'

Tiếp tục là thay đổi WeclomeController để yêu cầu user authentication:

class WelcomeController
  before_action :authenticate_user!

  def index
  end
end

Bạn đừng quên button logout : app/views/welcome/index.html.erb

<%= link_to "Signout", destroy_user_session_path, method: :delete %>

Cấu hình Omniauth###

Đầu tiên mình cần phải config service của omniauth như sau

    config.omniauth :google_oauth2, ENV['GOOGLE_OAUTH2_APP_ID'], ENV['GOOGLE_OAUTH2_APP_SECRET'], scope: "email,profile,offline", prompt: "consent"
    config.omniauth :instagram, ENV['INSTAGRAM_APP_ID'], ENV['INSTAGRAM_APP_SECRET']
    config.omniauth :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], scope: "email"
    config.omniauth :twitter, ENV['TWITTER_APP_ID'], ENV['TWITTER_APP_SECRET']

Bạn cần phải cho các giá trị appidsecret key vào trong biến môi trường của server. Ở đây thì giá trị truyền vào trong scope sẽ là những giá trị mà mình lấy về được.

Kết nối Devise với omniauthable

Mở app/models/user.rb và add :omniauthable vào devise và bỏ đi :validatable:

 devise :omniauthable, :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable

Đây là list những cái sẽ connect với service của ban khi bạn ở trang sign in or login pages.

Tạo FormUser để xử lý validations

Không phải tất cả các service đều return lại email, nhưng mà devise validations lại yêu cầu dạng email. Vì vậy nên mình sẽ di chuyển validations từ class User vào FormUser. ý

  1. bỏ :validatable khỏi app/models/user.rb
  2. Config devise sử dụng model mới của mình
  3. Tạo class forms_user.rb

config/routes.rb

devise_for :users, class_name: 'FormUser'

app/models/form_user.rb

class FormUser < User
  attr_accessor :current_password

  validates_presence_of   :email, if: :email_required?
  validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
  validates_format_of     :email, with: Devise.email_regexp, allow_blank: true, if: :email_changed?

  validates_presence_of     :password, if: :password_required?
  validates_confirmation_of :password, if: :password_required?
  validates_length_of       :password, within: Devise.password_length, allow_blank: true

  def password_required?
    return false if email.blank?
    !persisted? || !password.nil? || !password_confirmation.nil?
  end

  def email_required?
    true
  end
end

Tạo model Identity để lưu access_keys và metadata

Flow để kết nối với oauth authentications:

  1. User sẽ yêu cầu /users/auth/:provider, provider sẽ là 1 trong số những giá trị mình truyền vào trong scope.
  2. Omniauth chuyển hướng đến remote service.
  3. User có thể access được và redirect về callback path
  4. Gọi controller OmniauthCallbacks để lấy về những thông tin tương ứng.

Những thông tin này sẽ được sử dụng để tạo user và truy cập. Ngoài ra thì mình cũng cần thiết phải lưu access_token để có thể access vào service.

Đối với trường hợp Google thì phức tạp hơn 1 chút và bạn cần phải lưu cả refresh_token nữa.

$ rails generate model identity user:references provider:string accesstoken:string refreshtoken:string uid:string name:string email:string nickname:string image:string phone:string urls:string

app/models/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)
    identity = find_by(provider: auth.provider, uid: auth.uid)
    identity = create(uid: auth.uid, provider: auth.provider) if identity.nil?
    identity.accesstoken = auth.credentials.token
    identity.refreshtoken = auth.credentials.refresh_token
    identity.name = auth.info.name
    identity.email = auth.info.email
    identity.nickname = auth.info.nickname
    identity.image = auth.info.image
    identity.phone = auth.info.phone
    identity.urls = (auth.info.urls || "").to_json
    identity.save
    identity
  end
end

Tiếp tục, mình cẩn phải ra lệnh cho devise sử dụng model này.

Tạo OmniauthCallbacksController để kéo data về

Chúng ta sẽ tạo 1 method để xử lý những authentication khác nhau khi callbacks, được gọi là generic_callback. Logic của controller này sẽ như sau :

  1. Tạo object Identity cho những oauth data. Update với những thông tin mới nhất.
  2. Nếu không có user nào liên kết với Identity thì liên kết nó với current_user.
  3. Nếu không có current_user thì tạo mới object User.
  4. Nếu object current_user không có email và mình get được 1 cái mail từ remote service thì sẽ set email đó cho nó.
  5. Log in user đó. !!!

Route cho controller mới routes.rb.

devise_for :users, class_name: 'FormUser', :controllers => { omniauth_callbacks: 'omniauth_callbacks' }

Tạo app/controllers/omniauth_callback_controller.rb:

class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def instagram
    generic_callback( 'instagram' )
  end

  def facebook
    generic_callback( 'facebook' )
  end

  def twitter
    generic_callback( 'twitter' )
  end

  def google_oauth2
    generic_callback( 'google_oauth2' )
  end

  def generic_callback( provider )
    @identity = Identity.find_for_oauth env["omniauth.auth"]

    @user = @identity.user || current_user
    if @user.nil?
      @user = User.create( email: @identity.email || "" )
      @identity.update_attribute( :user_id, @user.id )
    end

    if @user.email.blank? && @identity.email
      @user.update_attribute( :email, @identity.email)
    end

    if @user.persisted?
      @identity.update_attribute( :user_id, @user.id )
      # This is because we've created the user manually, and Device expects a
      # FormUser class (with the validations)
      @user = FormUser.find @user.id
      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

Override RegistrationsController để xử lý việc add thêm email address và password

Chúng ta sẽ add 1 email và thiết lập password cho user.

Đầu tiên phải thiết lập route cho devise hiểu về controller mới.

devise_for :users, class_name: 'FormUser', :controllers => { omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations'}

Tạo controller registrations

class RegistrationsController < Devise::RegistrationsController
  def update_resource(resource, params)
    if resource.encrypted_password.blank? # || params[:password].blank?
      resource.email = params[:email] if params[:email]
      if !params[:password].blank? && params[:password] == params[:password_confirmation]
        logger.info "Updating password"
        resource.password = params[:password]
        resource.save
      end
      if resource.valid?
        resource.update_without_password(params)
      end
    else
      resource.update_with_password(params)
    end
  end
end

Tạo các method cho User để liên kết tới clients

app/models/user.rb:

 has_many :identities

  def twitter
    identities.where( :provider => "twitter" ).first
  end

  def twitter_client
    @twitter_client ||= Twitter.client( access_token: twitter.accesstoken )
  end

  def facebook
    identities.where( :provider => "facebook" ).first
  end

  def facebook_client
    @facebook_client ||= Facebook.client( access_token: facebook.accesstoken )
  end

  def instagram
    identities.where( :provider => "instagram" ).first
  end

  def instagram_client
    @instagram_client ||= Instagram.client( access_token: instagram.accesstoken )
  end

  def google_oauth2
    identities.where( :provider => "google_oauth2" ).first
  end

  def google_oauth2_client
    if !@google_oauth2_client
      @google_oauth2_client = Google::APIClient.new(:application_name => 'HappySeed App', :application_version => "1.0.0" )
      @google_oauth2_client.authorization.update_token!({:access_token => google_oauth2.accesstoken, :refresh_token => google_oauth2.refreshtoken})
    end
    @google_oauth2_client
  end

Kêt luận###

Enjoy your new login function !!! (hihi)