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

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

公式ドキュメント
[github-devise-wiki OmniAuth: Overview](https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview)

参考資料
[Devise+OmniAuthでQiita風の複数プロバイダ認証-Qiita](https://qiita.com/mnishiguchi/items/e15bbef61287f84b546e)
[Rails5.2から追加された credentials.yml.enc のキホン-Qiita](https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39)
[[Rails]gem "OmniAuth" の脆弱性対策-Qiita](https://qiita.com/NT90957869/items/2a3ce18dedf93ccf2bdc)
[secrets.ymlや環境変数をRails 5.2のEncrypted Credentialsに移行する](https://qiita.com/d-mato/items/aab2b6b2f587a8582631)

#gem

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

```rb: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

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

#userモデルの設定

userモデルを作成

```
$ rails g devise User
```
```rb
: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](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)

###Facebook Developer
[【2019年】これで完璧！Facebook Developerに登録する方法を図解つきで説明](https://pasolack.com/sns/facebook-developer-entry/)

#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`

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

###Facebook Developer
またこのサイトを見てもらって、
[【2019年】これで完璧！Facebook Developerに登録する方法を図解つきで説明](https://pasolack.com/sns/facebook-developer-entry/)

**ダッシュボード下の設定→ベーシック→ウェブサイトのサイト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を設定します。

```rb:config/initializers/devise.rb

Devise.setup do |config|
  config.omniauth :facebook, 'App ID', 'App Secret'
  config.omniauth :twitter, 'API key', 'API secret'
end
```
私の場合ローカルで繋がったのが確認出来るまでは直接書いちゃってます。

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

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

```rb: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に伝える必要があります。

```rb: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 コントローラーを作成します。

```rb: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" %>
このコードが書いてあるログインとサインアップ画面に出てると思います。

```rb: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
```

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

```erb: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 %>
<!-- -------------ここまで----------------------------------------------------- -->
```
<img width="1440" alt="スクリーンショット 2019-07-08 8.32.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348610/7fbcfa4e-70c0-89c0-b2d1-2e0395c42023.png">

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



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

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

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

###ルートの作成

```rb: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
```
###コントローラーの作成
上のルートに合わせてコントローラーを作成します。（生身はサイトのコピーです）

```rb: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`（変更時に入力するパスワードのフォーム）が残っているため取り除きます。

```erb: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](https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39)

```vim
$ 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
```
また、値は以下のコマンドで参照出来ます。（コンソールで試してみます）

```pry
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`を書き換えます。

```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に移行する](https://qiita.com/d-mato/items/aab2b6b2f587a8582631)

```
$ heroku config:set RAILS_MASTER_KEY=`cat config/master.key`
```
git push heroku masterを行う前に実行してください。
先にpushするとエラーが出てしまいました。


#"OmniAuth" の脆弱性対策
githubにpushすると`omniauth`の脆弱性を発見しましたとメールがきました。
対策として以下を参考にしました。↓
[[Rails]gem "OmniAuth" の脆弱性対策-Qiita](https://qiita.com/NT90957869/items/2a3ce18dedf93ccf2bdc)

###gemの追加
```rb:gemfile
gem "omniauth-rails_csrf_protection"
```
###リンクにメソッドを宣言する

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

```erb: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のアカウントが１つしかなかったため、何度も消して試してました。
以下データベースのリセットのコマンドです。

###development
```
$ rails db:migrate:reset
```
###prodction(heroku)
```
$ heroku pg:reset DATABASE
```
このコマンドだともう一度`heroku run rails db:migrate`を行う必要があります。
多分もっといいコマンドがあるはず。

#最後に

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