Facebookログイン認証機能を実装してみた + エラーの原因はバリデーションだった[Rails]

# **最初に**
webサービスを開発するにあたってユーザー認証機能はマストですよね。
いかにUXを良くしユーザに使ってもらえるサービスにするかは全ての開発者の望むところだと思います。
sns認証サービスに対応しておくとユーザの登録の手間が省け、ユーザ数の増加に繋がるはずです。
そこで、今回はfacebook認証を実装し、「サクッとログイン」を叶えていきましょう。

devise認証機能を実装済みという前提でお話していきますので、まだの方は下記記事を参考にdeviseによるログイン機能を実装しておきましょう。

[[*Rails*] deviseの使い方（rails4版）](https://qiita.com/cigalecigales/items/73d7bd7ec59a001ccd74)

# **1. 「uidと」「provider」をカラムに追加**
Userテーブルに、uid(任意のid)、provider(facebookやtwitter)の2つのカラムを追加します。

```
$ rails g migration AddColumnsToUsers uid:string provider:string
$ rake db:migrate
```

# **2. Facebookアプリを作成**

[Facebook Developer](https://developers.facebook.com/)にアクセスして、「新しいアプリを追加」を選択。

<img width="1440" alt="Screen Shot 2018-10-31 at 8.21.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/269862/103e6918-19d9-2c74-31a8-d4dfa39a07cb.png">

続いて、表示されたモーダルウィンドウにメールアドレスとアプリ名（適当で良い）を記入。

<img width="1440" alt="Screen Shot 2018-10-31 at 8.25.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/269862/5a8aaace-fbcd-8600-279d-1313d971d2b1.png">

左メニューの「設定」→「ベーシック」を選択。

遷移先ページの一番下の「+プラットホームを追加」を選択。
そこから、「Website」を選び、url記入欄「サイトURL」に下記のように記述する。

<img width="957" alt="p2me_―_設定_-_開発者向けFacebook.png" src="https://qiita-image-store.s3.amazonaws.com/0/269862/f7f99cfa-6e8e-e8e3-acb7-2787084076e9.png">

注：production環境の場合は適当なURLに変更する。

これでFacebook側の設定は終了です。

# **3. Gemを追加**

``` 
gem 'omniauth'
gem 'omniauth-facebook'
```

の2行をGemfileに追記する。

そして、

```
$ bundle install
```

# **4. AppIDとSecretIDを追加**

config/initializers/devise.rbに下記のコードを追記する。。

```devise.rb
Devise.setup do |config|
  config.omniauth :facebook, 'App ID', 'App Secret'
end
```

ここでは、先ほど設定したFacebookアプリの「App ID」「APP Secret」の2つを付け足しましょう。

<img width="908" alt="p2me_―_設定_-_開発者向けFacebook.png" src="https://qiita-image-store.s3.amazonaws.com/0/269862/7e934531-5a89-201c-f52b-2b29e85e64a3.png">

# **5. deviseにメソッドを追加**

deviseの機能を拡張させる。
app/model/user.rbのファイルの末尾に「:omniauthable」を追加。

```user.rb
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable, :omniauthable
```

# **6. Userモデルにfindメソッド実装**

次に、app/model/user.rbモデルのファイルに、

```user.rb
class User < ApplicationRecord
  # ...

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid: auth.uid,
        provider: auth.provider,
        email: auth.info.email,
        password: Devise.friendly_token[0, 20]
      )
    end
    user
  end
end
```

# **7. コールバック処理の実装**

続いて、コールバック処理です。
app/controller以下に、usersフォルダを作成しましょう。
そのusersフォルダに、

> omniauth_callbacks_controller.rb

というファイルを作ります。このファイルはこんな感じで編集します。

```omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    callback_from :facebook
  end

  private
  def callback_from(provider)
    provider = provider.to_s

    # userモデルに定義したfinde_for_oauthメソッドを使用
    @user = User.find_for_oauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end
end
```

# **8. ルーティング設定**

コールバック用のルーティングを設定。
config/routes.rbのdevise_forの後ろに、コールバック用の記述を追記する。

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

http://localhost3000/users/sign_up
にアクセス。

> cannot load such file

エラーに遭遇。。
だが、サーバーを立ち上げなおすと解決！

<img width="562" alt="ログイン_-_ピトミー_ソーシャル自己分析サービス.png" src="https://qiita-image-store.s3.amazonaws.com/0/269862/cdbc0bed-ee58-04eb-cd0c-f26feb9bc73e.png">

よし、Sign in with Facebookでログイン。

<img width="1026" alt="Action_Controller__Exception_caught.png" src="https://qiita-image-store.s3.amazonaws.com/0/269862/bb049e14-763e-9e36-e3ce-3cea579abd26.png">

あら、またまたエラー。

> current_sign_in_at メソッドが定義されてないよ

というエラーですね。

エディタで検索をかけるとDeviseのmigrationファイルでヒットしました。

```2018...._devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|

    #...
      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

    #...
  end
end
```

`current_sign_in_at`　はTrackableに属しています。
つまり、おそらくuserモデルでTrackableを有効にしていることが原因でしょうか。
消してみましょう。

```user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable
end
```

↓

```user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable
end
```

というように変更し、サイドFacebookログインを試みると ...

無事ログインできました！
万歳。

ですが、またまた問題が発生。
データベースにユーザが登録されません。

```user.rb
def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first
    unless user
     user = User.new(
       uid: auth.uid,
       provider: auth.provider,
       email: auth.info.email,
       password: Devise.friendly_token[0, 20]
     )
    if user.save
      p "success"
    else
      p "failed"      
      p user.errors
    end
   end
   user
end
```

user.rbファイルを編集しました。
変更点はcreateメソッドではなくnew + saveメソッドを使うというところ。
createメソッドはnewメソッドとsaveメソッドを同時に行うメソッドであるため、これらに分解することで、エラーの発生箇所を特定することができます。

すると、エラーの際`p user.errors`により、エラー内容が出力されるので、確認すると、**エラーの原因はバリデーションでした**。

facebookログインの時は、バリデーションを無視させなければならないようです。
（そのほかに、良い方法があれば教えていただければ嬉しいです！）

よって、次のようにsaveのオプションとして、`validate: false`と追記すればデータがストアされました。


```user.rb
  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first
 
    unless user
      user = User.new(
        uid: auth.uid,
        provider: auth.provider,
        email: auth.info.email,
        password: Devise.friendly_token[0, 20],
        image: auth.info.image
      )
      user.save(:validate => false)
    end
    user
  end
```

エラーの原因特定のために記述した`if文`は消しております。


# **終わりに**
facebook認証の実装はコーディング以外にfacebook developerへの登録も必要ですので、少し手間はかかりますが、ぜひアプリケーションには導入したい機能ですよね。

何かご質問などございましたら、コメント欄などにてお願いします。
少しでも役にたったよー。って方は是非いいねお願いします(^^)


# **Rails関連記事**
[正規表現まとめ（基礎）[Ruby編]](https://qiita.com/takuyanin/items/2918ffb3151dcfa88565)
[配列で利用できる主なメソッドをまとめてみた[Ruby編]](https://qiita.com/takuyanin/items/3ca4cd7067b7b9e7e9b6)
[Mysql2::Error: Duplicate entry for key.. エラーを撃退した話(validationの設定)](https://qiita.com/takuyanin/items/b28d5111447d4dedc39d)
[Rails5でJqueryを利用しようとして少しハマった件（Uncaught ReferenceError: $ is not defined）](https://qiita.com/takuyanin/items/24444ec3005098471f3b)
[change_columnでの設定はrollbackできない話(This migration uses change_column, which is not automatically reversible.)[Rails: migration]](https://qiita.com/takuyanin/items/6d51ffb4078a417fed35)
[sessionに保存されたHashを別アクションで利用しようとした際にデータ型の変更によりハマった件[Rails]](https://qiita.com/takuyanin/items/e794e8bdb46287a2a269)