Rails6でSNS認証を実装しよう！連携後に本登録ページに遷移！

# はじめに
SNS認証のQiitaの記事は多く存在をしているのですが、SNS認証してそのまま登録完了という流れの記事しか見当たりませんでした。そこで、利用規約や名前をを確認する本登録ページを設けるための記事を書きました。
こちらの記事はgemのDeviseに付け加えていき実装していきます。

SNS認証=>本登録ページ=>登録完了=>自動でログインしてリダイレクト

上記の流れをつくっていきます。

# DeviseにOmniAuthを導入しよう

### 1. Gemを追加

```ruby:Gemfile.rb
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2' 
```

### 2. credentialsにIDとシークレットを追加

```credentials.yml.enc 
 facebook:
   client_id: <クライアントID>
   client_secret: <クライアントシークレット>
   
 google:
   client_id: <クライアントID>
   client_secret: <クライアントシークレット>
```

IDの作成は下記が参考になります。
https://qiita.com/nakanishi03/items/2dfe8b8431a044a01bc6

### 3. Deviseの設定を変更

```ruby:model/user.rb
devise :database_authenticatable, :registerable, :recoverable, :rememberable, 
       :trackable, :validatable, :confirmable, :timeoutable, 
       :omniauthable, omniauth_providers: %i[facebook google_oauth2] # 追加
```


### 4. Userモデルに紐づくSNS認証を管理するのみのモデルを用意
Userモデルのカラムに追加しても良いのですが、それだと本登録前に離脱したときにユーザーが登録されてることになってしまうので、本登録前までは別のモデルのみしか作成できないようにします。

1. SnsCredentialモデル作成

```
rails g model sns_credential
```

2. Migrationファイルを編集

```ruby:db/migrate/xxxxxxxxxxxxxx_create_sns_credentials.rb
class CreateSnsCredentials < ActiveRecord::Migration[6.1]
  def change
    create_table :sns_credentials do |t|
      t.string :provider # facebookやgoogleなど
      t.string :uid # アカウント固有のID
      t.references :user, null: true, foreign_key: true

      t.timestamps
    end
  end
end
```

3. リレーションをつける

```ruby:model/sns_credential.rb
class SnsCredential < ApplicationRecord
  belongs_to :user, optional: true # 本登録前まではnilのためoptionalはtrueに
end
```
```ruby:model/user.rb
has_many :sns_credentials # 追加
```

### 5.omniauth_callbacksを編集

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


```ruby:controllers/users/omniauth_callbacks_controller.rb```
# frozen_string_literal: true

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

    def google_oauth2
      callback_from
    end

    private

    def callback_from
      auth = request.env['omniauth.auth']
      @sns_credential = SnsCredential.find_for_oauth(auth) # 手順6で説明

      if @sns_credential.user # すでにアカウントがあるか確認
        sign_in_and_redirect @sns_credential.user, event: :authentication
      else # なかったら、本登録画面へ
        redirect_to <任意のパス>_path(auth: auth.except('extra'))
      end
    end
  end
end
```

### 6. find_for_oauthメソッドの作成

```ruby:model/sns_credential.rb
def self.find_for_oauth(auth)
  sns_credential = SnsCredential.find_or_create_by(provider: auth.provider, uid: auth.uid) # SnsCredentialモデルが存在したら取得して、なかったら作成する。

  user = find_by(email: auth.info.email)
  sns_credential.update(user_id: user.id) if user # すでに同じメールアドレスがUserモデルにあったら、UserとSnsCredentialモデルを紐付ける。そうすることによって、複数のパターン（facebookやらgoogleやら通常認証など）で認証できるため

  sns_credential
end
```

### 7. 本登録ページの作成

例として、パスワードはなしのフォームで、providerとuidを隠しフィールドとして持たせました。ここら辺はお好みで。providerとuidをセッションで管理しても良いですし。

```example.rb'''
<%= form_for(resource, url: <任意のパス>) do |f| %>
  <%= f.text_field :name, value: params[:auth][:info][:name], autocomplete: "name", required: true %>
  <%= f.email_field :email, value: params[:auth][:info][:email], autofocus: true, autocomplete: "email", required: true, readonly: true %>
  <%= link_to t('dictionary.registrations.term_here'), term_pages_path, target: "_blank" %>
  <%= f.label t('dictionary.registrations.term_agree') %>
  <%= f.hidden_field :provider, value: params[:auth][:provider] # 追加 %> 
  <%= f.hidden_field :uid, value: params[:auth][:uid] # 追加 %>
  <%= f.submit "Sign Up" %>
<% end %>
```

### 8. 本登録処理

```
def create_for_oauth
  sns_credential = SnsCredential.find_by(provider: params[:user][:provider], uid: params[:user][:uid])
  if sns_credential
    resource = User.create(name: params[:user][:name],
                           email: params[:user][:email],
                           password: Devise.friendly_token[0, 20])

    return redirect_to new_user_session_path, alert: 　resource.errors.full_messages.first unless resource.persisted?　# createに失敗したらエラーメッセージ表示してリダイレクト

    sns_credential.update(user_id: resource.id) # UserとSnsCredentialを紐付け
    sns_credential.user.update(confirmed_at: Time.now)　# 確認メールは不要なので
    sign_in(sns_credential.user) # サインインした状態に
    redirect_to root_path
  end
end
```

# まとめ
Deviseとの連携の記事を書きましたが、Devise以外でも似たような構成でいいのかなと思ってます。本登録をするメリットとしては、利用規約の確認になりますね。サービスを運営する上で利用規約の確認は非常に大事ですので、個人開発などをする際に意識は薄くなりがちですが実装してみてください。