【Rails】SNS認証(Twitter、Facebook、google)

- ユーザー登録において、メールアドレス認証の他に、Twitter、Facebook、googleなどのSNS認証機能を付けたい。
- ライブラリ（Gem）のOmniAuthを使用( **SNSのAPIにリクエストを送ると、API上で認証後、SNSの登録情報がアプリ側に返される**流れ)。
- 参: [OmniAuth のGithub](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview)
- 参: [google-oauth2 のGit Hub](https://github.com/zquestz/omniauth-google-oauth2)

# 実装する機能
1. メールアドレスでのユーザー登録。
1. SNS認証でのユーザー登録（**名前とメールアドレスを自動入力、PW不要。その他の情報は入力させる**）。
1. メールアドレス登録済みなら、SNSを紐付ける

# 手順
## 1. API側の設定 〜 アプリ側の基本設定
- 各サイトでAPI登録し、環境変数で、omniauthを設定。
- [Qiita: Twitter、Facebook、googleのAPI登録までの手順](https://qiita.com/manbolila/items/8caa1f5d2b1fb96d2646)

## 2. アプリ側の実装(メソッドの設定)
### 2-0. （事前準備）メールアドレスでの登録機能の実装
- 追加したカラムを受け取るためのストロングパラメータ設定。

```rb:application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?

private
def configure_permitted_parameters  # メールアドレス以外の自分で追加したカラムを許可
  devise_parameter_sanitizer.permit(:sign_up, keys: [:name, ..])
end
```
- 仮の確認用のビューを作成. 

```haml:users/index.html.haml
- if user_signed_in?
  = "#{current_user.name}でログイン中"
  = link_to 'ログアウト', destroy_user_session_path, method: :delete
- else
  = link_to '新規登録', new_user_path
  = link_to 'ログイン', new_user_session_path
```
```haml:users/new.html.haml
= link_to 'メールアドレスで登録', new_user_registration_path
```
ここで、動作確認しとく。


### 2-1. SNS認証(OmniAuth)の実装
- 今回は、SNS認証と、Userモデルに保存するタイミングが異なる！
  - SNS情報から、取得したユーザー情報を照合（今回は、クラスメソッドで切り出して実装）。
  - SNS照合結果から、アプリ登録済みユーザーか? → ログイン or 新規登録画面へ遷移。
- [公式ドキュメント](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview)には、「登録ボタンクリック → ユーザー登録完了」の仕様が記載。

#### コントローラー
- twitter、facebook、google_oauth2アクションから処理(コールバック関数名：authorization)を呼ぶ。
  - SNS認証時に呼ばれるコールバック関数名は決まってる。
- APIからのレスポンスが`request.env["omniauth.auth"]`変数に入る。
- DB操作を行うメソッド`User.from_omniauth`を作り、変数を渡す。
  - このメソッドで、**Userモデルのインスタンスを返してもらって、＠userに代入**しとくことで、deviseのヘルパーが使えるようになる。
- ＠userが未保存(新規登録)の場合、ビューに＠user変数を渡したいので、renderを使用。

```rb:users/omniauth_callbacks_controller.rb
  def twitter
    authorization   # コールバック
  end

  def facebook
    authorization   # コールバック
  end

  def google_oauth2
    authorization   # コールバック
  end

  def failure
    redirect_to root_path
  end

  private
  # コールバックのメソッド定義
  def authorization   # APIから取得したユーザー情報はrequest.env["omniauth.auth"]に格納されてる
    sns_info = User.from_omniauth(request.env["omniauth.auth"])     # User.from_omniauth は、モデルで定義（次項）
    @user = sns_info[:user]    # deviseのヘルパーを使うため、＠user に代入(ハッシュ(モデルの返り値)から値を取得)

    if @user.persisted? # ユーザー登録済み(ログイン処理)
      sign_in_and_redirect @user, event: :authentication   # authenticationのcallbackメソッドを呼んで、@user でログイン
      set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
    else                # ユーザー未登録(新規登録画面へ遷移)
      @sns_id = sns_info[:sns].id                  # ハッシュ(モデルの返り値)から取得した、認証データを一時的に保持(ユーザー登録ページに渡す)
      render template: new_user_registration_url   # ユーザー登録ページに遷移
    end
  end
```

##### 参）`request.env["omniauth.auth"]` に格納されてる情報
- `provider` （必須） : プロバイダー（例：twitter、facebookなど）
- `uid`（必須） : プロバイダーに固有の識別子(文字列)。
- `info` （必須） : ユーザー情報(ハッシュ)
  - **name**（必須） : 表示名(通常、姓名の連結。指定子、ニックネームの場合も)。
  - **email** : メールアドレス（一部サイトでは提供なし）
  - **nickname** : ユーザー名（Twitterの@名、アカウント名など）
  - **first_name**
  - **last_name**
  - **location** : ユーザーのロケーション(通常、市と州)。
  - **description** : ユーザー説明文。
  - **image** : プロフィール画像(URL、50x50ピクセルの正方形)。
  - **phone** : 電話番号
  - **urls** : サイト/URL識別子のキー/値のペアを含むハッシュ(例: "Blog" => "http：//intridea.com/blog")
- `credentials` : アクセストークンや資格情報を提供する場合、これを通過する。
  - **token** : OAuth、OAuth 2.0プロバイダーが提供するアクセストークン。
  - **secret** : OAuthプロバイダーが提供するアクセストークンシークレット。
  - **expires** : アクセストークンに有効期限があるかを示すブール値
  - **expires_at** : 有効期限(FacebookとGoogle Plusは返す。Twitter、LinkedInにはない)。
- `extra` : 追加情報(プロバイダー固有の形式)。
  - **raw_info** : 全情報のハッシュ(例：Twitterの場合、APIから返されたJSONハッシュ)。





#### モデル
- `User.from_omniauth` メソッドの中身を作成(ユーザー登録で渡すデータの定義)。
- sns認証したことがあれば、アソシエーションで取得し、ログイン。なければ、SNS認証時のnameとemail情報を取得。

```rb:SNS認証のコールバック時に呼ばれるメソッドを定義(user.rb)
def self.from_omniauth(auth)       # snsから取得した、providerとuidを使って、既存ユーザーを検索
  sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create  # sns認証したことがあればアソシエーションで取得、なければSns_credentialsテーブルにレコード作成

  # snsのuser or usersテーブルに対し、SNS認証で取得したメールアドレスが登録済みの場合は、取得 or なければビルド(保存はしない)
  user = sns.user || User.where(email: auth.info.email).first_or_initialize(
    name: auth.info.name,
    email: auth.info.email
  )
  if user.persisted?   # userが登録済みの場合：そのままログインするため、snsのuser_idを更新しとく
    sns.user = user
    sns.save
  end
  { user: user, sns: sns }   # user、snsをハッシュで返す(コントローラーがこれを受け取る)
end
```
- omniauth_callbackコントローラー → APIにリクエストを送る。コントローラーでの処理はないので、passthruアクション不要。
  - `new_record?` : 未保存なら、trueを返す
  - `persisted?` : 未保存、かつ、削除履歴ナシなら、trueを返す




#### ビュー
- SNS認証用のリンクを設置。
- このリンクは、SNS認証での新規登録/ログインを兼ねてる。

```haml:例)自分で設置する場合(users/new.html.haml、devise/sessions/new.html.haml)
= 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
```
```haml:例)共通化する場合(devise/shared/_links.html.haml)
- if devise_mapping.omniauthable?
  .omniauth_btn
    - resource_class.omniauth_providers.each do |provider|
      = link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider)
```
- 最初のSNS認証時は、user_idがない（user_idが確定してない）。なので、sns保存には、モデルに、**belongs_to user, optional: true**オプション(外部キーのnil許可)を忘れずに！
- ２回目以降のログイン時/SNS認証時は、登録済みのため、user_idカラムが更新される。




#### SNS認証時、PW不要にする
- SNS認証時だけ、PWを自動生成するイメージ。
- 定義した変数を使って、PW入力欄の表示/非表示を分ける。

```haml:devise/registrations/new.html.haml
- if @sns_id.present?  # ＠sns_idがあれば、非表示(hidden_fieldで、params[:sns_auth]としてtrueを返す)
  = hidden_field_tag :sns_auth, true
- else   # ＠sns_id がなければPW入力欄を表示
  .field
    = f.label :password
    %em (#{@minimum_password_length} 文字以上)
    %br/
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    %br/
    = f.password_field :password_confirmation, autocomplete: "new-password"
```
- paramsは通常、devise/registrationsのcreateアクションに送られるが、今回は、コントローラーをオーバーライドする。users/registrations_controller.rbを作成( createアクションの動作を変更(SNS認証時のみPW不要)したいため )。
  - **オーバーライド** : 親クラスで定義したメソッドと同じ名前のメソッドを、子クラスでも定義した場合、子クラスで定義したメソッドの方が実行されること。

```rb:users/registrations_controller.rb
def create
  if params[:sns_auth] == 'true'
    pass = Devise.friendly_token    # PWの自動生成
    params[:user][:password] = pass
    params[:user][:password_confirmation] = pass
  end
  super
end
```
- rubyでは、継承先クラスで**superメソッド**を使うと、親モデルの同名メソッドを実行できる(devise/registrationsのcreateアクションを呼んでくれる)。
  - params[:sns_auth]が送られた時だけ、Devise.friendly_tokenでPWを自動生成し、それ以外の場合はsuperメソッドで、deviseに処理を任せる。