Help us understand the problem. What is going on with this article?

DeviseとOmniauthでtwitter,facebook ログイン機能

More than 1 year has passed since last update.

はじめに

自分が実装しているときに割とハマったので書きました。
deviseでuserモデルを作成するとこから始めます。

公式ドキュメント
github-devise-wiki OmniAuth: Overview

参考資料
Devise+OmniAuthでQiita風の複数プロバイダ認証-Qiita
Rails5.2から追加された credentials.yml.enc のキホン-Qiita
[Rails]gem "OmniAuth" の脆弱性対策-Qiita
secrets.ymlや環境変数をRails 5.2のEncrypted Credentialsに移行する

gem

以下のgemを追加して bundle install します。

gemfile
gem "devise"
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'

deviseのインストール

$ rails g devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

上の4つを行ってください。

userモデルの設定

userモデルを作成

$ rails g devise User
:models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable
# :omniauthableを追加してください。
end

bundle install します。

omniauth用のカラムをuserモデルに追加して行きます。↓
oauth認証時にproviderの名前(facebook,twitter)、uid(providerのユーザーID)がいるみたいなので追加します。
また、image,nameも取得したいので追加します。

$ rails g migration add_columns_to_users provider uid name image
$ rake db:migrate

Twitter DeveloperとFacebook Developerのアカウント作成

すいませんが以下のサイトを見てください。詳しく書いてあります。

Twitter Developer

Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2018年9月時点の情報-Qiita

Facebook Developer

【2019年】これで完璧!Facebook Developerに登録する方法を図解つきで説明

Developerでアプリを作成

アカウントを作成したら、Developerでアプリを作成してください。

Twitter Developer

Twitter Developerでは Callback URLに
https://127.0.0.1:3000/users/auth/twitter/callback(127.0.0.1:3000にはprodction用のurl)
http://localhost:3000/users/auth/twitter/callback

この2つを記入してください。

Facebook Developer

またこのサイトを見てもらって、
【2019年】これで完璧!Facebook Developerに登録する方法を図解つきで説明

ダッシュボード下の設定→ベーシック→ウェブサイトのサイトurlに
ローカルだけで試すのであれば
http://localhost:3000

ローカルと本番環境(heroku)
自分のサイトのurl

を記入してください。

本番環境では有効なOAuthリダイレクトURIの設定が必要
facebookログイン下の設定→有効なOAuthリダイレクトURIに
https://127.0.0.1:3000/users/auth/facebook/callback(127.0.0.1:3000にはprodction用のurl)

Oauthの設定

プロバイダーの宣言

config/initializers/devise.rbでプロバイダーのkeyとsecret_keyを設定します。

config/initializers/devise.rb
Devise.setup do |config|
  config.omniauth :facebook, 'App ID', 'App Secret'
  config.omniauth :twitter, 'API key', 'API secret'
end

私の場合ローカルで繋がったのが確認出来るまでは直接書いちゃってます。

注意としてここに書いたままで、 githubなどのリモートリポジトリーにあげないこと。
上げるときは、dotenvfigarocredentials.yml.encなどで管理してください。

userモデルにメソッドを追加

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

# このから下を追加--------------------------------------------------------------
  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.name = auth.name
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20] # ランダムなパスワードを作成
      user.image = auth.info.image.gsub("_normal","") if user.provider == "twitter"
      user.image = auth.info.image.gsub("picture","picture?type=large") if user.provider == "facebook"
    end
  end
end

このメソッドはproviderand uidフィールドで既存のユーザーを見つけようとします。ユーザーが見つからない場合は、ランダムなパスワードといくつかの追加情報を使用して新しいユーザーが作成されます。 後ろにくっついているfirst_or_createメソッドにそのような機能があるみたいです。

emailはdeviseでユーザーを登録する時に必ず、必要なので追加してます。

imageについてはそのまま登録してしまうと画像が荒くなってしまいます。
twitterに関してはgsubメソッドを使用してurlの"_normai"""に置き換えてます。
facebookに関してはgsubメッソドを使用してurlの"picture""picture?type=large"に置き換えてます。

また、carrierwaveを使ってアップローダーを挟みたい場合、画像の保存先をuser.imageからuser.remote_image_urlに変更したらアップローダー経由でimageを保存できます。(ローカルでしか試していません)

コールバックの設定

どのコントローラーでOmniauthコールバックを実装するかをDeviseに伝える必要があります。

config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

end

今回はcontrollers/users/omniauth_callbacksでコールバックを実装します。
なので次にusersの中にomniauth_callbacks コントローラーを作成します。

controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def facebook
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, kind: "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end

  def twitter
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, kind: "Twitter") if is_navigational_format?
    else
      session["devise.twitter_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

コールバックはプロバイダと同じ名前のアクションとして実装する必要があります。(今回はtwitterとfacebook)

ここでuserモデルで作成したself.from_omniauthメソッドを使って、引数にrequest.env["omniauth.auth"]をセットしてます。
request.env["omniauth.auth"]の中に受け取ったユーザーのデータが入ってるみたいです。

persisted?メソッドでデータベースに@userのデータがあるかないかで条件分岐してます。

リンクについて

ログインへのリンクはdeviseで勝手に生成されています。
追加した:omniauthableのおかげみたい。
<%= render "devise/shared/links" %>
このコードが書いてあるログインとサインアップ画面に出てると思います。

models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable # これです。
end

実際に出現しているリンクが以下のリンクです。

devise/shared/_links.html.erb
<%- if controller_name != 'sessions' %>
  <%= link_to "Log in", new_session_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
  <%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
  <%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
  <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
  <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
<% end %>

<!-- --------この部分のコードです。------------------------------------------- -->
<%- if devise_mapping.omniauthable? %>
  <%- resource_class.omniauth_providers.each do |provider| %>
    <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br />
  <% end %>
<% end %>
<!-- -------------ここまで----------------------------------------------------- -->

スクリーンショット 2019-07-08 8.32.45.png

適当で申し訳ないですが、こんな感じでリンクが出ます。
ここまで実装しますと、twitterとfacebookでログインが出来るようになります。

編集ページについて

deviseはデフォルトで編集時にパスワードが必要になります。
twitter、facebookでログインするとこの場合はパスワードをランダムで生成してログインしているため、パスワードを把握しておらず、編集が出来ないと思います。

そこで編集時にパスワードを入力をしないでいいように編集します。(ただし、パスワードの変更はそのページでは出来なくなります)

私もなんでこうなるのかとか曖昧ですのでこちらを読んでもらえると助かります。
参考リンク↓
github-devise-wiki How To: Allow users to edit their account without providing a password

ルートの作成

config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', registrations: 'registrations' }
# registrations: 'registrations' を追加してます。
  resources :users, only: [:index, :show]
end

コントローラーの作成

上のルートに合わせてコントローラーを作成します。(生身はサイトのコピーです)

controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController

  protected

  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

update_resourceメソッドをオーバーライドして、アップデート時のパスワード入力を取り除きました。
devise/registrations/edit.html.erbにcurrent_password(変更時に入力するパスワードのフォーム)が残っているため取り除きます。

devise/registrations/edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true, autocomplete: "name" %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :image %><br />
    <%= f.file_field :image %>
  </div>


  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

  <div class="field">
    <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
    <% if @minimum_password_length %>
      <br />
      <em><%= @minimum_password_length %> characters minimum</em>
    <% end %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

<!-- -----------この部分を取り除きます--------------------------------------------- -->
  <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>
<!-- -----------この部分を取り除きます--------------------------------------------- -->

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

<h3>Cancel my account</h3>

<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p>

<%= link_to "Back", :back %>

これで完了です。このフォームではパスワードは変更出来なくなっているので、そっちも消していいと思います。

キーとシークレットキーの管理について(credentials.yml.enc)

今回はcredentials.yml.encを使って行きます。
細かいところはこちらへ↓
Rails5.2から追加された credentials.yml.enc のキホン-Qiita

$ EDITOR="vi" bin/rails credentials:edit

EDITORを指定してください。今回はvimです。
以下の記述を追加します。ご自身のkeyとserect_keyを記述してください。

facebook:
  key: "App ID"
  secret: "App Secret"
twitter:
  key: "API key"
  secret: "API secret"

credentials.yml.encファイル自体の中身が暗号化されてしまうため、参照するには以下のコマンドで入力する必要があります。

$ rails credentials:show

また、値は以下のコマンドで参照出来ます。(コンソールで試してみます)

irb(main):001:0> Rails.application.credentials.facebook
=> {:key=>App ID, :secret=>"App Secret"}
irb(main):002:0> Rails.application.credentials.facebook[:key]
=> App ID
irb(main):003:0> 

上のコードを参考にconfig/initializers/devise.rbを書き換えます。

config/initializers/devise.rb
Devise.setup do |config|
  config.omniauth :facebook, Rails.application.credentials.facebook[:key],
                             Rails.application.credentials.facebook[:secret]

  config.omniauth :twitter, Rails.application.credentials.twitter[:key],
                            Rails.application.credentials.twitter[:secret]
end

これで完了です。問題なくログインできると思います。(ローカル環境)

Prodction環境の設定(heroku)

アクセス用のkeyとsecret_keyを渡してあげる必要があるります。
参考サイト(詳しくはこちら)↓
secrets.ymlや環境変数をRails 5.2のEncrypted Credentialsに移行する

$ heroku config:set RAILS_MASTER_KEY=`cat config/master.key`

git push heroku masterを行う前に実行してください。
先にpushするとエラーが出てしまいました。

"OmniAuth" の脆弱性対策

githubにpushするとomniauthの脆弱性を発見しましたとメールがきました。
対策として以下を参考にしました。↓
[Rails]gem "OmniAuth" の脆弱性対策-Qiita

gemの追加

gemfile
gem "omniauth-rails_csrf_protection"

リンクにメソッドを宣言する

リンクにmethod: :postを加えるだけです。

devise/shared/_links.html.erb
# 上記のコード省略

<!-- --------この部分のコードです。------------------------------------------- -->
<%- if devise_mapping.omniauthable? %>
  <%- resource_class.omniauth_providers.each do |provider| %>
    <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %><br />
  <% end %>
<% end %>
<!-- -------------ここまで----------------------------------------------------- -->

その他

twitterとfacebookのアカウントが1つしかなかったため、何度も消して試してました。
以下データベースのリセットのコマンドです。

development

$ rails db:migrate:reset

prodction(heroku)

$ heroku pg:reset DATABASE

このコマンドだともう一度heroku run rails db:migrateを行う必要があります。
多分もっといいコマンドがあるはず。

最後に

これでtwitterとfacebookでログインすることができます。
まだまだ、書いている途中なので、付け加えて行きます。

sakakinn
主にRailsについて記事を投稿しています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away