SNSログイン機能(Google,Twitter(X),Facebook)

# SNSログイン機能
<br>

SNSログイン機能の機能実装は共通している部分が多いため、一気にGoogle, Twitter, Instagramのログイン機能を実装する。
個別で必要な時は適宜不必要な部分を削除して使用してください。

## 環境
OS：macOS Monterey ver 12.0.1 (M1, 2020)
Ruby：3.2.1
Rails：7.0.5
MySQL：8.0.33
OAuth：2.0

## 技術
OAuth
OmniAuth

## Gem
**全対象**：Devise(認証), omniauth-rails_csrf_protection(CSRF攻撃対策)
**Google認証**：omniauth-google-oauth2
**Twitter認証**：omniauth-twitter
**Facebook認証**：omniauth-facebook

##  前提条件
Deviceを用いた新規登録・ログイン・ログアウト機能が完成している

<br>
<hr>
<br>

# 準備

<br>

## GoogleAPI登録(Google認証に必要)

### 前提条件
Gmailを持っている

### [Google Cloud Platform](https://console.developers.google.com/project)にログイン

### [Google Cloud Platform](https://console.developers.google.com/project)で任意の名前でプロジェクト作成

### OAuth同意画面でプロジェクト詳細設定
> **公開ステータス**：テスト
> **ユーザーの種類**：外部
> **テストユーザー**：開発環境で動作確認する時に使用するメールアドレス(普段使ってるGmailでOK)

### 認証情報画面でOAuth2.0クライアントID,クライアントシークレット発行
> **名前**：任意の名前
> **承認済みのJavaScript生成元**：http://localhost, http://localhost:3000 (2つ記述する)
> **承認済みのリダイレクトURI**：ログイン後にリダイレクトさせたいURLを記述
> 上記を記述し終えるとクライアントID, クライアントシークレットが発行されるのでメモする(**漏洩注意**)

<br>
<hr>
<br>

## TwitterAPI登録(Twitter認証に必要)

### 前提条件
メールアドレス・電話番号登録済みのアカウント
鍵アカでないこと

### [TwitterDevelopePortal](https://developer.twitter.com/en/portal/) でログイン

### 「Sign up for Free Account」から新規登録
無料・有料アカウントあるが、今回は無料アカウントでOK

### 「Describe all of your use case of Twitter’s data and API」欄でAPI使用目的を英語で250文字以上記述
そこまで重要なものではないので結構適当で大丈夫

###   プロジェクト新規作成
デフォルトで存在するプロジェクトでも可

###  [ プロジェクト名 ] → [ User authentication settings ]
> **App permissions**：Request email from usersをON
> **Type of App**：Web App, Automated App or Bot
> **App info**：Callback URI / Redirect URL に 
> https://127.0.0.1:3000/users/auth/twitter/callback, http://127.0.0.1:3000/users/auth/twitter/callback を入力 
> 🚨Twitterはlocalhostを使用できないので「127.0.0.1:3000」を使用する
>**WebsiteURL, Terms of service, Privacy policy**：現在既に有効なWebsiteのURLを貼る

### [ プロジェクト名 ] → [ Keys and tokens ]
API Key と API secretをメモ(**漏洩注意**)
🚨ClientID, ClientSecretは使用しないので注意

<br>
<hr> 
<br>

## FacebookAPI登録(Facebook認証に必要)

### 前提条件
Facebookのアカウントを持っている

### [MetaforDeveloper](https://developers.facebook.com)アクセス→ [ スタート ]
Registar, Contact info, About youの3項目入力

### 新規アプリ作成
「ユーザーにFacebookアカウントでのログインを許可する」から作成

### プロジェクト詳細設定
詳細設定後に発行されるアプリIDとapp secretをメモ(**漏洩注意**)

<br>
<hr>
<br>

# 実装

## Gemインストール
```Ruby
gem "omniauth-google-oauth2"  ## Google認証
gem "omniauth-twitter"  ##Twitter認証
gem "omniauth-facebook" ## Facebook認証
gem "omniauth-rails_csrf_protection" ## どの認証でも必要
```

## Usersテーブルにカラム追加
 db/migrate/00000000000000_devise_create_users.rbに以下のカラムを追加
```Ruby
t.string :provider
t.string :uid
```

## SnsCredentialsテーブル作成
以下のように新しくSnsCredentialsテーブルを作成
```Ruby
class CreateSnsCredentials < ActiveRecord::Migration[7.0]
  def change
    create_table :sns_credentials do |t|
      t.string :provider
      t.string :uid
      t.references :user, type: :string, foreign_key: true
      t.timestamps
    end
  end
end
```

## Userモデル, SnsCredentialモデルにリレーション関係記述
**Userモデル**
```Ruby
has_many :sns_credential, dependent: :destroy
```
**SnsCredentialモデル**
```Ruby
belongs_to :user
```

## Userモデルのdeviseにomniauth系の記述追記
deviseでdeviseのヘルパーメソッドが使えるようになる
```Ruby
  devise :database_authenticatable, :registerable,
  :recoverable, :rememberable,
  :omniauthable, omniauth_providers: %i[google_oauth2 twitter facebook]
```

## credentialsにIDとシークレット系を保存
```terminal
EDITOR=vi rails credentials:edit          
```
ターミナル上で以下のように記述(SNS名, 変数名は適宜変更)
```terminal
google:
  client_id: ID
  client_secret: シークレット
```

