Google認証の導入について（devise+Omniauth）

# はじめに
さまざまな記事を拝見して、Google認証は設定できましたが、備忘録として今回記事を作成いたします。基本的には[公式](https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview)を参考に作成していますが、書かれていることをそのまま記述するのではなく、調べて理解したことを記述しております。間違い等ございましたら、教えていただきますようお願いいたします。少しでもお役に立てれば幸いです。

# 現状
###### 技術スタック
OS: macOS Sonoma 14.6.1
Docker: 24.0.2
Docker Compose: v2.18.1
Ruby: 3.1.4
Rails: 7.0.8.1
データベース: PostgreSQL 12.18
CSSフレームワーク: Bootstrap 5
CircleCI: 継続的インテグレーション（CI/CD）を導入
+ Gemのdeviseについては導入していましたが、既にUserモデルを作成してしまっていたので、[こちら](https://qiita.com/nakad119/items/967e53ace5788afdbcc1)を参考に設定しました。
>上記の記事で一点私と異なった点が、rails db:migrateでエラーが起こったとなっていたが、私の場合は以下のコマンドでエラーが出ず実行できた。
```php
 docker-compose exec web rails db:migrate
```

# 手順
#### １ Google認証情報の設定
これはGoogleの開発者用サイトで、Googleログインを使用するために設定するものです。そのために、私は以下のサイトを参考にしました。

https://zenn.dev/tteaoocl/articles/5601b28fa8045d

こちらの[サイト](https://code.google.com/apis/console/)から設定を行い、APIキーを取得してください。

#### ２ Gemのインストール及びカラムの追加
下記のGemを追加してください。
```php:Gemfile
gem 'devise'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection'
```
以下の２つのカラムを追加してください。
```php:ターミナル
rails g migration AddOmniauthToUsers provider:string uid:string
docker-compose exec コンテナ名 rails db:migrate
```
これらのカラムは`provider`はどの外部認証活用したかを保存し、`uid`はそれらのサービスでのIDを保存しておく。それらによって、すでにデータがあればログインでき、なければ新規ユーザーとしてデータを保存する。

#### ３ プロバイダーの設定
先ほど作成したGoogleログインを使用するために作成したAPIキーを活用して、紐づける役割だと思います、、、、、（自信がないので違っていれば教えてください）
```php:config/initializers/devise.rb
config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
```

#### ４ モデルに外部認証の設定を追加
```php:app/models/user.rb
devise :omniauthable, omniauth_providers: [:google_oauth2]
```
上記の設定は、deviseに`omniauthable`を追加して、外部認証を有効にします。`omniauth_providers: [:google_oauth2]`はどの外部認証サービスを活用するかを明確にします。今回はGoogleログインを使用する設定です。

:::note info
これにより、config/routes.rbにてevise_for :usersを記述することで、google関連の以下のルーティングも自動作成される。
:::
```php:ターミナル
user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:format) devise/omniauth_callbacks#passthru
user_google_oauth2_omniauth_callback  GET|POST /users/auth/google_oauth2/callback(.:format) devise/omniauth_callbacks#google_oauth2
```

#### ５ ルーティングの定義
ログアウトのルーティングには名前付きルートヘルパーを活用して、`link_to`にて`method: :delete`を省略することもできますが、今回はdevise_scopeにてそれらの設定は行いません。
```php:config/routes.rb
devise_for :users, controllers: {
    omniauth_callbacks: 'users/omniauth_callbacks'
}
```
`devise_for :users`の部分では、deviseが提供する認証機能のルートを自動生成してくれます。ですので、先ほどのモデルの設定があることで必要なルートが作成されます。
Googleからの認証結果が、`/users/auth/google_oauth2/callback`に送られるタイミングでdeviseがそのリクエストを検知するらしい。もう少し簡単に言うと、ルートにリクエストが来たタイミングで、`controllers:`が参照されるらしいです。
> ここで疑問に思ったことは、ルートにリクエストが来たタイミングならログインボタンを押したタイミングでも参照されるのでは？と疑問に思いました。結論、そのタイミングは参照されない様子。ログインボタンを押したタイミングはユーザーをリダイレクトするので、リクエストされるのはユーザーが認証を完了してGoogleが認証結果をアプリのURLに送られた時のみ。つまり、アプリからGoogleにリクエストする時に'controllers'は参照されずに、Googleからアプリにリクエストする時に参照されるらしいです。難しいですが、少し理解ができました。

#### ６ コントローラーの設定
まず`app/controllers/users/omniauth_callbacks_controller.rb`を作成します。
```php:ターミナル
bin/rails g controller users::omniauth_callbacks 
```
作成できたら、`omniauth_callbacks_controller.rb`を編集していきます。
```php:users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_action :verify_authenticity_token, only: :google_oauth2

  def google_oauth2
    @user = User.from_omniauth(request.env['omniauth.auth'])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      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
```
```php
  skip_before_action :verify_authenticity_token, only: :google_oauth2
```
上記の記述は、railsはCSRFトークンを使用して、フォーム送信やログインリクエストが正しいかチェックしているらしい。ただし、外部認証だとCSRFトークンが含まれていないため、`google_oauth2`のCSRFトークンのチェックを無効にしているらしいです。
```php
  def google_oauth2
    @user = User.from_omniauth(request.env['omniauth.auth'])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      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
```
上記の記述は認証結果を処理するための記述です。リクエストが送られたら上記のメソッドが呼び出されます。`User.from_omniauth`メソッドは認証情報を検索し、見つからなければ作成します。なので、基本的には`if @user.persisted?`では`true`になるはずですが、Googleからの情報にメールアドレスがないなどの場合には、バリデーションエラーとなって、`else`の処理になります。

#### ７ モデルの追記
先ほどの`User.from_omniauth`メソッドを追記して、ユーザーの情報を取得・保存する処理を記述する。
```php:app/models/user.rb
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
  end
end
```

#### ８ ログイン・ログアウトボタンの設置
```php
<%= button_to "グーグルでのログイン", user_google_oauth2_omniauth_authorize_path, method: :post, data: { turbo: false } %>
```
`link_to`で実装した場合、エラーが出てしまったので、一旦はこちらの記述で実装しております。

# おわりに
今回のGoogle認証は初学者の私にとってかなり難しかったですが、色々なコードを見て学ぶ勉強になりました。今はGoogle認証だけですが、時間があれば他の認証も追加していきたいと思います。
以下、参考にさせていただいた記事等を載せておきます。最後まで閲覧いただき誠にありがとうございました。

https://qiita.com/nakad119/items/9caa1ce803185b93fe85

↑公式とこちらのサイトを見ながら実装いたしました。大変お世話になりました。

https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview

https://zenn.dev/tteaoocl/articles/0f0e6443cd318f

https://github.com/zquestz/omniauth-google-oauth2?tab=readme-ov-file