Логін через Facebook, Google та Email в Rails (частина 2)

Продовження. Першу частину дивіться за посиланням: http://rubyforce.io/rails-devise-part-1/

Ми одразу ж розділимо дані користувача та способи автентифікації. Для цього створимо нову модель Authentication:

rails g model authentication provider:string uid:string user_id:integer

app/models/authentication.rb

class Authentication < AplicationRecord
  belongs_to :user
end

Згенеруємо контролер:

rails g controller authentications

app/controllers/authentication_controller.rb

class AuthenticationsController < ApplicationController
  def destroy
    @authentication = current_user.authentications.find(params[:id])
    @authentication.destroy
    flash[:notice] = "Successfully destroyed authentication."
    redirect_to root_path
  end
end

Єдина дія яку ми будемо виконувати безпосередньо над автентифікацією – це видалення. Створення буде відбуватися у іншому контролері відповідальному за callback’и.

В модель User додаємо зв’язок з authentications. А також додаткові модуль omniauthable та провайдерів. Метод apply_omniauth будує відповідні authentications для користувача. А метод класу set_user повертає існуючого або створює нового користувача. Обидва методи ми використаємо у callbacks_controllers.rb.

app/models/user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  has_many :authentications, dependent: :destroy

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :omniauthable, omniauth_providers: [:facebook, :google_oauth2]

  def apply_omniauth(omniauth)
    authentications.build(provider: omniauth['provider'], uid: omniauth['uid'])
  end

  def self.set_user(omniauth)
    User.find_by(email: omniauth.info.email) || User.new(email: omniauth.info.email, password: Devise.friendly_token[0,20]) 
  end
end

Створюємо таблицю authentications:


rake db:migrate

Наступний крок – створити аплікації на Facebook та Google

Для цього реєструємось на developers.facebook.com та console.developers.google.com. Кожна аплікація надає нам ID та SECRET. Також важливо вказати Valid OAuth redirect URIs – це адреса нашого сайту http://localhost:3000/.

Зауваження: якщо ви плануєте розгортати свою аплікацію локально (так як в даному прикладі) то деактивуйте Use Strict Mode for Redirect URIs для Facebook.

Додаємо ID та SECRET:

config/initializers/devise.rb


...
# Facebook
config.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET']

# Google
config.omniauth :google_oauth2, ENV['GOOGLE_ID'], ENV['GOOGLE_SECRET']
...

Ключі зберігаємо за допомого dotenv:

.env


...
FACEBOOK_ID=ХХХХХХХХХХХХХХХХ
FACEBOOK_SECRET=ХХХХХХХХХХХХХХХХ
GOOGLE_ID=ХХХХХХХХХХХХХХХХ
GOOGLE_SECRET=ХХХХХХХХХХХХХХХХ
...

Тепер потрібно створити контролер, який буде обробляти callback’и після авторизації. Для кожного провайдера потрібен окремий метод. Важлива саме назва. А так як функціонал методів однаковий, то можна використати alias_method:

app/controllers/callbacks_controller.rb

class CallbacksController < Devise::OmniauthCallbacksController
  def all
    omniauth = request.env["omniauth.auth"]
    authentication = Authentication.find_by_provider_and_uid(omniauth['provider'], omniauth['uid'])

    if authentication
      flash[:notice] = "Signed in successfully."
      sign_in_and_redirect(:user, authentication.user)
    elsif current_user || User.exists?(email: omniauth.info.email)
      user = current_user || User.find_by_email(omniauth.info.email)
      user.authentications.create!(:provider => omniauth['provider'], :uid => omniauth['uid'])
      flash[:notice] = "Authentication successful."

      if current_user
        redirect_to root_path
      else
        sign_in_and_redirect(:user, user)
      end
    else
      user = User.set_user(omniauth)
      user.apply_omniauth(omniauth)

      if user.save
        flash[:notice] = "Signed in successfully."
        send_set_password_email(user)
        sign_in_and_redirect(:user, user)
      else
        cookies[:omniauth] = omniauth.except('extra')
        redirect_to new_user_registration_url
      end
    end
  end

  alias_method :facebook, :all
  alias_method :google_oauth2, :all

  private

  def send_set_password_email(user)
    user.send_reset_password_instructions
  end
