devise + omniauth で google/facebook/twitterアカウントでログイン認証を実装した際のメモ

#はじめに
devise と omniauth を利用したSNSアカウントでのログイン認証を実装してみました。
プログラム自体、書き始めてまだ2か月程なので、間違ってる部分とかかなりある気がしますが、
あくまでも自分用メモとして残しておこうと思います！

もしも参考にしていただける場合は、この記事を過信しすぎないようお願いします…(´・ω・`)

また間違いなどがありましたら、コメントいただけますと幸いですm(_ _)m

#動作環境

- AWS Cloud9
- ruby on rails -v 5.2.2
- devise
- omniauth-facebook (6.0.0)
- omniauth-google-oauth2 (0.8.0)
- omniauth-twitter (1.4.0)

#Gemfileにgemを追加して保存

```ruby:Gemfile
gem 'devise'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
```

#gemのインストール

```bash:terminal
$ bundle install
```

#Deviseのインストール
```bash:terminal
$ rails g devise:install
```

#DeviseでUserモデルの作成
```bash:terminal
$ rails g devise User
```

作成されたモデル **↓**

```ruby:app/model/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable  
end
```

gem omniauth を利用するために、devise:内に``:omniauthable`` を追加する。

```ruby:app/model/user.rb（omniauthable追加した状態）
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable
end
```

今回は、通常アカウント作成時にはユーザー名の登録を必須としたいのと、
SNSアカウントを利用したログインの場合はSNSアカウントで利用しているユーザー名をそのまま利用したいので、
usersテーブルにnameカラムを追加します。
また、``Facebook/Twitter/Google``アカウントでログインする場合に必要になる下記3つのカラムも同時に追加します。

**追加するカラム**
``name``/``provide``/``uid``/``image``

```bash:terminal
$ rails g migration add_columns_to_users provider uid name image
```

name については名無しは不可としたいので、マイグレーションファイルを開いて、
nameカラムに``null: false``を指定し、マイグレートします。

```ruby:db/migrate/add_columns_to_users.rb
class AddColumnsToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :name, :string, null: false
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_column :users, :image, :string
  end
end
```
```bash:terminal
$ rails db:migrate
```

#Twitter/Google/Facebook Developerアカウントの取得

こちらについては、まとめるとえらい時間かかりそうなので省略します・・・。
今後時間があるときにまとめようと思いますので、その時に追記します！
現時点でわからない方は、ググって頑張りましょう！
（完全初心者の僕でもどうにかなったので、たぶん大丈夫！）

#OAuthの設定

###omniauthを利用するための初期設定


``devise.rb``の250行目付近にある omniauth の設定箇所にて、
facebook/twitter/googleのプロバイダーを設定します。


環境変数の設定および、各種プロバイダーのApp_ID/Keyの取得については、
省略しますので、頑張ってググってください！

```ruby:config/initialize/devise.rb
　# ==> OmniAuth
  # Add a new OmniAuth provider. Check the wiki for more information on setting
  # up on your models and hooks.
  # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
  config.omniauth :facebook, ENV['FB_APP_ID'], ENV['FB_APP_SECRET'] #追加
  config.omniauth :twitter, ENV['TW_APP_ID'], ENV['TW_APP_SECRET'] #追加
  config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'] #追加
```
今回は``gem 'dotenv'``で環境変数を管理するので、Railsアプリのルートディレクトリに``.env``ファイルを作成し、
中身に以下の通り記述します。

```env:.env
FB_APP_ID=　Facebook Developersで取得したアプリIDを記載
FB_APP_SECRET=　Facebook Developersで取得したシークレットキーを記載
TW_APP_ID= Twitter Developersで取得したアプリIDを記載
TW_APP_SECRET= Twitter Developersで取得したシークレットキーを記載
GOOGLE_CLIENT_ID= Google APIsで取得したクライアントIDを記載
GOOGLE_CLIENT_SECRET= Google APIsで取得したシークレットキーを記載
```

###Userモデルにメソッドを追加

``app/models/user.rb``に以下のメソッドを追加します。

```ruby:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable 
         
  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.name = auth.info.name
      user.email = User.dummy_email(auth)
      user.password = Devise.friendly_token[0, 20]
      user.image = auth.info.image.gsub("_normal","") if user.provider == "twitter"
      user.image = auth.info.image.gsub("picture","picture?type=large") if user.provider == "facebook"
      user.image = auth.info.image if user.provider == "google_oauth2"
    end 
  end
  
  private
  
  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end
```

####``where(provider: auth.provider, uid: auth.uid).first_or_create``
各種SNSアカウントでログインを試みたユーザーの``porovider``/``uid``が存在するかを確認。

1. 存在していた場合：見つかったユーザーでそのままログインする
2. 存在していなかった場合：アカウントが自動生成され、そのままログインする
***

####``user.provider = auth.provider``
ログインに使用したプロバイダー名をusersテーブルのproviderカラムに追加
プロバイダー名は、twitter/facebook/googleの内、
``config/initialize/devise.rb``に追加した``config.omniauth``で設定したプロバイダ名が入る

####``user.uid = auth.uid``
ログインユーザー識別用IDをusersテーブルのuidカラムに追加

####``user.name = auth.info.name``
ログインに使用したアカウントのユーザー名を取得し、usersテーブルのnameカラムに追加
例）Twitterアカウントのユーザー名が``まひろ``だとすると、そのまま``まひろ``がnameカラムに追加される

####``user.email = User.dummy_email(auth)``
バリデーションでメールアドレスの登録は必須としているので、
providerとuidをミックスしてダミーメールアドレスを作成している

ダミーメールアドレスを作成している``self.dummy_email(auth)``メソッドはprivate部分に記述してます。

※各種デベロッパー側の設定によって、SNSアカウントのメールアドレスを取得できるようだけど、
　設定がややこしくてよくわからなかったので、とりあえず今回はダミーメールアドレス作成で乗り切ります

```ruby:app/models/user.rb
  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
```

####``user.password = Devise.friendly_token[0, 20]``
ログイン時にパスワード入力などは行わないが、パスワードは必須の為、
バリデーションに引っかかってエラーにならないようランダムパスワードを生成しています

####``user.image = auth.info.image.gsub("_normal","") if user.provider == "twitter"``

``user.provider``が``twitter``の時、``auth.info.image.gsub("_normal","")``が実行されます。
``auth.info.image``でTwitterのプロフィール画像のURLを取得しています。
※imageカラムに追加されるのは、画像ではなく、取得したプロフィール画像のURLとなります。

``.gsub("_normal","")``は、取得したプロフィール画像のURLから``"_normal"``という文字列を``""(空文字)``に置き換えています。
これを行う事で、取得したプロフィール画像が荒くなるのを防いでいます。
（※わかりやすくいうと、"_normal"を空文字に置き換える事で、実質的には指定の文字列を削除しています）

####``user.image = auth.info.image.gsub("picture","picture?type=large") if user.provider == "facebook"``

``user.provider``が``facebook``の時、``auth.info.image.gsub("picture","picture?type=large")``が実行されます。
``auth.info.image``でFacebookのプロフィール画像のURLを取得しています。
※imageカラムに追加されるのは、画像ではなく、取得したプロフィール画像のURLとなります。

``.gsub("picture","picture?type=large")``は、取得したプロフィール画像のURLから
"picture"という文字列を"picture?type=large"に置き換えています。
これを行う事で、大きめのプロフィール画像が取得されます。
（後でプロフィール画像のサイズを調整する際に荒くなりにくい）

####``user.image = auth.info.image if user.provider == "google_oauth2"``
``user.provider``が``google_oauth2``の時、``auth.info.image``が実行されます。
``auth.info.image``でGoogleのプロフィール画像のURLを取得しています。
※imageカラムに追加されるのは、画像ではなく、取得したプロフィール画像のURLとなります。

画像サイズについては、デフォルトでちょうどよいサイズなので特に指定はしていません。
サイズを変えたい場合は、取得したURLの末尾に``=wサイズの値（例：=w500）``を追加する処理を書く事で、
プロフィール画像のサイズを任意に変更できます。

#omniauth_callback_controllerの作成

omniauthを使ってログインする際のコールバックコントローラを作成します。
初期状態で、``devise/omniauth_callbacks#Action``が設定されていますが、
controllerフォルダにdeviseというフォルダがなく、実体化されていない？ため、必須の作業となるようです。
（※軽くググった限りでは、deviseフォルダのomniauth_callbacksを編集する方法については出てこなかったです…）

```bash:terminal
$ rails g devise:controllers users
```
```bash:app/controllers/users/

omniauth_callbacks_controller.rb # 使うのはこれだけ
confirmations_controller.rb
passwords_controller.rb
registrations_controller.rb
sessions_controller.rb
unlocks_controller.rb
```

コントローラを変更したいのは、``omniauth_callbacks_controller.rb``のみで、
他はdeviseの標準のコントローラを利用するため、他のコントローラファイルは削除します。

``routes.rb``にルーティングを設定します。

```ruby:routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root to: "toppages#index"

  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
end
```

生成された``omniauth_callbacks_controller.rb``を開いて、以下の内容を記述します。

```ruby:omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  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")
      redirect_to new_user_registration_url
    end
  end

  def twitter
    # 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: "Twitter") if is_navigational_format?
    else
      session["devise.twitter_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end
  
  def google_oauth2
    # 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: "Google") if is_navigational_format?
    else
      session["devise.google_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end

end
```

メソッド名は必ず、``config.omniauth``で設定したプロバイダー名を利用しましょう。
勝手なメソッド名をつけるとエラーになります。

これで、URLから``ドメイン名/users/sign_up``にアクセスして、
各種プロバイダーでのサインアップリンクをクリックすれば、SNSアカウントでのログインに成功すると思います。

以上で完了です。
細かいデザインなどはまた別の機会にやる事にします。

#終わり
書くのめっちゃ疲れたけど、だいぶ頭の中が整理できた気がする…

間違ってる部分もあると思います…（間違っている箇所に気づける程、まだ慣れていません…
もし気づいた点などありましたら、コメントください・・・