Google,FacebookのAPIを用いてログイン機能を作る

#初めまして
今回外部APIを用いて初めて機能を作ったので備忘録として残したいと思います。
なお、APIの初期設定は割愛させていただきます。
Rails側だけのコード処理についてのみとなりますので、ご了承ください。

#開発環境
RubyonRails6

#導入について
今回、私はユーザー管理機能として「devise」gemを使用しております。

RailsでSNS認証を実装するためには「omniauth」というGemをします。
Google Facebookだけではなく、TwitterなどのSNSアカウントを用いてログイン機能などを実装できるみたいです。

また、「omniauth」以外にも「omniauth-rails_csrf_protection」というGemをインストールします。
これは、Omniauth認証はCSRF脆弱性が指摘されている為です。

```
Gemfile
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem "omniauth-rails_csrf_protection"
gem 'omniauth', '~>1.9.1'
```
bundle installをしましょう。

API登録する時は、環境変数を設定します。
自分は、MacOSがCatalinaなので

```
vim ~/.zshrc

```
↑のコマンド実行します。

```
iを押してインサートモードに移行
export FACEBOOK_CLIENT_ID='ご自身のアプリID'
export FACEBOOK_CLIENT_SECRET='ご自身のapp secret'
export GOOGLE_CLIENT_ID='ご自身のクライアントID'
export GOOGLE_CLIENT_SECRET='ご自身のクライアントシークレット'

編集が終わったらescapeキーを押してから:wqと入力して保存して終了
```
この時、気をつけないといけないのが、元々ある記述を消さないようにすることです。
間違えて消してしまってPCの動作がおかしくなったなんてこともあるみたいです！

ターミナルで

```
source ~/.zshrc
```

これで環境変数の設定が完了しました。

# 次は、アプリ側で環境変数を読み込みます。

```
config/initializers/devise.rb

Devise.setup do |config|
  config.omniauth :facebook,ENV['FACEBOOK_CLIENT_ID'],ENV['FACEBOOK_CLIENT_SECRET']
  config.omniauth :google_oauth2,ENV['GOOGLE_CLIENT_ID'],ENV['GOOGLE_CLIENT_SECRET']
end
```
ここでの記述で余分にスペースを開けたり、誤字に気をつけてください。
不備があると、あとで使用するときにうまく実装することができません。
→自分はここで記述をミスして、１時間ほどパニクリました

# 次に、SnsCredentialモデルを作成します。

何故作成するかというと「SNS認証とユーザー登録のタイミングが異なる」使用の為、SNS認証時にはusersテーブルのレコードを作成することはできないのです。
ですので、SNSに関わる別テーブルを用意した方がベストだということです！

```
マイグレーションファイル
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

```
uidとproviderをデータベースに保存するので、記述します。そして、アソシエーションのために、外部キーとしてuser_idを持たせます。

Userモデルを編集します。

```
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2]
```

:omniauthable, omniauth_providers: [:facebook, :google_oauth2]の記述によりAPIを使用できるようになります。

アソシエーションを組みます。

```
user.rb

has_many :sns_credentials
```

```

sns_credential.rb

belongs_to :user
```

アソシエーション完了です。

deviseのクラスを継承したコントローラーを生成します。
このコントローラー内に標準で実装されているdeviseの設定を上書きすることによって、deviseが提供するアクションの内容を設定できます。

```
rails g devise:controllers users
```

```
route.rb

Rails.application.routes.draw do
 devise_for :users, controllers: {
   omniauth_callbacks: 'users/omniauth_callbacks',
   registrations: 'users/registrations'
 }
end
```


# ビューにAPIサインアップ、ログインを呼び出す記述をします。

```
registrations/new.html.erb

<%= link_to "Facebookで登録", user_facebook_omniauth_authorize_path, method: :post%>
<%= link_to "Googleで登録", user_google_oauth2_omniauth_authorize_path, method: :post%>

```

```
sessions/new.html.erb
<%= link_to 'Facebookでログイン', user_facebook_omniauth_authorize_path, method: :post%>
<%= link_to 'Googleでログイン', user_google_oauth2_omniauth_authorize_path, method: :post%>
```

# Userモデルにメソッドを作成します

```
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)
   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
end
```

#SNS認証を実現させるためのメソッドをコントローラーに書きます。

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

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

 def facebook
  authorization
 end

 def google_oauth2
  authorization
 end

 private

 def authorization
   sns_info = User.from_omniauth(request.env["omniauth.auth"])
   @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
```

authorizationはprivateメソッド内に記述し、authorizationを呼び出す「facebook」と「google_oauth2」というアクションを定義しています。
Userモデルから送られるデータをビューで扱えるようにします。
@userには「nickname」と「email」の情報をもたせて、SNS認証の判断は「sns_id」で行うため、idだけで扱えるようにします。



# ビューの編集をします。
SNS認証を行っているかいないかで条件分岐をします。
パスワード入力を不要にするためです。

```
registrations/new.html.erb

#省略
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

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

# コントローラーを編集します

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

```
superメソッドを使うと、親モデルの同名のメソッドを実行します。今回であれば「registrations#create」が実行されます。
params[:sns_auth]を取得した時だけ、「Devise.friendly_token」を使ってパスワードを自動生成し、あとの処理はsuperメソッドでdeviseのregistrations#createが実行します。

# 最後に
長々と書いてしまいましたが、上記の方法でローカル環境では実装することができました。

最後まで読んでいただき、ありがとうございました。