end

Фактично, у цьому контролері міститься вся логіка нашої аплікації. Ми переверіяємо наявність автентифікацій та користувача і виконуємо відповідні дії по їх створенню та логінізації. Важливий момент – це надсилання листа новоствореному користувачу за допомогою send_set_password_email. В даному випадку ми використовуємо вже існуючий mailer Devise’у reset_password_instructions. Але можна також створити та налаштувати власний mailer.

І додаємо шляхи для даного контролера, а також для users та authentications:

config/routes.rb

Rails.application.routes.draw do
  root to: 'home#index'

  devise_for :users, controllers: { omniauth_callbacks: 'callbacks' }
  resources :users
  resources :authentications, only: [:destroy]
end

Налаштування для роботи mailer‘а:

config/environment/development.rb


...
config.action_mailer.perform_caching = false
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_options = { from: ENV['EMAIL_USERNME'] }
config.action_mailer.default_url_options = { host: "localhost:3000" }

config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
		address:              'smtp.gmail.com',
		port:                 587,
		domain:               'localhost',
		user_name:            ENV['EMAIL_USERNAME'],
		password:             ENV['EMAIL_PSWD'],
		authentication:       'plain',
		enable_starttls_auto: true  }
...

Тут доволі стандартні налаштування для Gmail. Знову ж таки ми використовуємо змінні оточення (ENV) для вразливих даних (адреса пошти та пароль).

.env


EMAIL_USERNAME=ХХХХХХХХХХХХХХХХ
EMAIL_PSWD=ХХХХХХХХХХХХХХХХ

На даний момент є одна проблема. Коли користувач перейде за посиланням в листі, то відбудеться редірект на головну сторінку. Це відбувається тому що користувач залогінений. Для того щоб пропустити перевірку, потрібно модифікувати passwords_controller.rb. Це один із контролерів Devise’у, в якому міститься відповідна логіка. Створюємо контролер:

app/controllers/passwords_controller.rb

class PasswordsController < Devise::PasswordsController
  skip_before_action :require_no_authentication, :only => [:edit, :update]

  def update
    super
    if resource.errors.empty?
      sign_out(resource_name)
      sign_in(resource_name, resource)
    end
  end
end

Тепер при реєстрації за допомогою аккаунту соціальних мереж, користувач отримає лист із можливістю встановити новий пароль, який можна буде використовувати для логіну.

P.S.

На головній сторінці відображається повідомлення про те, що користувач з певним email залогінився у певний спосіб:

app/views/home/index.html.erb


<p id="notice"><%= notice %></p>

<h1>Authentication with multiple social network accounts</h1>
<% if current_user %>
  <p>You are logged in as <mark><%= current_user.email %></mark> with <mark><%= @provider %></mark></p>
<% else %>
  <p>Please Log In</p>
<% end %>

У контролері визначена змінна @provider, яка може містити назву провайдера (facebook, google_oauth2) або email. Значення зберігається у cookies: app/controllers/home_controller.rb

class HomeController < ApplicationController
  def index
    @provider = cookies[:provider]
  end
end

Для того щоб мати коректне значення cookies[:provider] створюємо ініціалізатор, який буде спрацьовувати після логінізації та після logout’у користувача:

config/initializers/warden_hooks.rb

Warden::Manager.after_set_user except: :fetch do |user, auth, opts|
  if auth.request.env['omniauth.auth']
    auth.cookies[:provider] = auth.request.env['omniauth.auth'][:provider]
  else
    auth.cookies[:provider] = 'email'
  end
end

Warden::Manager.before_logout do |user,auth,opts|
  auth.cookies.delete :provider
end

Автор: Ілля Кузьма

Leave a Reply

Be the First to Comment!

Notify of
avatar
wpDiscuz