【Rails6】Devise＋SNS認証で登録＆ログイン(複数連携可)

###はじめに
複数のSNSと連携可能な認証をつくります。
Deviseは実装済みとして進めます。
各APIキーの取得等にも触れませんので適宜お調べ下さい。
今回はfacebookとtwitterで実装しますが他のSNSでも基本的には同じだと思います。

###環境

```console:環境
Windows10
ruby 2.6.6
Rails 6.0.3.1
```

###下準備

#####gem追加

```rb:Gemfile
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
```
保存後bundle installして下さい。

#####credentials.ymlにAPIキーを記述

Windowsの方は[前回の記事](https://qiita.com/ChangGin/items/369ade18ed49f932cb42)を参考にしてみてください。

```yml:credentials.yml

facebook:
  api_key: pk_test_~
  secret_key: sk_test_~
twitter:
  api_key: pk_test_~
  secret_key: sk_test_~
```
#####Deviseの設定
config/initializer内。

```rb:devise.rb
#以下を追加
config.omniauth :facebook, Rails.application.credentials.facebook[:api_key], Rails.application.credentials.facebook[:secret_key], scope: 'email', info_fields: 'email,name'
config.omniauth :twitter, Rails.application.credentials.twitter[:api_key], Rails.application.credentials.twitter[:secret_key], scope: 'email', callback_url: 'https://localhost:3000/users/auth/twitter/callback'
```

twitterの方はコールバックURLを明示する必要があるみたいです。

#####Socialモデルを作成
今回はDeviseでUserモデルを作成済みとします。
Userモデルへカラムを追加してもいいですが、今回はUserモデルに紐づくSocialモデルを新たに作成します。
(UserモデルとSocialモデルは一対多の関係となります。)

```console:console
rails g model social
```

Socialモデルにはuser_id、provider("facebook"や"twitter"などが入る)、uid(各SNSアカウントと紐づけるためのidが入る)を持たせます。

```rb:2020~create_socials.rb
class CreateSocials < ActiveRecord::Migration[6.0]
  def change
    create_table :socials do |t|
      t.references :user, null: false, foreign_key: true
      t.string :provider, null: false
      t.string :uid, null: false
      t.timestamps
    end
  end
end
```
保存したらrails db:migrateする。

#####関連付け

```rb:user.rb
has_many :socials, dependent: :destroy#追加
devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, 
          :omniauthable#追加
```

```rb:social.rb
belongs_to :user
validates :uid, uniqueness: {scope: :provider}#同一provider内で多重登録できないようにする
```
同じSNSアカウントが複数のユーザーに紐付かないようにします。
「{scope: :provider}」は多分なくても平気だと思いますが念のため。

###実装
#####コールバック処理をつくる

```rb:routes.rb
devise_for :users, controllers: {
  sessions: 'users/sessions',
  password: 'users/password',
  registrations: 'users/registrations',
  omniauth_callbacks: 'users/omniauth_callbacks'#追加
  }
```

以下app/controllers/users/omniauth_callbacks_controller.rb内。
(rails routes等で確認し、この中のメソッドを各リダイレクト先に設定して下さい。)

```rb:omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

 def facebook
    if request.env['omniauth.auth'].info.email.blank?#Facebookでメール使用を許可しているか
      redirect_to '/users/auth/facebook?auth_type=rerequest&scope=email'
    end
    callback_from :facebook
  end

  def twitter
    callback_from :twitter
  end


  private

  def callback_from(provider)
    provider = provider.to_s

    if user_signed_in?
      @user = current_user
      User.attach_social(request.env['omniauth.auth'], @user.id)#後でattach_social作る。SNSからの情報とログイン中のUserのidを渡す。
    else
      @user = User.find_omniauth(request.env['omniauth.auth'])#後でfind_omniauth作る。SNSからの情報を渡す。
    end

    if @user.persisted?#登録済みor登録できたら
      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
```

リダイレクト元のSNSの種類を「callback_from」へ渡して切り分けます。
「callback_from」内では「SNS認証での新規ユーザー登録またはSNS認証済みユーザーのログイン」か「ログイン中ユーザーのSNS連携」かを切り分けました。
「登録後」「ログイン後」「連携後」の処理は今回は分けずに、全てユーザーページにリダイレクトさせています。

次にUser.rb内に各処理を書きます。

```rb:user.rb
 def self.find_omniauth(auth)#SNS認証での新規登録またはsnsログイン
    social = Social.where(uid: auth.uid, provider: auth.provider).first
    unless social.blank?#sns認証済み(ログイン)
      user = User.find(social.user.id)
    else#sns認証での新規登録
      temp_pass = Devise.friendly_token[0,20]#今回は取り敢えずランダムなパスワードを作ります
      user = User.create!(
        username: auth.info.name,
        email: auth.info.email,
        password: temp_pass,
        password_confirmation: temp_pass,
      )
      social = Social.create!(
        user_id: user.id,
        provider: auth.provider,
        uid: auth.uid,
      )
    end

    return user
 end

 def self.attach_social(auth, user_id)#sns連携追加時
    social = Social.create!(
      user_id: user_id,
      provider: auth.provider,
      uid: auth.uid,
    )
  end
```

「SNS認証での新規ユーザー登録またはSNS認証済みユーザーのログイン」時はさらに「新規登録」か「ログイン」かを切り分けます。
「新規登録」時はUserとSocialを両方作成し(同時にUserとSocialの紐づけもする)、Userを返します。
「ログイン」時はSNSから送られてきた情報(auth.uidとauth.provider)からSocialを探し、紐づくUserを返します。
「ログイン中ユーザーのSNS連携」時はSocialのみ作成します。ログイン中のUserと紐づけるため、user_idを受け取ってます。

一応これで処理は終わりです。あとは「user_facebook_omniauth_authorize_path」などでリンクを作成すれば、Userの状況に応じて新規登録やログイン、連携を行ってくれます。

###おしまい
実際には連携したSNSへのリンクを作ったり、SNSでの新規登録時のパスワード再設定の仕組み、Deviseのメール認証、連携の解除ボタンなどもあったほうが良いかと思いますが、ひとまず最小構成で作ってみました。
Rails勉強中なのでスマートでない所が多々あるかと思います。もしおかしなことをしてたら教えて頂けると嬉しいです！