【Rails】Sorceryでfacebook認証　Sorceryの設定

# はじめに

mkcertとSorceryの拡張機能を用いて、ローカル環境でfacebook認証機能を追加しました。
Sorceryのwikiが少し古かったり、facebook for developersとの連携に苦戦したので、備忘録がてら書いておこうと思います。
また、設定に不備があった場合のエラーについては[別記事](https://qiita.com/aiandrox/items/0bd2877820bc779c96e4)で書いています。

## 動作環境

```
ruby 2.6.5
Rails 5.2.3
sorcery 0.14.0
mkcert 1.4.1
```

## 前提

- sorceryによるログイン機能
- mkcertでのHTTPS通信を許可
- facebook for developersで「facebookログイン」の作成

は済ませていることとします。

# 簡単な流れ

[Sorceryのwiki](https://github.com/Sorcery/sorcery/wiki/External)にチュートリアルがあるので、基本的にはそれに従っていきます。

## authenticationsテーブルを作成

```terminal
$ rails g sorcery:install external --only-submodules
        gsub  config/initializers/sorcery.rb
      insert  app/models/user.rb
      create  db/migrate/20200203110653_sorcery_external.rb
```

コマンドを打つと、外部認証に必要なauthenticationsテーブルを作成するためのマイグレーションファイルが作られます。

```ruby:db/migrate/20200203110653_sorcery_external.rb
class SorceryExternal < ActiveRecord::Migration
  def change
    create_table :authentications do |t|
      t.integer :user_id, :null => false
      t.string :provider, :uid, :null => false
    
      t.timestamps
    end

    add_index :authentications, [:provider, :uid]
    add_index :authentications, :user_id # user_idにindexを貼る場合は追加
  end
end
```

```teminal
$ bundle exec rails db:migrate
```


## Authenticationモデルの設定

```terminal
$ rails g model Authentication --migration=false
```

先ほど既にマイグレーションファイルは作成しているので、`--migration=false`オプションを付けています。

```rb:app/models/authentication.rb
class Authentication < ActiveRecord::Base
  belongs_to :user
end
```

```rb:app/models/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :authentications, dependent: :destroy
  accepts_nested_attributes_for :authentications # has_many :authenticationsより下に書く
end
```

ここまではfacebook認証に限らず、外部認証共通の処理となります。

## sorcery.rbの設定

sorcery.rbの以下の部分のコメントアウトを外します。
`blabla`って何のことだ？って思ったんですが、英語で「などなど」って意味なんですね。

```rb:config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:external, blabla, blablu, ...] # コマンドを打つと自動的に追加される

Rails.application.config.sorcery.configure do |config|
  ...
  config.external_providers = [:facebook, blabla, ...]  # 外部認証に使用するもののみ追加
  # 例 [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid, :salesforce, :slack, :line]
  ...
  config.facebook.key = Rails.application.credentials.dig(:sorcery, :facebook, :key) # 1.で解説
  config.facebook.secret = Rails.application.credentials.dig(:sorcery, :facebook, :secret) # 1.で解説
  config.facebook.callback_url = 'https://localhost:3000/oauth/callback?provider=facebook' # 2.で解説
  config.facebook.user_info_path = 'me?fields=email' # 3.で解説
  config.facebook.user_info_mapping = { email: 'email' } # 3.で解説
  config.facebook.access_permissions = %w[email] # 3.で解説
  config.facebook.display = 'page'
  config.facebook.api_version = 'v6.0' # 4.で解説
  config.facebook.parse = :json
  ...

  # --- user config ---
  config.user_config do |user|
  ...
    # -- external --
    user.authentications_class = Authentication # 認証用のモデルがAuthenticationではない場合は変更する
    ...
  end
  ...
end
```

`config.facebook`に関する最初の3行は、環境によって値を変更することになると思いますが、ここでは省略します。[Config](https://github.com/rubyconfig/config)などを使ってください。

### 1. facebook keyとsecret

facebook keyとsecretは外部には漏らしたくないため、[credentials](https://railsguides.jp/security.html#%E7%8B%AC%E8%87%AA%E3%81%AEcredential)で管理します。
参考：[Rails5.2から追加された credentials.yml.enc のキホン](https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39)

```terminal
$ rails credentials:edit
```

```credentials.yml
# ここの構成は自由ですが、Rails.application.credentials.dig(:sorcery, :facebook, :key)のように対応するようにしてください。
sorcery:
  facebook:
    key: 'アプリIDの値'
    secret: 'app secretの値'
```

[facebook for developers](https://developers.facebook.com/)からマイアプリにアクセスし、「ダッシュボード」下の「設定」→「ベーシック」の画面から「アプリID」と「app secret」を確認し、記述してください。
IDは15桁の数字、secretは英数字のハッシュとなっています。
<img width="1039" alt="スクリーンショット 2020-02-05 21.49.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/148ad498-54f2-91d9-73c3-ea5fae2c9cb2.png">

### 2. callback_url

HTTPS化をしているため、デフォルトの`"http://0.0.0.0:3000/oauth/callback?provider=facebook"`ではエラーが出ます。
SSL証明書の発行されている`https://localhost:3000/oauth/callback?provider=facebook`をここに記述します。
そして、「Facebookログイン」「設定」の「有効なOAuth リダイレクトURI」にも同じものを登録します。
<img width="1019" alt="スクリーンショット 2020-02-05 20.19.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/06068b1b-e142-f899-73e4-94a81f86a628.png">

### 3. facefookから取得するデータ

デフォルトでfacebookから取得できるデータは以下の通りです。
>id
first_name（名）
last_name（姓）
middle_name（ミドルネーム）
name（フルネーム）
name_format（デフォルトは`{last}{first}`）
picture（プロフィール画像）
short_name（設定されていない場合はフルネーム）
[アクセス許可のリファレンス](https://developers.facebook.com/docs/facebook-login/permissions/#user-data)

それ以外のデータを取得したい場合は、`config.facebook.access_permissions`に記述します。
emailを取得するためにはfacebook側の手続きは要りませんが、それ以外にはアプリレビューが必要です。

Email、姓名、プロフィール画像を取得してUserに入れる場合はこのようになります。

```rb
config.facebook.user_info_path = 'me?fields=email,first_name,last_name,picture.type(large)'
# facebookから取得するデータの受け取り方
config.facebook.user_info_mapping = { email: 'email', first_name: 'first_name', last_name: 'last_name', remote_avatar_url: 'picture/data/url' }
# facebook側の属性名とUserモデルの属性を対応させる
config.facebook.access_permissions = %w[email]
# デフォルト以外のデータを取得する場合はここに書く
```

プロフィール画像の取得方法は他と少し違うので注意が必要です。
参考：[Sorcery + CarrierWave で Facebook 認証時に大きめのアイコン画像を保存する](https://qiita.com/9m/items/a7d811029cfee201321f)

### 4. APIのバージョン

[facebook APIのバージョン](https://developers.facebook.com/docs/apps/versions?locale=ja_JP)は「Facebookログイン」の「クイックスタート」から「ウェブ」「2. JavaScript用Facebook SDKを設定する」で確認できます。
<img width="1027" alt="スクリーンショット 2020-02-05 21.59.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/8d9dad99-07f3-a03e-ff53-81844ded139e.png">

参考：[自分が管理するFacebookアプリのAPIバージョン確認方法](https://snowadays.jp/2014/09/3151)
少しレイアウトが変わっていますが、こちらの方法でも確認できます。

## Oauth処理を行うコントローラを作成

```terminal
$ rails g controller Oauths oauth callback
```

```rb:app/controllers/oauths_controller.rb
class OauthsController < ApplicationController
  skip_before_action :require_login # applications_controllerでbefore_action :require_loginを設定している場合

  def oauth
    login_at(auth_params[:provider])
  end

  def callback
    provider = auth_params[:provider]
    if (@user = login_from(provider))
      redirect_to root_path, notice: "#{provider.titleize}でログインしました"
    else
      begin
        @user = create_from(provider)
        reset_session
        auto_login(@user)
        redirect_to root_path, notice: "#{provider.titleize}でログインしました"
      rescue StandardError
        redirect_to root_path, alert: "#{provider.titleize}でのログインに失敗しました"
      end
    end
  end

  private

  def auth_params
    params.permit(:code, :provider)
  end
end
```

基本的にはチュートリアルの通りです。多少、Rails5の書き方に直します。

## ログインボタンを追加

```erb:login_page.html.erb
<%= link_to 'Login with Facebook', auth_at_provider_path(provider: :facebook) %>
```

## ルーティングの追加

```rb:config/routes.rb
post "oauth/callback", to: "oauths#callback"
get "oauth/callback", to: "oauths#callback"
get "oauth/:provider", to: "oauths#oauth", as: :auth_at_provider
```

```terminal
          oauth_callback POST   /oauth/callback(.:format)                                                                oauths#callback
                          GET    /oauth/callback(.:format)                                                                oauths#callback
         auth_at_provider GET    /oauth/:provider(.:format)                                                               oauths#oauth
```

#おわりに

ご指摘などがございましたら、コメントか編集リクエストでお願いいたします。