## 環境変数設定
config/initializer/devise.rb
```Ruby
config.omniauth :google_oauth2, Rails.application.credentials.google[:client_id], Rails.application.credentials.google[:client_secret], skip_jwt: true
config.omniauth :twitter, Rails.application.credentials.twitter[:client_id], Rails.application.credentials.twitter[:client_secret], skip_jwt: true
config.omniauth :facebook, Rails.application.credentials.facebook[:client_id], Rails.application.credentials.facebook[:client_secret], skip_jwt: true
```

## Model編集
🚨Devise.friendly_token[12]の12の部分は自分のプロジェクトのパスワードの桁数のバリデーション等に応じて適宜変更
(ユーザーはSNS認証でパスワードを要求されないが、deviseの仕様としてパスワードの保存が必須なので代わりに裏側で自動的にパスワードを入れている)
user.rb
```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[12]
    end

  end

```

## Controller作成
omniauth_callbacks_controller.rb作成
```Ruby
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def google_oauth2    
    callback_for(:google)    
  end

  def twitter
    callback_for(:twitter)
  end

  def facebook
    callback_for(:facebook)
  end

  def callback_for(provider)

    @user = User.from_omniauth(request.env['omniauth.auth'])
    @user.save!
          
    if @user.persisted?
      sign_in @user 
      redirect_to platforms_path 
    else
      redirect_to new_user_registration_path
    end

  end

  def failure
    redirect_to root_path and return
  end

end

```

## Routing編集
```Ruby
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions',
    passwords: 'users/passwords',
    omniauth_callbacks: "users/omniauth_callbacks"
  }
```

## Viewにボタン設置
```Ruby
## Google
<%= link_to user_google_oauth2_omniauth_authorize_path, method: :post do %>
  Googleアカウントでサインインする
<% end %>

## Twitter
<%= link_to user_twitter_omniauth_authorize_path, method: :post do %>
  Twitterアカウントでサインインする
<% end %>

## Facebook
<%= link_to user_facebook_omniauth_authorize_path, method: :post do %>
  Facebookアカウントでサインインする
<% end %>
```

## 補足
### パスワードについて
SNS認証はパスワードはユーザーに要求されないので、ユーザー全員にt対してパスワードのバリデーションをかけている場合は、SNS認証で登録・ログインする人には要求しないようにする
(以下はproviderが存在するかどうかで判別している)
```Ruby
class PasswordFormatValidator < ActiveModel::EachValidator
  VALID_PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[A-Za-z\d]{8,12}\z/
  def validate_each(record, attribute, value)
    unless record.provider.present?      
      unless value =~ VALID_PASSWORD_REGEX              
        record.errors.add(attribute, (options[:message] || 'が正しい形式ではありませんんん'))
      end
    end
  end
end
```
### authの中身
下記のようにさまざまな情報を取得することができる。
下記はauthで取得できる情報の一部なのでデバッグで確認してみてほしい。
User.rbのself.from_omniauth(auth)のメソッド内で確認可能
```terminal
 {"provider"=>"twitter",
 "uid"=>"0000000000000000000",
 "info"=>
  {"nickname"=>"000000",
   "name"=>"田嶋太一",
   "email"=>"00000000@gmail.com",
   "location"=>"東京 世田谷",
   "image"=>"画像のURL",
   "description"=>"",
   "urls"=>{"Website"=>0000000, "Twitter"=>"0000000"}},
 "credentials"=>{"token"=>"0000000000000000000000", "secret"=>"Qx42N9jWKc5zjMnl8xDU3b1VJplRJEi4hqBei97vAoUzJ"},
```

## 確認

### 新規登録・ログインの確認
MySQLのテーブル内に登録したメールアドレス, uid, providerが保存されていて2回目以降もログイン可能で成功
🚨複数SNSで同一メアドが登録されないようにするためにメールアドレスにuniqueを適用した方が良い

## まとめ

各SNSのID,シークレット取得
必要なGemインストール
Usersテーブルにカラム追加
SnsCredentialテーブル作成
User, SnsCredentialリレーション関連付け
deviseにomniauth追記
credentialsでID,シークレット管理
環境変数設定
Modelでauthのデータ取得
Controllerで各SNSの処理振り分け
Routingでomniauthのページを返せるようにする
Viewにボタン設置
(パスワードバリデーション関連)
(authデバッグ)
データベース保存確認,動作確認

🚨各バージョン、開発者ツールの画面仕様の変更等で違う場合あり

## 参考記事
- [devise](https://github.com/heartcombo/devise)
- [FacebookAPI取得方法](https://di-acc2.com/marketing/sns-tech/18499/)
- [twitterAPI取得方法](https://blog.nyanco.me/howto-get-twitter-api-202306/)
- [omniauth](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview)
- [API登録参考記事](https://qiita.com/manbolila/items/8caa1f5d2b1fb96d2646#twitter-api-%E3%81%AE%E7%99%BB%E9%8C%B2)
- [railsSNS認証やり方 ](https://qiita.com/manbolila/items/48d361fd5968179918c3)
- [devise + omniauthでGoogle認証](https://qiita.com/iloveomelette/items/b6e65caea88b6e86cfdd#%E5%90%84%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E7%B7%A8%E9%9B%86)
- [rails7 + omniauth + twitter](https://qiita.com/kaorumori/items/449c0eef54d46bafeb74)
- [rails omniauth-twitterでログイン機能](https://blog.tatehitolog.com/entry/2020/02/09/215835)
- [pomniauth実装まとめ](https://qiita.com/Hassan/items/176bc2c6fd75a3e00111)