Skip to content

Instantly share code, notes, and snippets.

@illia108
Last active December 11, 2017 08:58
Show Gist options
  • Save illia108/f2b46f2be88e6efb3e44e913953b4e7b to your computer and use it in GitHub Desktop.
Save illia108/f2b46f2be88e6efb3e44e913953b4e7b to your computer and use it in GitHub Desktop.
How to set up rails app with Devise and social networks accounts Login

Логін через Facebook, Google та Email в Rails.

Велика кількість аплікацій потребує систему реєстрації та автентифікації користувачів. Це складне завдання. Так як йдеться про безпеку персональних даних. На щастя існує декілька хороших рішень для Rails фреймворку. Мабуть, найпоширенішим є Devise. Devise є досить простим у встановленні. Складність полягає у великій кількості можливих сценаріїв.

В залежності від потреб бізнесу можна організувати різні способи реєстрації. Якщо достаньо отримати пошту користувача, яка буде також виконувати роль логіну, і запитувати пару логін / пароль для входу, то стандартні налаштування Devise - це те що потрібно.

У цій статті ми розглянемо більш складний flow:

  1. Відвідувач має можливість обрати спосіб реєстрації: за допомогою аккаунтів facebook чи google, або через пошту.
  2. Після реєстрації через пошту користувач може під'єднати аккаунти соціальних мереж (facebook та/чи google)
  3. Після входу через один із аккаунтів соціальних мереж користувач отримує лист де має змогу перейти на сторінку встановлення паролю для входу.
  4. В подальшому користувач може від'єднати акаунти соціальних мереж та повністю видалити акаунт на сайті.

У статті ми крок за кроком пройдемо весь шлях реалізації даного завдання.

Посилання на репозиторій https://github.com/RubyForceTeam/authentication_solution_illia108.git

Створюємо нову rails аплікацію з базою даних PostgreSQL (у разі розгортання на heroku це полегшить роботу):

rails new auth --database=postgresql

cd auth

Додаємо необхідні бібліотеки:

Gemfile

gem 'devise'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem 'dotenv-rails', groups: [:development, :test]

Перші три gem'и потрібні безпосередньо для реєстрації та авторизації користувачів. Останній dotenv-rails - для збереження конфіденційної інформації. Ми повернемось до нього пізніше.

bundle

Створюємо базу даних:

rails db:create

Налаштовуємо Devise:

rails generate devise:install
rails generate devise User
rake db:migrate

В результаті отримуємо модель User, відповідну міграцію та шлях.

Devise потребує визначеної головної сторінки:

rails g controller home index

Згенерували новий контролер із методом index. Та встановлюємо цей метод на root_path:

config/routes.rb

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

Для зручності додамо мінімальну навігацію:

app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Auth</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= link_to 'Home', root_path %>
    <%= link_to 'Users', users_path %>
    <% if current_user %>
      <%= link_to('Logout', destroy_user_session_path, method: :delete) %>
    <% else %>
      <%= link_to('Login', new_user_session_path) %>
    <% end %>
    <%= yield %>
  </body>
</html>

Стартуємо сервер та перевіряємо результат:

rails s

Першу частину завершено. У нас є реєстрація користувачів за допомогою логіну та паролю.


Ми одразу ж розділимо дані користувача та способи автентифікації. Для цього створимо нову модель 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 Devisereset_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'a. Знову ж таки ми використовуємо змінні оточення (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
@otkachyk
Copy link

otkachyk commented Dec 8, 2017

"Велика кількість аплікацій потребують систему..." - можливо краще замінити "потребують" на "потребує";
"Нащастя існує декілька.." - на щастя;
"...перейти на сторінку встановлення пароль для..." - паролю;
"В резултаті отримуємо" - результаті;
"створює новго користувача" - нового
"Кожна аплікаціє надає нам" - аплікація
"Згенерували новий контроллер із методом" - контролер

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment