[RUBY] I want to authenticate users to Rails with Devise + OmniAuth

What I want to do this time

Basic user authentication function using Devise

I want to use one-click registration for SNS authentication without going through a temporary registration email.

When editing user information by default in Devise, you will be asked for a password every time, but since it is not user-friendly, I want to edit user information without entering a password

Personal purpose of this article

memorandum. Keep the source code for this project on GitHub.

https://github.com/zizynonno/devise_omniauth

1 Introducing devise

1.1 Creating a project

Create a new project.

$ rails new devise_omniauth
$ cd devise_omniauth

1.2 Adding and installing Gemfile

Add the following gem to the Gemfile.

Gemfile


source 'https://rubygems.org'

(abridgement)...

# Devise
gem 'devise'
gem 'devise-i18n'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
gem 'dotenv-rails'

Gemfile


gem 'devise' #User authentication
gem 'devise-i18n' #devise i18n
gem 'omniauth-twitter' #twitter authentication
gem 'omniauth-facebook' #facebook authentication
gem 'dotenv-rails' #Setting environment variables

Install gem.

$ bundle install

2 devise settings

Added devise related files.

$ rails g devise:install

When you run this command, the terminal will tell you about devise settings in English. Let's run from 1 to 4.

** 2.1 Specify default URL **

config/environments/development.rb


Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  (abridgement)...

  # mailer setting
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
end

** 2.2 Specify root_url ** Specify the page to be displayed when accessing * http: // localhost: 3000 / * specified in No. 1. I haven't created any pages in this project, so I'll add them first.

Let's add a Pages controller and an index page and a show page.

$ rails g controller Pages index show

Specify the following in routes.rb.

config/routes.rb


Rails.application.routes.draw do
  root 'pages#index'
  get 'pages/show'
  (abridgement)...
end

** 2.3 Add flash message ** When you log in, a message like "You have logged in" will appear at the top. Inserts the specified tag just below the <body> tag in the following file.

erb:app/views/layouts/application.html.erb


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

  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
 </head>
 <body>
  <%#from here%>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
  <%#So far%>
                                                                                                                
  <%= yield %>

 </body> 
</html>

** 2.4 Generate Devise View **

$ rails g devise:views

Then, the following file will be generated.

app/views/devise/shared/_links.html.erb (Partial for link)
app/views/devise/confirmations/new.html.erb (Authentication email resend screen)
app/views/devise/passwords/edit.html.erb (Password change screen)
app/views/devise/passwords/new.html.erb (Screen to send an email when you forget your password)
app/views/devise/registrations/edit.html.erb (User information change screen)
app/views/devise/registrations/new.html.erb (User registration screen)
app/views/devise/sessions/new.html.erb (Login screen)
app/views/devise/unlocks/new.html.erb (Unlock email resend screen)
app/views/devise/mailer/confirmation_instructions.html.erb (Email account verification statement)
app/views/devise/mailer/password_change.html.erb (password change completion statement for email)
app/views/devise/mailer/reset_password_instructions.html.erb (Email password reset statement)
app/views/devise/mailer/unlock_instructions.html.erb (Unlock text for email)

3 User model settings

3.1 Creating a User model

$ rails g devise User

When you execute, the migration file and user file will be created.

db/migrate/20200912194315_devise_create_users.rb


class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      # ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      # ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      # ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end
  end
end

app/models/user.rb


class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

3.2 Editing migration file and User model

I will comment out only those that use this.

db/migrate/20200912194315_devise_create_users.rb


class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      t.string   :unlock_token # Only if unlock strategy is :email or :both
      t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    add_index :users, :unlock_token,         unique: true
  end
end

