OmniAuthの公式のwikiを読んでみた。

##はじめに

[OmniAuthの公式のwiki](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview#facebook-example)を読んで学習した内容を備忘録としてこちらに投稿します。

##OmniAuthって？
Deviseのバージョン1.2から追加された、[OAuth](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)に関するモジュールのこと。
このモジュールを利用すれば、twitterやfacebookといったプロバイダーに登録されている情報でユーザー認証を行うことができます。

##Before you start
config.omniauthは、アプリケーションにomniauth プロバイダーのミドルウェアを追加します。これは、config/initializers/omniauth.rbにconfig.omniauthを記述するべきではないことを意味します。なぜなら、そうしてしまうとお互いのomniauth プロバイダーのミドルウェアが衝突し、認証ができない事態に陥ってしまうからです。

config/initializers/devise.rbにだけ、config.omniauthを記述するようにしましょう。

##Facebook example
まずは、以下のgemをGemfileに追加してください。

##1. gemの追加
~~~:gemfile
gem 'omniauth-facebook'
gem 'omniauth-rails_csrf_protection'
~~~
###gem 'omniauth-facebook'
facebookのomniauth機能を追加するためのgem
"omniauth-#{provider}"の形で各プロバイダーのomniauthのgemが提供されている。
各プロバイダーについては[こちら](https://github.com/omniauth/omniauth/wiki/List-of-Strategies)から確認してください。

###gem 'omniauth-rails_csrf_protection'
OmniAuth2.0から
CSRF脆弱性 [CVE-2015-9284](https://nvd.nist.gov/vuln/detail/CVE-2015-9284)の対応のためにインストールが必要なgem
これがないと「Not found. Authentication passthru.」と表示され認証が失敗します。
参考：[OmniAuth - Rails CSRF Protection](https://github.com/cookpad/omniauth-rails_csrf_protection)

バージョン1系からの変更点については、[こちら](https://github.com/omniauth/omniauth/releases/tag/v2.0.0)でご確認ください。

変更点について簡単に紹介しておくと、

1. サービスプロバイダーのサービス認可画面へリダイレクトするエンドポイントはデフォルトでGET/POSTどちらも有効でしたが、2.0からPOSTのみに変更となりました。
1. gem omniauth-rails_csrf_protection'のようなCSRF用validatorを使用すること

これらの変更は、１系でCSRF脆弱性がみつかったため、それに対応するためのものとのことです。

1.について、どうしてgetメソッドがダメなのか調べてみたところ、どうやらgetメソッドはCSRFtokenの漏洩リスクが高いようです。

参考：[Cross-Site Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#cross-site-request-forgery-prevention-cheat-sheet)
##2.検索用カラムの追加
Deviseは、uidとproviderの情報を使って、DBからデータを検索するので、テーブルに存在していない場合追加する必要があります。

~~~bash
rails g migration AddOmniauthToUsers provider:string uid:string
rake db:migrate
~~~

##3.認証用プロバイダーの宣言
deviseが認証で利用するプロバイダーを識別できるように、config/initializers/devise.rbに以下のように追記します。

~~~ruby:config/initializers/devise.rb
config.omniauth :facebook, "APP_ID", "APP_SECRET"
~~~
“Invalid credentials”等の理由でFacebookで認証ができなかった場合は、token_params: { parse: :json }を追記してください。

~~~ruby:config/initializers/devise.rb
config.omniauth :facebook, "APP_ID", "APP_SECRET", token_params: { parse: :json }

~~~

##4.モデルの設定
deviseにOmniAuthのプロバイダーを識別させるためには、config/initializers/devise.rbへの設定と別にモデルへの設定も必要です。以下のように記述してください。

~~~ruby:app/models/user.rb
devise :omniauthable, omniauth_providers: %i[facebook]
~~~

ここまで、できたら変更をdeviseに知らせるためにrailsをリスタートさせましょう。

認証に複数のプロバイダーを利用したい場合は、[こちら](https://github.com/heartcombo/devise/wiki/OmniAuth-with-multiple-models)を参照してください。

##5.Deviseのurlメソッドについて

~~~ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
~~~

~~~ruby:app/models/user.rb
devise :omniauthable, omniauth_providers: %i[facebook]
~~~
Userモデルでomniauthable、routes.rbでdevise_for :users の設定ができたらdeviseは以下２つのurlメソッドを作成します。この時、*_urlというメソッドは作成されません。

1. user_{provider}_omniauth_authorize_path
1. user_{provider}_omniauth_callback_path


OmniAuth 2.0+からは、HTTP GETは許可されておらず、HTTP POSTを使う必要があります。button_toヘルパーを使用するか、link_toヘルパーを使用する際は、method: :postの記述を入れるようにしましょう。

~~~html:app/views/devise/shared/_links.html.erb
<%= link_to "Sign in with Facebook", user_facebook_omniauth_authorize_path, method: :post %>

# you can also switch to using `button_to`, which doesn't require rails-ujs for performing POST requests:
<%= button_to "Sign in with Facebook", user_facebook_omniauth_authorize_path %>

~~~
HTTP GETは、CSRF脆弱性をついた攻撃を受ける可能性があるようです。
参考：[possibility of CSRF attacks.](https://nvd.nist.gov/vuln/detail/CVE-2015-9284)

##6.Omniauth callbacksの設定
###1. routes.rbの設定
link_to,button_toがクリックされると、ユーザーは、Facebookのページにリダイレクトされ、そこから認証情報を取得します。
認証情報の取得後に、元のアプリケーションにリダイレクトバックする必要があるため、config/routes.rb以下のように編集します。

~~~ruby:config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
~~~
これで、Deviseは、Devise側のOmniauthCallbacksControllerではなく、自身のusersフォルダー配下のOmniauthCallbacksControllerを参照するようになります。

###2.omniauth_callbacks_controller.rbの設定
以下のファイルを作成します。

~~~ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
~~~
Devise::OmniauthCallbacksControllerをUsers::OmniauthCallbacksControllerが継承しています。これで、devise側のコントローラーをオーバーライドできます。
###3.OmniauthCallbacksControllerのオーバーライド
コールバックコントローラーには、OmniAuthで利用するプロバイダーと同名のアクション（メソッド）を定義する必要があります。今回は、Facebookを利用しているので以下のように編集します。

~~~ruby:app/controllers/users/omniauth_ca
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  # See https://github.com/omniauth/omniauth/wiki/FAQ#rails-session-is-clobbered-after-callback-on-developer-strategy
  skip_before_action :verify_authenticity_token, only: :facebook

  def facebook
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication # this will throw if @user is not activated
      set_flash_message(:notice, :success, kind: "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"].except(:extra) # Removing extra as it can overflow some session stores
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

~~~
####1.facebookアクションについて特筆すべき点
1. OmniAuthで取得した、Facebookの全ての情報は、request.env["omniauth.auth"]に格納され、ハッシュとして利用できます。request.env["omniauth.auth"]の中身については[こちら](https://github.com/simi/omniauth-facebook#auth-hash)で確認ください。
1. Userモデルに既にユーザーが登録されていた場合は、sign_in もしくは sign_in_and_redirectのいずれかでサインインさせます。[Warden callbacks.](https://stackoverflow.com/questions/9221390/what-does-event-authentication-do/13389324#13389324)を利用したい場合は、:event => :authenticationオプションを使います。
1. あなた次第ですが、flashメッセージについては、deviseのデフォルトのものを使用することができます。
1. Userモデルにユーザーが登録されていなかった場合は、sessionにOmniAuthで取得したFacebookの全ての情報を保存します。この時、sessionのキーを"devise."で始まるように命名していれば、ユーザーのサインイン後に自動でこのsession情報を削除してくれます。そして最終的には、ユーザーをregistration formにリダイレクトさせます。

###2.CookieOverflow
ちなみに、以下の部分を実行した際にエラーが発生しました。

~~~ruby
session["devise.facebook_data"] = request.env["omniauth.auth"].except(:extra) # Removing extra as it can overflow some session stores
~~~
こんなエラーが発生します。#twitterとなっているのは、私が、twitterOmniAuthの実装をしていたからです。facebookでも同様のエラーが起きます。
![スクリーンショット 2021-10-08 9.33.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/c476803e-62cb-e649-df20-d7acafddc2ee.png)

####エラー原因について
railsのデフォルトのsession storeは、cookieベースとなっています。
cookieの場合、約４KB(4,096byts)がデータを保存できる限界容量となっています。限界容量を超えてしまうとオーバーフローしてしまいます。
今回は、request.env["omniauth.auth"].except(:extra)の情報がcookieの限界容量を超えたため、オーバーフローによるエラーが発生してしまいました。
オーバーフローしないようにデータ容量の大きい(:extra)の部分をexceptしていますが、cookieではそれでも限界容量を超えてしまうようです。
参考：[ActionDispatch::Session::CookieStore ](https://api.rubyonrails.org/v6.1.3/classes/ActionDispatch/Session/CookieStore.html)

cookieオーバーフローの解決策としては、session storeをより容量のあるストアに変更することです。

Active Recordを用いてデータベースに保存する 方法が一番いいかと思います。(activerecord-session_store gemが必要)これについては、別記事に載せる予定です。
詳細については、以下でご確認ください。
参考:[Ruby on Rails ガイド](https://railsguides.jp/action_controller_overview.html#%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3)


##7.self.from_omniauth(auth)メソッドの定義
コントローラーの設定完了後、app/models/user.rbにfacebookアクションで登場した、self.from_omniauth(auth)メソッドを定義します。

~~~ruby:app/models/user.rb
def self.from_omniauth(auth) 
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0, 20]
    user.name = auth.info.name   # assuming the user model has a name
    user.image = auth.info.image # assuming the user model has an image
    # If you are using confirmable and the provider(s) you use validate emails, 
    # uncomment the line below to skip the confirmation emails.
    # user.skip_confirmation!
  end
end
~~~
このメソッドは、providerとuidの情報で、Userモデルからユーザーを探します。
もし、ユーザーが見つからなかった場合、ランダムなパスワードと、その他の情報を持った新たなユーザを作成します。
注意するべき点として、first_or_createメソッドは新たにユーザーを作成する場合、providerとuidの情報を以下のように自動で登録してくれます。
user.provider=auth.provider
user.uid=auth.uid
first_or_create! メソッドは、first_or_createメソッドと同様に動作しますが、ユーザーレコードがバリデーションに引っかかる等で正常に登録できない場合は、例外エラーを発生させます。

加えて、first_or_createメソッドについて注意するべき点としては、ユーザーが見つかった場合、そのユーザーのプロバイダーから取得したデータが前回登録時から更新されていたとしても、そのデータを更新できないということです。このことについては、[こちら](https://dev.to/rodreegez/creating-and-updating-users-with-omniauth-4b08)でご確認ください。

実は、今回紹介した、first_or_createメソッドは、railsの[過去のパブリックAPI](https://guides.rubyonrails.org/v3.2.13/active_record_querying.html#first_or_create)には載っているのですが、最新のパブリックAPIには、載っていないんです。代わりに、[最新のパブリックAPI](https://railsguides.jp/active_record_querying.html#find-or-create-by)には、find_or_create_byが載っています。
このことについて、調べたところ、first_or_createメソッドは、where句を伴わないと期待する動作にならず、分かりずらいのが原因ではないかとのことでした。

参考：[Rails first_or_create vs find_or_create_by on ActiveRecord](https://rubyinrails.com/2020/05/26/rails-first-or-create-vs-find-or-create-by/)
参考：[first_or_create vs find_or_create_by](https://stackoverflow.com/questions/42178674/first-or-create-vs-find-or-create-by)

以上を踏まえて、今風にself.from_omniauthメソッドを定義すると以下のようになるかと思います。

~~~ruby
def self.from_omniauth(auth)
    find_or_create_by(provider: auth.provider, uid: auth.uid) do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
      user.name = auth.info.name   # assuming the user model has a name
      user.image = auth.info.image # assuming the 
      user model has an image
    # If you are using confirmable and the provider(s) you use validate emails, 
    # uncomment the line below to skip the confirmation emails.
    # user.skip_confirmation!
    end
end
~~~

##8.new_with_sessionについて
DeviseのRegistrationsControllerは、デフォルトの挙動として、リソースをビルドする前にUser.new_with_sessionを実行します。これは、次のことを意味します。
サインアップ前に常にユーザーは初期化されてしまいます。そのため、セッションからデータをコピーしておく必要がある場合は、ユーザーモデルにnew_with_sessionメソッドを定義する必要があります。

~~~ruby:app/models/user.rb
class User < ApplicationRecord
  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
      end
    end
  end
end
~~~
このコードを理解するためには、以下の二つのメソッドを理解する必要があります。

1. super
1. tap

####1.superについて

> superクラスとは、オーバーライドされる前のメソッドを呼び出すことができるメソッドです。言い換えれば、継承元のメソッドを出力することが可能です。

引用：[superクラスとは](https://techtechmedia.com/super-overwrite-ruby/)

####2.tapについて

>tap {|x| ... } -> self
self を引数としてブロックを評価し、self を返します。
メソッドチェインの途中で直ちに操作結果を表示するためにメソッドチェインに "入り込む" ことが、このメソッドの主目的です。

引用：[Ruby 3.0.0 リファレンスマニュアル ](https://docs.ruby-lang.org/ja/latest/method/Object/i/tap.html)

~~~ruby
hash = {}
#  => {}
hash.tap{ |h| h[:value] = 42 }
#  => {:value=>42}
hash
#  => {:value=>42}
~~~
> tap の場合は、ブロックの内容にかかわらずレシーバー自身が返り

引用：[Ruby: Object#tap、Object#then を使ってみよう](https://techracho.bpsinc.jp/kazz/2019_12_23/85305)

super.tapで継承元のself.new_with_sessionメソッドの内容を上書きしているようです。
参考：[What does self.new_with_session(params, session) do in this case](https://stackoverflow.com/questions/44446166/what-does-self-new-with-sessionparams-session-do-in-this-case)

ここまでを理解したうえで、もう一度コードを見てみましょう。

~~~ruby
class User < ApplicationRecord
  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
      end
    end
  end
end
~~~

まず、superについて、

User＞ApplicationRecord
Userモデルは、ApplocationRecordモデルを継承しています。
ApplicationRecordは、deviseインストール時に、deviseのモジュールをインクルードしています。
つまり、
superとは[devise/lib/devise/models/registerable.rb](https://github.com/heartcombo/devise/blob/master/lib/devise/models/registerable.rb)で定義された以下のnew_with_sessionメソッドのことを表しています。

~~~ruby:devise/lib/devise/models/registerable.rb
 # A convenience method that receives both parameters and session to
        # initialize a user. This can be used by OAuth, for example, to send
        # in the user token and be stored on initialization.
        #
        # By default discards all information sent by the session by calling
        # new with params.
        def new_with_session(params, session)
          new(params)
        end
~~~

次に、tap以下の処理について、
tapメソッドの処理に当てはめると、、、

:::note info
super を引数としてブロックを評価し、super を返します。
:::
このtapメソッドの戻り値は、以下のブロック処理の中身となります。

~~~ruby
if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
~~~
ここまで来れば、以下のメソッドの意味がわかりますね。

~~~ruby
class User < ApplicationRecord
  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
      end
    end
  end
end
~~~
つまり、[devise/lib/devise/models/registerable.rb](https://github.com/heartcombo/devise/blob/master/lib/devise/models/registerable.rb)で定義されたnew_with_sessionメソッドの内容をブロック処理の内容で上書きするということになります。
メソッドの挙動としては、サインアップページにリダイレクトバック後に、フォームをemail入力部が空白であった場合、session["devise.facebook_data"]内のemailの情報を補完するというもの。
自動入力が実現できます。

##9.Facebook認証でのサインアップを取消す設定
ユーザーにFacebook認証でのサインアップをキャンセルすることを許可したい場合は、cancel_user_registration_pathにリダイレクトさせるようにします。そうすることで、"devise."で始まるsessionの情報を削除することができ、それ以降new_with_sessionメソッドが呼び出されることはなくなります。

##10.Logout linksの設定
ログアウト用のリンクを設定します。　

~~~ruby:config/routes.rb
devise_scope :user do
  delete 'sign_out', :to => 'devise/sessions#destroy', :as => :destroy_user_session
end
~~~

これで、お終いです！お疲れ様でした！
実装がうまくいっていることができれば、次は、実装テストに移りましょう。
https://github.com/omniauth/omniauth/wiki/Integration-Testing

##参考
[OmniAuth: Overview](https://github.com/heartcombo/devise/wiki/OmniAuth%3A-Overview)
[ActionDispatch::Session::CookieStore ](https://api.rubyonrails.org/v6.1.3/classes/ActionDispatch/Session/CookieStore.html)