[Rails]SNS認証(Twitter、Facebook、Google)機能の実装

#はじめに

今回はRailsアプリにおけるdeviseによるSNS認証での、新規登録・ログイン機能の実装方法を解説します。

#前提条件
・deviseによるユーザー管理機能を実装済み
・SNS認証の外部APIを登録済み

外部APIの登録手順は以下の記事がわかりやすいです。
>[【Rails】SNS認証の登録手順(Twitter、Facebook、google)](https://qiita.com/manbolila/items/8caa1f5d2b1fb96d2646)

#機能の仕様

・　Twitter/Facebook/Google登録を押すとSNS認証が始まり、ニックネームとメールアドレスが入力された状態でユーザー登録が始まる

・SNS認証での新規登録の際はパスワードが自動生成され、新規登録できる

#手順

## 1)APIの設定

こちらは最初にも書いたように上記記事を参考に行ってください。

## 2)RailsアプリにSNS認証を実装
### 2-1)Gemのインストール

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

# omniauth認証はCSRF脆弱性が指摘されているため対策としてインストール
gem 'omniauth-rails_csrf_protection'
# 環境変数を管理するためインストール(vim ~/.zshrcで定義することも可能)
gem 'dotenv-rails'
```

`Gemrile`に記述したら忘れず`bundle install`しましょう。

`dotenv-rails`については以下を参考にしてみてください。

>[DockerコンテナにRailsの環境変数を適用させる方法(aws::Sigv4::Errorsの解決法)](https://qiita.com/sc30gsw/items/f85ab4eaf4ca8164b3b0)

```ruby:ターミナル
% bundle install
```
### 2-2)環境変数の設定

```ruby:ターミナル
% vim ~/.zshrc

# iを押してインサートモードにして入力
export TWITTER_API_KEY = 'メモしたID'
export TWITTER_API_SECRET_KEY = 'メモしたSECRET'
export FACEBOOK_API_KEY = 'メモしたID'
export FACEBOOK_API_SECRET_KEY = 'メモしたSECRET'
export GOOGLE_API_KEY='メモしたID'
export GOOGLE_API_SECRET_KEY='メモしたSECRET'

# 定義したらesc→:wqで保存
```

保存したら下記コマンドを実行し設定を反映させましょう。

```ruby:ターミナル
% source ~/.zshrc
```

`gem dotenv-rails`インストールしている場合は`.env`ファイルをアプリディレクトリに作成し、そのファイル内に記述していきます。

```ruby:.env
TWITTER_API_KEY = 'メモしたID'
TWITTER_API_SECRET_KEY = 'メモしたSECRET'
FACEBOOK_API_KEY = 'メモしたID'
FACEBOOK_API_SECRET_KEY = 'メモしたSECRET'
GOOGLE_API_KEY = 'メモしたID'
GOOGLE_API_SECRET_KEY =　'メモしたSECRET'
```

記述が完了したら、pushしないように`gitignore`ファイルに`.env`を追加します。

```ruby:gitignore
/.env
```

###2-3)アプリ側で環境変数を読み込む
`config/initializers/devise.rb`ファイルを編集します。

```ruby:config/initializers/devise.rb
Devise.setup do |config|
  # 省略
  config.omniauth :twitter,ENV['TWITTER_API_KEY'],ENV['TWITTER_API_SECRET_KEY']
  config.omniauth :facebook,ENV['FACEBOOK_API_KEY'],ENV['FACEBOOK_API_SECRET_KEY']
  config.omniauth :google_oauth2,ENV['GOOGLE_API_KEY'],ENV['GOOGLE_API_SECRET_KEY']
end
```
環境変数の設定は以上です。

#3)SNS認証機能のサーバーサイド実装

###3-1)SNS認証用のモデルの作成
SNS認証時はAPIにリクエストを送って、認証を行います。
そのため**usersテーブルとは別にSNS認証用のテーブルを作成する**必要があります。

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

```ruby:db/migrate/XXXXXXXXXXX_crate_sns_credentials.rb
class CreateSnsCredentials < ActiveRecord::Migration[6.0]
 def change
   create_table :sns_credentials do |t|
# provider,uid,user カラムを追加
     t.string :provider
     t.string :uid
     t.references :user,  foreign_key: true

     t.timestamps
   end
 end
end
```

編集できたら`rails db:migrate`を実行します。

###3-2)UserモデルとSnsCredentialモデルの編集
deviseでOmniAuthを使えるよう編集していきます。

```ruby:app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:twitter, :facebook, :google_oauth2]

has_many :sns_credentials
```

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

###3-3)deviseのコントローラーの設定
ターミナルで下記コマンドを実行し、deviseのコントローラーを作成します。

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

コントローラーを作成したら、deviseのルーティングを設定します。

```ruby:config/routes.rb
Rails.application.routes.draw do
 devise_for :users, controllers: {
   omniauth_callbacks: 'users/omniauth_callbacks',
   registrations: 'users/registrations'
 }
  root to: 'users#index'
end
```

ここまででSNS認証を実現するための準備が完了です。
もう少し頑張りましょう。

#4)SNS認証を行うためのメソッドの実装

###4-1)メソッドの実装

>[OmniAuthのGitHub](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview)

>[Google-auth2のGitHub](https://github.com/zquestz/omniauth-google-oauth2)

上記のドキュメントにもありますが、deviseのコントローラー内にメソッドを定義していきます。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

 def twitter
  authorization
 end

 def facebook
  authorization
 end

 def google_oauth2
  authorization
 end

 private

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

次に定義したアクションをビューで呼び出します。

```app/views/users/new.html.erb
<%= link_to 'Twitterで登録', user_twitter_omniauth_authorize_path, method: :post%>
<%= link_to 'Facebookで登録', user_facebook_omniauth_authorize_path, method: :post%>
<%= link_to 'Googleで登録', user_google_oauth2_omniauth_authorize_path, method: :post%>
```

次に`Userモデル`にメソッドを作成します。

```ruby:app/models/usr.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, omniauth_providers: [:facebook, :google_oauth2]

 has_many :sns_credentials
# クラスメソッドを定義する
 def self.from_omniauth(auth)
  # 定義できたら「binding.pry」を記述しSNSから情報を取得できるか確認してみましょう
 end
end
```

認証ボタンで登録すると処理が止まりますので、ターミナルで`auth`と入力し情報を取得できているか確認してみましょう。

確認できたら、メソッドの中身を記述していきます。

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

処理については`first_or_createメソッド`を使うことで、DBに保存するかどうかを判断しています。

次にSNS認証を行っていなかった(新規登録の場合)にDBに検索をかけるように記述を加えます。

```ruby:app/models/user.rb
def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
   # sns認証したことがあればアソシエーションで取得
   # 無ければemailでユーザー検索して取得orビルド(保存はしない)
   user = User.where(email: auth.info.email).first_or_initialize(
     nickname: auth.info.name,
       email: auth.info.email
   )
 end
```

`first_or_initializeメソッド`を用いて検索をかけることでDBに新規レコードを保存しないように処理を行えます。

### 4-2)Userモデルからの処理を記述

MVCの流れに沿って、モデルの処理をコントローラーで記述していきます。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb

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

ここまでで新規登録機能の実装が完了しました。

#5)ログイン機能の実装

###5-1)Userモデルの編集

ログイン時の処理を記述していきます。

```ruby:app/models/user.rb
def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
   # sns認証したことがあればアソシエーションで取得
   # 無ければemailでユーザー検索して取得orビルド(保存はしない)
   user = User.where(email: auth.info.email).first_or_initialize(
     nickname: auth.info.name,
       email: auth.info.email
   )
# 以下を追記
   # userが登録済みであるか判断
   if user.persisted?
     sns.user = user
     sns.save
   end
   { user: user, sns: sns }
 end
```

次にビューを編集します。
OmniAuthは**新規登録とログインを兼ねている**ためパスは同じです。

```ruby:app/views/devise/sessions/new.html.erb
<%= link_to 'Twitterでログイン', user_twitter_omniauth_authorize_path, method: :post%>
<%= link_to 'Facebookでログイン', user_facebook_omniauth_authorize_path, method: :post%>
<%= link_to 'Googleでログイン', user_google_oauth2_omniauth_authorize_path, method: :post%>
```
以上でログイン機能の実装は終了です。

最後にSNS認証時のパスワード入力をしなくてもいいように実装していきます。

###5-2)パスワード入力についての処理実装

sns_credentialモデルに`optional: true`というオプションを追加します。このオプションをつけることで外部キーの値がなくても保存できるようになります。

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

コントローラーに以下の記述を追加

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
Class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
 #中略
 def authorization
   sns_info = User.from_omniauth(request.env["omniauth.auth"])
# @user と @sns_id を追加
   @user = sns_info[:user]

   if @user.persisted?
     sign_in_and_redirect @user, event: :authentication
   else
     @sns_id = sns_info[:sns].id
     render template: 'devise/registrations/new'
   end
 end
```
end

ビューファイルのpasswordのフォームで、SNS認証を行っているかの条件分岐を記述します。

```ruby:app/views/devise/registrations/new.html.erb

 <%if @sns_id.present? %>
   <%= hidden_field_tag :sns_auth, true %>
 <% else %>
   <div class="field">
     <%= f.label :password %>
     <% @minimum_password_length %>
     <em>(<%= @minimum_password_length %> characters minimum)</em>
     <br />
     <%= f.password_field :password, autocomplete: "new-password" %>
   </div>

   <div class="field">
     <%= f.label :password_confirmation %><br />
     <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
   </div>
 <% end %>
```

最後にdeviseの`createアクション`を作動させるように、コメントアウトを外し下記のようなを記述します。


```ruby:app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
 # before_action :configure_sign_up_params, only: [:create]
 # before_action :configure_account_update_params, only: [:update]


 # GET /resource/sign_up
 # def new
 #   super
 # end

 # POST /resource
 def create
   if params[:sns_auth] == 'true'
     pass = Devise.friendly_token
     params[:user][:password] = pass
     params[:user][:password_confirmation] = pass
   end
   super
 end
#省略
```

このように記述することで、ビューファイルからのparamsから送信されてきた値を保存することができます。

これで完全にSNS認証機能の実装は終了です。

自分のアプリで使うのは初めてだったので備忘録として記録してこうと思い作成しました。
お役に立てば幸いです。



#参考文献
[OmniAuthのGitHub](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview)

[Google-auth2のGitHub](https://github.com/zquestz/omniauth-google-oauth2)

[【Rails】SNS認証(Twitter、Facebook、google)](https://qiita.com/manbolila/items/48d361fd5968179918c3)

[【Rails】SNS認証の登録手順(Twitter、Facebook、google)](https://qiita.com/manbolila/items/8caa1f5d2b1fb96d2646)