【Rails】2022年度版 Devise + Omniauth でGoogle認証(SNS認証)を丁寧に解説

## 概要

今回は標題の通り、Devise + Omniauth でGoogle認証の実装手順を記していきます。

**なぜ今さら記すことに至ったかですが、参照させていただいた記事を軸に進めていたのですが、所々エラーになってしまったり、プラスで追加しなければいけないものもあるので、今後導入する方の参考になればと思ったからです。**

他のSNS認証でも大枠は同じだと思うので、Twitterなど導入したいものがあれば置き換えて参考にしてくだされば幸いです。

尚、ご指摘箇所がございましたら
ご教授いただけますと幸いです。

## はじめに

では、早速といきたいところですが先に私の環境と前提をお話ししていきます。

- macOS Monterey 12.3.1 (M1)
- Rails 6.1.5
- MariaDB 10.6.7

そして、今回私は`Devise`を導入しており、後ほど`uid`カラムと`provider`カラムを追加するのですが`User`モデルではなく別の`Sns_Credential`モデルに追加する手順で進めていきます。

**また、`Devise`の各種実装は既に完了していることを前提としております。**
それでは、お待たせしました。手順を記していきます。

## IDとシークレットを取得

先にGoogle Cloud Platformにて[こちらの記事](https://zenn.dev/batacon/articles/e9b4a88ede2889#3.-google-api%E3%81%AE%E8%A8%AD%E5%AE%9A)を参考にしながら、`クライアントID`と`クライアントシークレット`を取得しておきましょう。
5分程度で終了するので、先に済ませてしまいます。

**IDとシークレットは後ほどファイルに記述していくため、必ずどこかしらにメモしておいてください！**
<font color="red">**そして絶対に他人に教えたりしないでください！**</font>

## Gemのインストール

IDとシークレットを取得できたら、次はomniauth系のgemなどをインストールしていきます。
今回、私はIDとシークレットを`crendentials.yml.enc`に記載していきます。
もし、`.env`に記述する方は記述方法や手順が若干異なりますので注意してください。

```ruby:Gemfile
gem "omniauth-google-oauth2"
gem "omniauth-rails_csrf_protection"
# '.env'を使う場合は以下も追加
gem "dotenv-rails"
```

`omniauth-rails_csrf_protection`をなぜ追加したかというと、OmniAuthの許可されたリクエストメソッドをGETで使用するとCSRF攻撃を受けてしまう可能性があるため、GETを無効にしてくれるGemです。[DeviseのGitHub](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview#facebook-example)でも追加してねと言われています。

:::note info
CSRF攻撃とは...
クロスサイトリクエストフォージェリのアルファベット頭文字を取った略称で、Webアプリケーションの脆弱性を利用した攻撃手法のこと。
攻撃用Webページにユーザを誘導し、不正なリクエストを攻撃対象のWebアプリケーションに送信します。攻撃対象のWebアプリケーションはその不正なリクエスト処理してユーザが意図していない処理が行われてしまいます。
ユーザに直接的な被害はないものの、攻撃対象のWebアプリケーションからは攻撃者と見なされてしまいます。
> 参照： [「クロスサイトリクエストフォージェリ（CSRF）」とは？](https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html#:~:text=%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA%EF%BC%88CSRF%EF%BC%89%E3%81%A8%E3%81%AF%E3%80%81Web,%E5%87%A6%E7%90%86%E3%81%97%E3%81%A6%E3%81%97%E3%81%BE%E3%81%84%E3%81%BE%E3%81%99%E3%80%82)
:::

他のSNS認証に関しても[DeviseのGitHub](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview#facebook-example)によると以下のように言っています。詳しいリストも公式にはあるので、ぜひ参考にしてください。

>Generally, the gem name is `omniauth-#{provider}` where provider can be "facebook" or "twitter", for example.
>基本的にジェムの名前は「`omniauth-#{provider}`」で、例えば"provider"のところに"facebook"や"twitter"が入るよ。

`Gemfile`に追加ができたら`bundle install`を行いましょう。

```zsh:ターミナル
$ bundle install

# '.env'を使う方は以下も実施(アプリのディレクトリで)
$ touch .env
```

`.env`の方は、<font color="red">**必ず`.gitignore`に追加をしてください！**</font>
ここに入れないとGitHubから`.env`は丸見えとなり悪意あるユーザが悪用してしまう可能性があります。
`crendentials.yml.enc`の場合は以下のコマンドをターミナルで実行してVimを開きます。

```zsh:ターミナル
$ EDITOR=vi rails credentials:edit
```
Vimが開けたら以下を追加します。
＊Vimの使い方は他の記事を参照してください。
**＊yaml形式なのでインデント幅には注意してください。半角スペース1つ間違えるだけでエラーとなります。**

```yml:config/crendentials.yml.enc
google:
  client_id: アプリID
  client_secret: アプリシークレット
```

`.env`の場合は以下の通りです。

```ruby:.env
GOOGLE_CLIENT_ID='******'
GOOGLE_CLIENT_SECRET='******'
```

## アプリIDとアプリシークレットの読み込み

アプリIDとアプリシークレットを入力できたら、次はこれらを読み込んでいきます。
`config/initializers/devise.rb`に以下を追加していきます。

```ruby:config/initializers/devise.rb
Devise.setup do |config|
  # 省略

  # ===== credentials.yml.encに記載した場合 =====
  config.omniauth :google_oauth2, Rails.application.credentials.google[:client_id], Rails.application.credentials.google[:client_secret], skip_jwt: true
  
  # ===== .envに記載した場合 =====
  config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], skip_jwt: true
  OmniAuth.config.logger = Rails.logger if Rails.env.development?  # debug用

  # 省略
end
```

Google認証する場合`skip_jwt: true`をつけないと`JWT::InvalidIatError`が発生するみたいなので、忘れずに記入します。`credentials.yml.enc`と`.env`で書き方が異なるので注意してください。

では、追加ができたらターミナルでちゃんと設定されているか確認を行います。

```zsh:ターミナル
$ rails c

# 起動できたら以下を実行（'credentials.yml.enc'の場合）
> Rails.application.credentials.google[:client_id]

# '.env'の場合
> ENV['GOOGLE_CLIENT_ID']
```

## Sns_Crendentialモデルの作成

では、Sns_Crendentialモデルを作成します。

```zsh:ターミナル
$ rails g model sns_credential
```

```ruby:マイグレーションファイル
class CreateSnsCredentials < ActiveRecord::Migration[5.2]
  def change
    create_table :sns_credentials do |t|
      t.string :provider
      t.string :uid
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end
```

```zsh:ターミナル
$ rails db:migrate
```

## 各モデルの編集

では、次にアソシエーションやメソッドなど必要なものを設定していきます。
コメントで解説を行っていきます。記述が長い場合は後に記述します。

```ruby:app/models/sns_credential.rb
class SnsCredential < ApplicationRecord
  # ===== 以下を追加 =====
  belongs_to :user
end
```

```ruby:app/models/user.rb
class User < ApplicationRecord
  # ===== 以下を追加 =====
  has_many :sns_credential, dependent: :destroy

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         # ===== Omniauthを使用するためのオプションを追加 =====
         :omniauthable, omniauth_providers: %i[google_oauth2]

  class << self   # ここからクラスメソッドで、メソッドの最初につける'self.'を省略できる
    # SnsCredentialsテーブルにデータがないときの処理
    def without_sns_data(auth)
      user = User.where(email: auth.info.email).first

      if user.present?
        sns = SnsCredential.create(
          uid: auth.uid,
          provider: auth.provider,
          user_id: user.id
        )
      else   # User.newの記事があるが、newは保存までは行わないのでcreateで保存をかける
        user = User.create(
          name: auth.info.name,    # デフォルトから追加したカラムがあれば記入
          email: auth.info.email,
          profile_image: auth.info.image,    # デフォルトから追加したカラム
          password: Devise.friendly_token(10)   # 10文字の予測不能な文字列を生成する
        )
        sns = SnsCredential.create(
          user_id: user.id,
          uid: auth.uid,
          provider: auth.provider
        )
      end
      { user:, sns: }   # ハッシュ形式で呼び出し元に返す
    end

    # SnsCredentialsテーブルにデータがあるときの処理
    def with_sns_data(auth, snscredential)
      user = User.where(id: snscredential.user_id).first
      # 変数userの中身が空文字, 空白文字, false, nilの時の処理
      if user.blank?
        user = User.create(
          name: auth.info.name,
          email: auth.info.email,
          profile_image: auth.info.image,
          password: Devise.friendly_token(10)
        )
      end
      { user: }
    end

    # Googleアカウントの情報をそれぞれの変数に格納して上記のメソッドに振り分ける処理
    def find_oauth(auth)
      uid = auth.uid
      provider = auth.provider
      snscredential = SnsCredential.where(uid:, provider:).first
      if snscredential.present?
        user = with_sns_data(auth, snscredential)[:user]
        sns = snscredential
      else
        user = without_sns_data(auth)[:user]
        sns = without_sns_data(auth)[:sns]
      end
      { user:, sns: }
    end
  end
end
```

`User.create`の引数はUserモデルで`NOTNULL`制約しているカラムは入れるようにしないとバリデーションエラーが発生します。

`auth`は後に記述する`@omniauth`すなわち`request.env['omniauth.auth']`です。
`request.env['omniauth.auth']`にはたくさんの情報が詰め込まれています。
一部を抜粋すると以下のようなものです。

```ruby:request.env['omniauth.auth']の中身（一部）
=> {"provider"=>"google_oauth2",
 "uid"=>"****",
 "info"=>
  {"name"=>"****",
   "email"=>"****",
   "unverified_email"=>"****",
   "email_verified"=>true,
   "first_name"=>"****",
   "last_name"=>"****",
   "image"=>"****"},
...
```

このようにハッシュ形式で渡ってきます。ぜひ`binding.pry`などで確認してみてください。
このパラメータを上記の変数などに格納しています。

## OmniauthCallbacksControllerの作成と編集

では、コントローラを作成するためにターミナルにて以下を実行しましょう。

```zsh:ターミナル
# 以下のフォルダがあれば実行しない
$ mkdir app/controllers/users/
# 以下は実行する
$ touch app/controllers/users/omniauth_callbacks_controller.rb
```
作成できたらコントローラ内を記述していきます。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def google_oauth2
    callback_for(:google)
  end

  def callback_for(provider)
    @omniauth = request.env["omniauth.auth"]
    info = User.find_oauth(@omniauth)
    @user = info[:user]
    if @user.persisted?    # persisted?は保存が完了しているかを評価するメソッド
      sign_in_and_redirect @user, event: :authentication
      # is_navigational_formatはフラッシュメッセージを発行する必要があるかどうかを確認する
      # capitalizeは文字列の先頭を大文字に、それ以外は小文字に変更して返すメソッド
      set_flash_message(:notice, :success, kind: provider.to_s.capitalize) if is_navigational_format?
    else
      @sns = info[:sns]
      render template: "devise/registrations/new"
    end
  end

  def failure
    redirect_to root_path and return
  end
end
```

## ルーティングの設定

続いて、ルーティングを以下のように設定します。
「ここを追加」以外は私の場合ですので、違っていても構いません。

```ruby:config/routes.rb
Rails.application.routes.draw do
  root "homes#index"
  devise_for :users, controllers: {
    registrations: "users/registrations",
    passwords: "users/passwords",
    # =====　以下を追加　=====
    omniauth_callbacks: "users/omniauth_callbacks"
  }
  
  # 省略
end
```

追加すると新しいルーティングが設定されます。具体的には以下の通りです。
見にくいですが、ご容赦ください。

```zsh:ターミナル
$ rails routes | grep user
user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:format) users/omniauth_callbacks#passthru
user_google_oauth2_omniauth_callback GET|POST /users/auth/google_oauth2/callback(.:format)  users/omniauth_callbacks#google_oauth2
```

## Viewの編集

それでは、ビュー側にリンクを追加していきましょう。
私の場合はFont AwesomeでGoogleのアイコンを追加しています。
`link_to`のブロック内は好みでカスタマイズしてください。

```erb:app/views/devise/registrations/new.html.erb
<div>
  <%= link_to user_google_oauth2_omniauth_authorize_path, method: :post do %>
    <i class="fa-brands fa-google"></i>
    Googleアカウントでサインインする
  <% end %>
</div>
```

`link_to`には`method: :post`をつけてあげないとCSRF攻撃関連で上手く動作しない可能性があるので、しっかりつけてあげてください。
[こちらの記事](https://zenn.dev/batacon/articles/e9b4a88ede2889#7%2F7.-view%E3%82%92%E4%BD%9C%E6%88%90%E3%80%81%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E8%A9%A6%E3%81%99)を引用すると

>link_toを使うとデフォルトでGETメソッドになるため、もしログインのリンクにmethod: :postを入れていなければ、omniauth-rails_csrf_protectionのgemがGETを無効にしているのでMissing templateのエラーになってしまいます。

尚、ログインページにも`app/views/devise/shared/_links.html.erb`を参照して表示されているので、そこも併せて編集するのも良いでしょう。

本番環境の設定は[こちらの記事](https://qiita.com/nakanishi03/items/2dfe8b8431a044a01bc6#%E6%9C%AC%E7%95%AA%E7%92%B0%E5%A2%83%E3%81%A7%E3%81%AF)を参照してください。

お疲れ様でした。
最後までお読みいただきありがとうございます。


## 参考

- [devise - GitHub](https://github.com/heartcombo/devise)
- [OmniAuth: Overview](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview)
- [RailsでFacebookとGoogleのOAuth連携。SNS認証の方法](https://qiita.com/nakanishi03/items/2dfe8b8431a044a01bc6#google)
- [【Rails】DeviseとOmniauthを使ってGoogle、Twitter、Facebook認証](https://qiita.com/d0ne1s/items/e1ee23ea23343d6aaa34)
- [Rails+omniauth-google-oauth2でGoogleログイン(devise無し)](https://zenn.dev/batacon/articles/e9b4a88ede2889)
- [【Rails】deviseを用いたSNS認証(Facebook・Google)の導入方法を簡単に解説](https://techtechmedia.com/sns-authentication-rails/)
- [[Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する](https://qiita.com/kazuooooo/items/47e7d426cbb33355590e)
- [RailsにGoogleのSNS認証を分かりやすく導入する方法~omniauth-google-oauth2~](https://hirocorpblog.com/post-236/)