In addition to what you put in the migration file, add ʻomniauth_providers: [: twitter ,: facebook] `to perform OAuth authentication.

app/models/user.rb


class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable, :omniauthable, omniauth_providers: [:twitter,:facebook]
end

3.3 Addition of column for omniauth

Then add provider, ʻuid, ʻusername used in omniauth-twitter, omniauth-facebook to the User table.

$ rails g migration add_columns_to_users provider uid username

The following migration file will be created.

db/migrate/20200912194427_add_columns_to_users.rb


class AddColumnsToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_column :users, :username, :string
  end
end

Once you have done this, do the following:

$ rake db:migrate

4 Authenticate with Twitter and facebook

4.1 Obtain API key and secret key for Twitter authentication and Facebook authentication

Facebook

Create an application from the following.

When the creation is completed, select "Add Platform"-> "Website" from the settings. Enter the URL in the site URL (eg http: // localhost: 3000).

Twitter

Create an application from the following.

After the creation is completed, make the following settings from "Settings".

  1. Callback URL --Example: http: // ~ / users / auth / twitter
  2. Check the following:

4.2 Editing the configuration file

Copy and paste each API key and secret key to the corresponding places below.

config/initializers/devise.rb


Devise.setup do |config|
  # The secret key used by Devise. Devise uses this key to generate
  (abridgement)...
  config.omniauth :facebook, 'Enter your App ID', 'Enter App Secret' #I will correct it soon
  config.omniauth :twitter, 'Enter API key', 'Enter API secret' #Don't commit to GitHub as we'll fix it soon
end

4.3 Implemented callback processing in User controller

You need to define a method with the same name as provider. However, since the callback processing is basically common to each provider, it is unified to the callback_from method.

$ rails generate devise:controllers users

app/controllers/users/omniauth_callbacks_controller.rb


class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    callback_from :facebook
  end

  def twitter
    callback_from :twitter
  end

  private

  def callback_from(provider)
    provider = provider.to_s

    @user = User.find_for_oauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end
end

4.4 Routing process

Set the routing for OAuth callback as follows.

config/routes.rb


Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

  # ...
end

5 Private API key and secret key

We have obtained each API key and secret key for Twitter authentication and Facebook authentication. This information is extremely confidential and should never be leaked to the outside. Since there is a risk of misuse, it is necessary to take measures to prevent the API key and secret key from being accidentally released to the public in the remote repository of GitHub or the production environment.

Since we have bundle install`` gem'dotenv-rails' to set environment variables at the beginning, we will learn how to use the API key and secret key using dotenv-rails.

** 5.1 Installation of .env file ** Next, place the .env file that defines the environment variables in the ** project root of the app **. The .env file is not automatically generated even if you bundle install`` dotenv-rails, so you need to manually create the file using the touch command as shown below.

$ touch .env 

** 5.2 Put the following in the .env file **

.env


TWITTER_API_KEY="Obtained Twitter API key"
TWITTER_SECRET_KEY="Obtained Twitter secret key"
FACEBOOK_API_ID="Obtained Facebook API key"
FACEBOOK_API_SECRET="Obtained Facebook secret key"

** 5.3 Use environment variables ** Assign environment variables to the hard-coded file using a description method such as ʻENV ['SECRET_KEY']`.

config/initializers/devise.rb


Devise.setup do |config|
  # The secret key used by Devise. Devise uses this key to generate
  (abridgement)...
  config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET_KEY']
  config.omniauth :facebook, ENV['FACEBOOK_API_ID'], ENV['FACEBOOK_API_SECRET']
end

6 Add methods to the user model

Create ** self.from_omniauth ** and ** self.new_with_session ** in the User model. In self.from_omniauth, if you search by uid and provider, it will be searched, and if not, a record will be created. For self.new_with_session, if you do not add this method, even if you register on the sign-up page after Twitter authentication, the uid and provider etc. that you got as authentication information will not be registered. Since they are not registered, even if you authenticate with Twitter, you will be skipped to the sign-up page every time as an unregistered user.

app/models/user.rb


class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable, :omniauthable, omniauth_providers: [:twitter]

  def self.from_omniauth(auth)
    find_or_create_by(provider: auth["provider"], uid: auth["uid"]) do |user|
      user.provider = auth["provider"]
      user.uid = auth["uid"]
      user.username = auth["info"]["nickname"]
    end
  end

  def self.new_with_session(params, session)
    if session["devise.user_attributes"]
      new(session["devise.user_attributes"]) do |user|
        user.attributes = params
      end
    else
      super
    end
  end
end

6.1 Implement find method in User model

The combination of ʻuidandprovider` is unique, which gets the user. If it does not exist in the record, create it.

app/models/user.rb


class User < ActiveRecord::Base
  # ...

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid:      auth.uid,
        provider: auth.provider,
        email:    User.dummy_email(auth),
        password: Devise.friendly_token[0, 20]
      )
    end
    user.skip_confirmation! #Immediate registration without going through a temporary registration email
    user
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end

If you also implement email address authentication, you need to save your email address when authenticating with OAuth. Here, taking advantage of the unique combination of ʻuidandprovider, it is generated as self.dummy_email`.

Edit the following file so that the controller you created earlier is called as the controller for the callback. If you do not write this, the controller on the devise side will be called.

config/routes.rb


Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
  root 'pages#index'
  get 'pages/show'
  (abridgement)...
end

You can now authenticate with Twitter. When you authenticate with Twitter for the first time, you will be taken to the sign-up page, where you can enter your email address and password to register and your user information will be registered. Since I have included the confirmable function this time, I get a message saying that I sent a confirmation message after registering, and I can not log in as it is. If you do not include this function, you will be logged in immediately after registering.

7 I want to be able to register immediately for SNS authentication without going through a temporary registration email.

app/model/user.rb


class User < ActiveRecord::Base
  # ...

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid:      auth.uid,
        provider: auth.provider,
        email:    User.dummy_email(auth),
        password: Devise.friendly_token[0, 20]
      )
    end
######Add this!######
user.skip_confirmation!
#######################
    user
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end

8 I'm tired of being asked for a password every time I edit user information

8.1 Modify routes.rb

routes.rb


devise_for :users, controllers: {  }

Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', 
                                         registrations: 'users/registrations' }
  root 'pages#index'
  get 'pages/show'
  (abridgement)...
