devise+omniauth-rails_csrf_protection+[facebook/google]を使ったユーザ登録とログイン

##この記事で解決できること
deviseを使ったSNS認証ログイン
snsのユーザ情報を利用して登録フォームに入力する

**メールアドレスで登録するとき**
<img width="700" alt="スクリーンショット 2019-11-21 6.07.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465913/6b58ff73-f32e-dfaa-7a1b-1e8dfb838599.png">

**snsを使って登録するとき**(ニックネームとメールアドレスはsnsの情報から自動で入力される)
<img width="724" alt="スクリーンショット 2019-11-21 6.05.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465913/6a441b0f-9a94-c8c3-4000-ac07a9421265.png">

##開発環境
MacOS Mojave 10.14.6
ruby 2.5.1
Rails 5.2.3
mysql Ver 14.14 Distrib 5.6.43, for osx10.14

deviseでの新規登録ができる前提で進めます

##実装手順
下記の記事で通常の登録を解説してます。こちらを読んでからのほうが流れが理解しやすいかもしれないです...
>
[devise+ウィザード形式でのユーザ登録](https://qiita.com/tanegashiman/items/d1b370b22fe3a096f0d3)

1 必要なgemを導入
2 devise.rbを変更
3 ルーティングを設定
4 sns情報を保存するためのテーブルを作る
4 user.rbを変更
5 コントローラーで処理を書く

###gemを導入
```rb.Gemfile
gem 'devise'
gem 'omniauth-rails_csrf_protection' #omniauthでもいいがセキュリティー的にこっちのほうが安全らしい
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
```
###devise.rbを変更
```config/initializers/devise.rb
  config.omniauth :facebook,ENV['FACEBOOK__CLIENT_ID'],ENV['FACEBOOK_CLIENT_SECRET']
  config.omniauth :google_oauth2,ENV['GOOGLE_CLIENT_ID'],ENV['GOOGLE_CLIENT_SECRET'],scope: 'email'
```
環境変数の設定方法やAPI_KEYの取得方法は下記の記事を参考にしてください
>
[[Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する](https://qiita.com/kazuooooo/items/47e7d426cbb33355590e)

###ルーティングを設定
```routes.rb
 devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
```

###sns情報を保存するためのテーブルを作る
```
rails g modle sns_credential provider:string uid:string user:references
```
```sns_credential.rb
class SnsCredential < ApplicationRecord
  belongs_to :user
end
```

###user.rbを変更
```user.rb
class User < ApplicationRecord
  devise  :omniauthable, omniauth_providers: %i[facebook google_oauth2] #このオプションを追加
  has_many :sns_credentials, dependent: :destroy

#validatesは省略

  #omniauth_callbacks_controllerで呼び出すメソッド
  def self.find_oauth(auth)
    uid = auth.uid
    provider = auth.provider
    snscredential = SnsCredential.where(uid: uid, provider: provider).first #firstをつけないとデータが配列で返されて使いたいメソッドが使えなくて困る

    #sns_credentialsが登録されている
    if snscredential.present?
      user = User.where(email: auth.info.email).first

      # userが登録されていない
      unless user.present?
        user = User.new(
        nickname: auth.info.name,
        email: auth.info.email,
        )
      end
      sns = snscredential
      #返り値をハッシュにして扱いやすくする  
      #活用例 info = User.find_oauth(auth) 
             #session[:nickname] = info[:user][:nickname]
      { user: user, sns: sns}

    #sns_credentialsが登録されていない
    else
      user = User.where(email: auth.info.email).first


      # userが登録されている
      if user.present?
        sns = SnsCredential.create(
          uid: uid,
          provider: provider,
          user_id: user.id
        )

        { user: user, sns: sns}

      # userが登録されていない
      else
        user = User.new(
        nickname: auth.info.name,
        email: auth.info.email,
        )
        sns = SnsCredential.new(
          uid: uid,
          provider: provider
        )

        { user: user, sns: sns}
      end
    end
  end
end
```

###コントローラーで処理を書く
deviseのコマンドでコントローラーを作るときに自動で作成される。ない場合は作成してください。

```controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  # callback for facebook
  def facebook
    callback_for(:facebook)
  end

  # callback for google
  def google_oauth2
    callback_for(:google)
  end

  # common callback method
  def callback_for(provider)
    info = User.find_oauth(request.env["omniauth.auth"])
    # snsの情報からuserが登録されているか　or snsから情報を取得できているかを確認
    @user = User.where(nickname: info[:user][:nickname]).or(User.where(email: info[:user][:email])).first || info[:user]
    
    # persisted?はデータがDBに保存されているかを確認する/配列に対しては使えないから@userを定義するときは気をつける
    if @user.persisted?
      #保存されていればログインしてroot_pathにリダイレクトされる
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
    else
      # 登録するアクションに取得した値を渡すために。sessionを利用してuserインスタンスを作成する
      session[:nickname] = info[:user][:nickname]
      session[:email] = info[:user][:email]

      #snsでのユーザ登録ではパスワードを入力させないので準備する。パスワードを作成するDeviseメソッドもある。
      #今回のバリデーションは英数字のみなのでこっちを採用
      session[:password_confirmation] = SecureRandom.alphanumeric(30)

      #SnsCredentialが登録されていないとき
      if SnsCredential.find_by(uid: info[:sns][:uid], provider: info[:sns][:provider]).nil?
        #ユーザ登録と同時にsns_credentialも登録するために
        session[:uid] = info[:sns][:uid]
        session[:provider] = info[:sns][:provider]
      end
      #登録フォームのviewにリダイレクトさせる
      redirect_to step1_signups_path
    end
  end

  def failure
    redirect_to root_path
  end
end

```
###リダイレクト先のコントローラー

```signups_controller.rb
class SignupsController < ApplicationController
  before_action :authenticate_user!, only: :done

  def index
    delete_session # ユーザ登録を途中でやめたときのsessionを削除するために
    redirect_to root_path if user_signed_in?
  end

  #omniauth_callbacks_controllerからリダイレクトされたアクション
  def step1
    #sns認証を使った場合は情報利用してインスタンスを作成.
    #viewで条件分岐などを利用してパスワードフォームを表示させない
    @user = if session[:password_confirmation]
              User.new(
                nickname: session[:nickname],
                email: session[:email],
                password_confirmation: session[:password_confirmation]
              )
            else
              User.new
            end
  end

  def step1_validates
    # バリデーションチャックのためにインスタンスを作成
    @user = if session[:password_confirmation].present?
              set_user_when_sns(user_params)
            else
              set_user_when_email(user_params)
            end
    @user.valid?
    skip_phonenumber_validate(@user.errors)
    if verify_recaptcha(model: @user, message: "選択してください") && @user.errors.messages.blank? && @user.errors.details.blank?
      create_session(user_params)  ##createにわたすためにsessionを作成
      redirect_to step2_signups_path
    else
      @user.errors.messages[:birthday_day] = change_birthday_validate_message(@user)
      render :step1
    end
  end

  def step2
    redirect_to signups_path if session[:lastname].blank? ##URLの直うちアクセスを防ぐため
    @user = User.new
  end

  def create
    set_user_with_session
    @user[:phonenumber] = user_params[:phonenumber]
    if @user.save
      SnsCredential.create(  #ユーザ登録と同時にこっちも登録
        uid: session[:uid],
        provider: session[:provider],
        user_id: @user.id
      )    
      sign_in User.find(@user.id) unless user_signed_in?
      redirect_to addresses_path
    else
      render :step2
    end
  end

  def done; end

  private

    def user_params
      params.require(:user).permit(:nickname, :email, :password, :password_confirmation, :lastname, :firstname,
                                  :lastname_kana, :firstname_kana, :birthday_year,
                                  :birthday_month, :birthday_day, :phonenumber)
    end

    def create_session(user_params)
      session[:nickname] = user_params[:nickname]
      session[:email] = user_params[:email]
      # sns認証のときはsessionを使っている
      if session[:password_confirmation]
        session[:password] = session[:password_confirmation]
      else
        session[:password] = user_params[:password_confirmation]
        session[:password_confirmation] = user_params[:password_confirmation]
      end
      session[:lastname] = user_params[:lastname]
      session[:firstname] = user_params[:firstname]
      session[:lastname_kana] = user_params[:lastname_kana]
      session[:firstname_kana] = user_params[:firstname_kana]
      session[:birthday_year] = user_params[:birthday_year]
      session[:birthday_month] = user_params[:birthday_month]
      session[:birthday_day] = user_params[:birthday_day]
    end

    def set_user_with_session
      @user = User.new(
        nickname: session[:nickname],
        email: session[:email],
        password: session[:password_confirmation],
        password_confirmation: session[:password_confirmation],
        lastname: session[:lastname],
        firstname: session[:firstname],
        lastname_kana: session[:lastname_kana],
        firstname_kana: session[:firstname_kana],
        birthday_year: session[:birthday_year],
        birthday_month: session[:birthday_month],
        birthday_day: session[:birthday_day]
      )
    end

  def set_user_when_sns(user_params)
    session[:email] = user_params[:email] unless session[:email] ##sns認証でメールがない場合sessionが作られないので
    User.new(
      ##omniauth_callbacks_controller#callback_forで作成したsessionを利用
      nickname: session[:nickname],
      email: session[:email],
      password: session[:password_confirmation],
      password_confirmation: session[:password_confirmation],
      lastname: user_params[:lastname],
      firstname: user_params[:firstname],
      lastname_kana: user_params[:lastname_kana],
      firstname_kana: user_params[:firstname_kana],
      birthday_year: user_params[:birthday_year],
      birthday_month: user_params[:birthday_month],
      birthday_day: user_params[:birthday_day]
    )
  end

  def set_user_when_email(user_params)
    User.new(
      nickname: user_params[:nickname],
      email: user_params[:email],
      password: user_params[:password_confirmation],
      password_confirmation: user_params[:password_confirmation],
      lastname: user_params[:lastname],
      firstname: user_params[:firstname],
      lastname_kana: user_params[:lastname_kana],
      firstname_kana: user_params[:firstname_kana],
      birthday_year: user_params[:birthday_year],
      birthday_month: user_params[:birthday_month],
      birthday_day: user_params[:birthday_day]
    )
  end

    # 電話番号をstep1で入力しないので空のときのバリデーションをスキップする
    def skip_phonenumber_validate(errors)
      errors.messages.delete(:phonenumber)
      errors.details.delete(:phonenumber)
    end

    # 生年月日のどれかにひとつでもバリデーションエラーがあった場合は同じエラーメッセージを表示する
    def change_birthday_validate_message(user)
      if user.errors.messages[:birthday_year].any? || user.errors.messages[:birthday_month].any? || user.errors.messages[:birthday_day].any?
        user.errors.messages.delete(:birthday_year)
        user.errors.messages.delete(:birthday_month)
        user.errors.messages[:birthday_year] = ["生年月日は正しく入力してください"]
      end
    end

    def delete_session
      session.delete(:nickname)
      session.delete(:email)
      session.delete(:password)
      session.delete(:password_confirmation)
      session.delete(:lastname)
      session.delete(:firstname)
      session.delete(:lastname_kana)
      session.delete(:firstname_kana)
      session.delete(:birthday_year)
      session.delete(:birthday_month)
      session.delete(:birthday_day)
      session.delete(:pid)
      session.delete(:provider)
    end
end
```
###リンクを通す
```
 = button_to "メールアドレスで登録",step1_signups_path, method: :get, class: "login2-panel__login-form-inner--email"

 # omniauth-rails_csrf_protectionを使うときはpostアクションを指定する
 = button_to "facebookで登録", user_facebook_omniauth_authorize_path, method: :post
 = button_to "googleで登録",user_google_oauth2_omniauth_authorize_path, method: :post
```
snsを使ってユーザ登録している場合はこれと同じURLでログインすることができる

###最後に
コードの可読性が悪いので皆さんのアドバイスを頂けると助かります！
少しでも勉強の手助けをできたらなと思います