【Rails6】SNS認証による新規登録機能実装（Facebook, Google）

#はじめに

現在Raisを使用しアプリケーションの作成を行なっています。今回は既に実装しているログイン機能にSNSアカウントでログインできるよう追加実装していきます。備忘録及び復習のため記述していきます。

#環境

Ruby on Rails '6.0.0'
Ruby '2.6.5'

#前提

- devise（gem)を使用し、ユーザーのログイン機能実装済み。
-  [facebook for developers](https://developers.facebook.com/)と [Google Cloud Platform](https://console.cloud.google.com/?hl=ja)から外部APIの設定済み。

#①Gemの導入と環境変数の設定

**omniauth**
 ・Google,Facebook,Twitter等のSNSアカウントを用いてユーザー登録やログインなどを実装できるgemです。今回はFacebookとGoogleのomniauthをインストールします。

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

```ruby:ターミナル
% bundle install
```

また、外部API設定時に取得したIDやシークレットキーを環境変数に設定します。環境変数の設定方法はいくつかあると思いますが、私の場合は、「gem 'dotenv-rails'」により「.envファイル」を設定済みのため、そこに記述しました。

```ruby:.env
FACEBOOK_CLIENT_ID='アプリID'
FACEBOOK_CLIENT_SECRET='app secret'
GOOGLE_CLIENT_ID='クライアントID'
GOOGLE_CLIENT_SECRET='クライアントシークレット'
```

次にアプリケーション側で環境変数を読み込む記述をします。

```ruby: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']
```

#②モデルの設定

SNSのWebAPIにリクエストを送ると、API上で認証を行います。その後、SNS上に登録されている情報がアプリケーション側に返される仕組みです。
APIからのレスポンスの中の、「uid」と「provider」をアプリケーションのデータベースに、ユーザー情報（名前など）と共に保存します。
しかし、「SNS認証とユーザー登録のタイミングが異なる」仕様であるため、SNS認証時にはusersテーブルのレコードを作成することはできません。そこでSNS認証時の情報を保存する別テーブル（SnsCredentialモデル）を作成しました。

```ruby:ターミナル
% rails g model sns_credential
```

```ruby:db/migrate/XXXXXXXXXXXXXXXX_create_sns_credentials.rb
class CreateSnsCredentials < ActiveRecord::Migration[6.0]
 def change
   create_table :sns_credentials do |t|
     t.string :provider
     t.string :uid
     t.references :user,  foreign_key: true

     t.timestamps
   end
 end
end
```

userモデルとのアソシエーションのため、外部キーとしてuser_idを持たせています。編集がおわり次第、「rails db:migrate」にてデータベースに反映します。

アソシエーションの設定をします。

```ruby:app/models/user.rb
(省略)
has_many :sns_credentials, dependent: :destroy

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2]
```
omniauth_providers: [:facebook, :google_oauth2]と記述することで、FacebookとGoogleのOumniAuthを使用できます。

```ruby:app/models/sns_credential.rb
class SnsCredential < ApplicationRecord
  belongs_to :user
end
```

#③deviseの再設定

deviseのコントローラーを再設定するため、コントローラーを作成しています。

```ruby:ターミナル
% rails g devise:controllers users
```

生成したコントローラーを使用させるために、deviseのルーティングを変更します。

```ruby:config/routes
Rails.application.routes.draw do
  devise_for :users, controllers: {
    omniauth_callbacks: 'users/omniauth_callbacks',
    registrations: 'users/registrations',
    sessions: 'users/sessions',
  }
・・・（省略）・・・
end
```

#④コントローラーの設定

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    authorization
  end

  def google_oauth2
    authorization
  end

  private

  def authorization
    @user = User.from_omniauth(request.env["omniauth.auth"]
  end
end
```

プライベートメソッド内に記述している「authorization」を呼び出す「facebook」と「google_oauth2」というアクションを定義します。

#⑤Viewファイルの編集

```ruby:app/views/users/registrations/new.html.erb
（省略）
<div class="d-flex justify-content-between mt-4">
  <%= link_to user_facebook_omniauth_authorize_path, class:"btn btn-outline-primary sns-btn", method: :post do %>
    <i class="fab fa-facebook fa-2x"></i>
  <% end %>
  <%= link_to user_google_oauth2_omniauth_authorize_path, class:"btn btn-outline-danger sns-btn", method: :post do %>
    <i class="fab fa-google fa-2x"></i>
  <% end %>
</div>
```

#⑥modelの編集

Userモデルに入るデータのため、Userモデルにクラスメソッドを作成しました。
クラスメソッドとは、クラスで共通の情報を使った処理に使用するようです。記述方法は、メソッド名の前にselfを.（ドット）で繋いで定義します。

```ruby:app/models/user.rb
（省略）
 def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
 end
```

上記でomniauth_callbacks_controller.rbに記述した、User.from_omniauthも呼び出せるようになりました。
また、取得した情報内の「provider」と「uid」をsnsに代入して、first_or_createメソッドを用いています。
（first_or_createメソッド：保存するレコードがデータベースに存在するか検索を行い、検索した条件のレコードがあればそのレコードのインスタンスを返し、なければ新しくインスタンスを保存するメソッドです。）

次に、SNS認証を行なったかどうか確認した後のコードを記述します。

```ruby:app/models/user.rb
（省略）
 def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
   user = User.where(email: auth.info.email).first_or_initialize(
      first_name: auth.info.last_name,
      last_name: auth.info.first_name,
      email: auth.info.email
    )
 end
```

上記で追加したコードは、SNS認証を行なっていなかった場合、メールアドレスで検索をします。first_or_initializeを用いて、名前とメールアドレスを返します。
（first_or_initialize：whereメソッドとともに使うことで、whereで検索した条件のレコードがあればそのレコードのインスタンスを返し、なければ新しくインスタンスを作るメソッドです。）

first_or_create：新規レコードをデータベースに保存する
first_or_initialize：新規レコードをデータベースに保存しない

#⑦controllerの編集

Userモデルから返ってきた後の処理を記述します。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    authorization
  end

  def google_oauth2
    authorization
  end

  private

  def authorization
    @user = User.from_omniauth(request.env['omniauth.auth'])

    if @user.persisted? #ユーザー情報が登録済みのため、新規登録ではなくログイン処理を行う
      sign_in_and_redirect @user, event: :authentication
    else #ユーザー情報が未登録のため、新規登録画面へ遷移する
      render template: 'devise/registrations/new'
    end
  end
end
```

Userモデルから返ってきた値を@userに代入します。これは、viewで取得した「名前」と「メールアドレス」を表示させるためです。

次に、ログイン時の記述を行います。

#⑧Userモデルの編集

```ruby:app/models/user.rb
（省略）
 def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
   user = User.where(email: auth.info.email).first_or_initialize(
      first_name: auth.info.last_name,
      last_name: auth.info.first_name,
      email: auth.info.email
    )

  # userが登録済みであるか判断
   if user.persisted?
     sns.user = user
     sns.save
   end
   user
 end
```

persisted?で登録済みのユーザーと判断を行い、「if文」の中が呼ばれます。新規登録時は、SnsCredentialモデルが保存されるタイミングで、user_idが確定していなかったので、SnsCredentialモデルとUserモデルは紐づいていません。ログインの際に、sns.userを更新して紐付けを行います。

最後にログイン画面のviewファイルも編集して完了です！！

#終わりに

もしこの記事を参考に、実装してみて、うまくできない場合は教えてください。。初めての実装のためお許しください。。