end

8.2 Override the update_resource method

registrations_controller.rb


class RegistrationsController < Devise::RegistrationsController

  protected
  #Append
  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

8.3 delete the current_password form

erb:views/devise/registrations/edit.html.erb


<div class="field">
    <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
    <%= f.password_field :current_password, autocomplete: "current-password" %>
</div>

Let's delete this form.

Now you can enter your user registration information without having to enter your password It is now possible to edit!

9 Bibliography of what you want to do

** In addition to the basic user authentication function, build a mechanism that allows one-click registration with SNS authentication. ** ** [* Rails *] How to use devise (rails5 version) Procedure to implement user authentication with Devise + OmniAuth in Rails

** I want to use environment variables to hide the API key and push it remotely (I don't want to hard-code App Secret etc.) ** [Rails] Let's manage environment variables by understanding how to install and use dotenv-rails! Environment variable settings

** I want to bring the email registered on twitter or facebook instead of the dummy (it takes about 3 days) ** Omniauth-Get email information on twitter Try using twitter oauth (also get email) Try using oauth on facebook (also get email) Problems and countermeasures for not being able to get an email address with Facebook authentication after July 9, 2015

** I want to be able to register immediately without using a temporary registration email for authentication using external sites such as Twitter, Google, and Github ** Records are not stored even if omniauth twitter authentication is completed in Devise Skip email authentication and email transmission when logging in to devise's Twitter [Rails5] Implementation that allows you to register and log in without using an email address with SNS authentication

** I want to translate devise into Japanese with i18n ** Japanese localization with i18n

** I'm tired of being asked for a password every time I edit user information ** [Devise] Edit user information without entering password

** Register as a user using only your email address. ** ** How to register as a user with devise only with an email address and set a password later How To: Email only sign up

** How to sign in with a different email address when signing in ** How To: Allow users to sign in with something other than their email address

** A method that does not require confirmation when updating an email address (I want to skip it) ** Do you need to check for email address updates with devise? Skip email address verification in Devise

** Other ** [Rails] What to do if "Unauthorized 403 Forbidden" appears in devise's Twitter authentication devise documentation

Recommended Posts

I want to authenticate users to Rails with Devise + OmniAuth
I want to play with Firestore from Rails
[Rails] I want to load CSS with webpacker
I want to push an app made with Rails 6 to GitHub
I was addicted to setting default_url_options with Rails devise introduction
I want to manually send an authorization email with Devise
I want to convert an array to Active Record Relation with Rails
I want to add a browsing function with ruby on rails
I want to use DBViewer with Eclipse 2018-12! !!
I want to add devise in Rails, but I can't bundle install
[Rails] I want to add data to Params when transitioning with link_to
I want to introduce the committee with Rails without getting too dirty
I want to use java8 forEach with index
I want to perform aggregation processing with spring-batch
Handle devise with Rails
I want to be able to read a file using refile with administrate [rails6]
Rails6 I want to make an array of values with a check box
[Rails] I want to test with RSpec. We support your step [Introduction procedure]
I want to use a little icon in Rails
I want to dark mode with the SWT app
I want to monitor a specific file with WatchService
I want to define a function in Rails Console
I want to transition screens with kotlin and java!
I want to get along with Map [Java beginner]
I want to redirect sound from Ubuntu with xrdp
I want to hit the API with Rails on multiple docker-composes set up locally
I want to convert characters ...
[Rails] How to install devise
[Rails] Add column to devise
[Rails] [bootstrap] I want to change the font size responsively
[Rails] I tried to create a mini app with FullCalendar
I want to make a list with kotlin and java!
I want to make a function with kotlin and java!
I want to create a form to select the [Rails] category
[Rails] I want to display "XX minutes ago" using created_at!
[Rails] I tried to implement batch processing with Rake task
Even in Java, I want to output true with a == 1 && a == 2 && a == 3
I want to distinct the duplicated data with has_many through
I want to implement various functions with kotlin and java!
I want to pass the startup command to postgres with docker-compose.
[Java] I want to test standard input & standard output with JUnit
After posting an article with Rails Simple Calendar, I want to reflect it in the calendar.
Swift: I want to chain arrays
[Rails] How to use gem "devise"
[Rails] How to use devise (Note)
I want to make a button with a line break with link_to [Note]
I want to connect SONY headphones WH-1000XM4 with LDAC on ubuntu 20.04! !!
I want to hook log file generation / open with log4j # FileAppender
I want to use FormObject well
I want to return to the previous screen with kotlin and java!
I want to INSERT Spring Local Time with MySQL Time (also milliseconds)
I want to avoid OutOfMemory when outputting large files with POI
I tried to implement the image preview function with Rails / jQuery
I want to operate cron with GUI, so I will install Dkron
I want to convert InputStream to String
I tried to interact with Java
I want to docker-compose up Next.js!
What I was addicted to when implementing google authentication with rails
[Rails] Add strong parameters to devise
Connect to Rails server with iPhone
How to get along with Rails