【Rails7】WebアプリにGoogleアカウントでログイン

# はじめに
Railsで開発中のWebアプリにGoogleアカウントでログイン出来るようにしたので、
記憶の整理を含めて記事に記述していこうと思います。

# 1.開発環境
MacOS
Ruby:3.2.3
Rails:7.0.8
Postgresql:16

# 2.事前準備
https://zenn.dev/yoiyoicho/articles/c44a80e4bb4515
上記の記事を参考にGoogle Cloud Platformに登録してAPIの設定を行い、クライアントIDとクライアントシークレットの準備をします。


# 3.事前知識(知っている方はスルーで大丈夫)
事前知識としてChatGPTから引用しながら以下にまとめます。
仕組みを学び、処理の流れを理解するとエラー発生時に何が原因か判断出来るようになり、沼りにくくなるので事前知識はとても大事だと個人的には思っています。下記を見て分からない箇所があればChatGPT等のAIと認識の摺り合わせをするのもおすすめです。


### OAuth
OAuthは、アクセス権限の認可を行なうためのプロトコルで、アプリがユーザーのパスワードを知らずに他のサービスの情報にアクセスするための安全な方法です。ユーザーの承認を得てアクセストークンを使って情報を共有します。

OAuthの仕組み
1. **リクエストの送信**：
あなたのアプリは、ユーザーにGoogle（などのサービス）のログイン画面にリダイレクトします。
2. **ユーザーの承認**：
ユーザーはGoogleのログイン画面でログインし、あなたのアプリがその情報にアクセスすることを承認します。
3. **承認コードの取得**：
承認が成功すると、Googleはあなたのアプリに承認コードを送ります。このコードは一時的なもので、非常に短い時間だけ有効です。
4. **トークンの取得**：
あなたのアプリはこの承認コードを使って、Googleからアクセストークンというものをもらいます。このトークンを使うと、あなたのアプリはユーザーのGoogleアカウント情報にアクセスできます。
5. **APIリクエスト**：
あなたのアプリは、このアクセストークンを使ってGoogleのAPIにリクエストを送り、必要な情報を取得します。

### トークンとは
トークンは、一時的なデジタルキーのようなもので、このキーを持っていると、特定のサービスにアクセスすることができます。たとえば、あなたのアプリがGoogleのアカウント情報にアクセスしたいとき、このトークンを使ってアクセスします。
### **アクセストークンを使ってGoogleアカウント情報にアクセスする流れ**

1. **ユーザーの認証**：
    - アプリがユーザーに対してGoogleアカウントにログインするよう求めます。
2. **認可コードの取得**：
    - ユーザーがログインすると、Googleから認可コードがアプリに送られます。
3. **アクセストークンの取得**：
    - アプリはこの認可コードを使ってGoogleからアクセストークンとリフレッシュトークンを取得します。
4. **Googleアカウント情報へのアクセス**：
    - 取得したアクセストークンを使って、アプリはGoogleのAPIにリクエストを送り、ユーザーのGoogleアカウント情報（例：メールアドレス、名前、プロフィール画像など）にアクセスします。
### **なぜトークンを使うのか？**


1. **セキュリティ**：ユーザーのパスワードを直接扱わずに済むため、セキュリティが向上します。
2. **限定されたアクセス**：トークンには有効期限があり、特定の情報にだけアクセスできるように制限できます。
3. **簡単な取り消し**：トークンが漏洩した場合でも、簡単に無効にできるため、安全性が保たれます。

### **承認コードとアクセストークンの違い**

 **承認コード**はユーザーがログインしてアプリにアクセスを許可したことを証明する一時的なコードです。これは"**短期間**"だけ有効です。承認コードが短期間しか使えなくするのは、悪意のある第三者に承認コードを盗まれても有効期限が切れていれば何の役にも立たないので、承認コードを使いまわして不正利用されるのを防ぐためです。


# 4.実装手順
**OAuthライブラリの設定**

まず、**`omniauth-google-oauth2`**などのOAuthライブラリをインストールして設定を行います。これにより、GoogleへのリダイレクトURLを生成する機能が使えます。また、今回は環境変数を設定するのに.envファイルを使うのでdotenv-railsもインストールします。
以下のgemを追加し**`bundle install`** を実行します

gemfile
```
gem 'omniauth'
gem 'omniauth-google-oauth2'          #GoogleのOAuth
gem 'omniauth-rails_csrf_protection'  #OAuth使う時のセキュリティ用
gem 'dotenv-rails'                    #環境変数設定時に使用
```
**`gem 'omniauth'`**

**OmniAuth**は、さまざまな認証プロバイダー（例：Google、Facebook、Twitterなど）を統一的に扱うための認証フレームワークです。これにより、異なるプロバイダーを使った認証を簡単に実装できます。
**`gem 'omniauth-google-oauth2'`**

**OmniAuth Google OAuth2**は、OmniAuthを使ってGoogleのOAuth2認証を行うためのプラグイン(アプリケーションの機能を拡張するソフトウェア)です。Googleアカウントを使ってユーザーを認証するための機能を提供します。

**`gem 'omniauth-rails_csrf_protection'`**

**OmniAuth Rails CSRF Protection**は、CSRF（クロスサイトリクエストフォージェリ）攻撃からアプリケーションを保護するためのGemです。OmniAuthを使った認証フローでCSRF対策を強化します。インストールするだけで、CSRFトークンのチェックが有効になり自動で保護します。

**OmniAuthとomniauth-google-oauth2の関係**

- **OmniAuth**：認証フレームワークであり、認証の基本的な仕組みを提供します。具体的なプロバイダー(今回だとGoogle)の認証は行いません。
- **omniauth-google-oauth2**：Google OAuth2プロバイダーを利用して認証を行うためのOmniAuthプラグインです。OmniAuthの上に追加され、Googleアカウントを使った認証機能を提供します。
- **OmniAuthだけの場合**：認証フレームワークは存在するが、具体的なプロバイダーに対する認証はできない。
- **omniauth-google-oauth2だけの場合**：OmniAuthが必要な基盤を提供していないため、機能しない。

次にルートディレクトリに`.env`を作成しGoogle Cloud Platformから取得したクライアントIDとクライアントシークレットを環境変数に設定する
```
# .env
GOOGLE_CLIENT_ID=取得したクライアントID
GOOGLE_CLIENT_SECRET=取得したクライアントシークレット
```

次にconfig/initializers/omniauth.rbを作成して認証の設定を行います。
```
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], {
    scope: 'userinfo.email, userinfo.profile'
  }
end
```
この設定はRailsアプリケーションでOmniAuthというライブラリを使ってGoogle OAuth2認証を設定するためのもので、 この設定によりGoogleのOAuth2プロバイダーを使ってユーザーのメールアドレスとプロフィール情報にアクセスできます。
`scope: 'userinfo.email, userinfo.profile'`は、どの情報にアクセスするかを指定しています。今回はユーザーのメールアドレスとプロフィール情報にアクセスするように指定しています。

config/routes.rbに以下を追加します。
```
Rails.application.routes.draw do
  get 'auth/:provider/callback', to: 'sessions#create'
  get 'auth/failure', to: redirect('/')
  get 'signout', to: 'sessions#destroy', as: 'signout'
end
```
Googleログインのみを実装したい方は`get 'auth/:provider/callback'`を`get 'auth/google_oauth2/callback'`で大丈夫です。
後にLINEやxなどの他のプロバイダーを使ってログイン出来るようにしたい方は:providerにしましょう。

app/controllers/sessions_controller.rbを作成し、以下を追加します。
```
class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(request.env["omniauth.auth"])
    session[:user_id] = user.id
    flash[:success] = 'ログインしました'
    redirect_to root_path
  end

  def destroy
    reset_session
    flash[:info] = 'ログアウトしました'
    redirect_to root_path, status: :see_other
  end
```
from_omniauthメソッドは後でモデルファイルに記述します。

Userモデルの作成
ターミナルで`rails g model User provider:string uid:string name:string email:string avatar:string`を実行してマイグレーションファイルを編集します。
```
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :provider, null: false
      t.string :uid, null: false
      t.string :name, null: false
      t.string :email, null: false
      t.string :image

      t.timestamps
    end

    add_index :users, [:provider, :uid], unique: true
  end
end
```
`:provider` にはプロバイダーが入ります。(一つのプロバイダーしか使わない場合は不要)
- **Googleの場合**：**`google_oauth2`**
- **LINEの場合**：**`line`**

`:uid`はプロバイダーから送られてくるものでプロバイダー側がアカウントのidを確認するのに使われます。

### **なぜproviderカラムが必要なのか？**
providerカラムなしでもログイン認証は可能ですが、複数プロバイダーを使う場合にuidの衝突リスクがあります。
uidは"**プロバイダー側が**"ユーザーを一意に識別するためのIDなので、
例えばユーザーAがGoogleアカウントでログインを行ったとして、そのGoogleアカウントのuidが「20」だとします。
別のユーザーBがLINEアカウントでログインを行い、そのログインしたLINEアカウントのuidも20だとすると、同じuidを持つ異なるユーザーが存在することになり、アプリケーションがどのユーザーを認識しているのか分からなくなります。これによって、誤ったユーザー情報の表示やデータアクセスのリスクが発生します。
また、もしもuidにユニーク制約を付けてしまった場合、先程の例でいうと後からログインを試みたユーザーBは既にuidがユーザーAに使われているためログインが出来なくなってしまいます。

上記のリスクを踏まえ、今後ログインできるプロバイダーを増やす事も考慮すると、
`add_index :users, [:provider, :uid], unique: true`にすることでproviderとuidを合わせてユニークにすることが出来きます。

後は実装したいwebサービスに必要な情報があればカラムに追加します。
自分の場合は後々通知機能に挑戦したいのでemailも含めています。不要な方は無しで大丈夫です。

上記を踏まえた上で`rails db:migrate`を実行

app/models/user.rbにsessionコントローラーで記述したクラスメソッドfrom_omniauthを追加します。
```
class User < ApplicationRecord
  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.email = auth.info.email
      user.name = auth.info.name
      user.image = auth.info.image
    end
  end
end
```
引数authは、OmniAuthから提供される認証情報が格納されたオブジェクトです。
`where(provider: auth.provider, uid: auth.uid)`はproviderとuidの条件に基づいて、データベース内のユーザーを検索します。
`first_or_create`whereで見つかった最初のユーザーを返します。もし見つからなければ、新しいユーザーを作成します。


最後にビューファイルにログインリンクを設置します。

```
# link_toを使う場合
<%= link_to "Googleでログイン", "/auth/google_oauth2", data: { turbo_method: :post } %>

# button_toを使う場合
<%= button_to "Googleでログイン", "/auth/google_oauth2",method: :post" %>
```

以上がGoogleアカウントでのログイン機能追加になります。
開発環境でのログインにはGoogle Cloud Platformのテストユーザーに追加したメールアドレスでログイン出来れば成功です。


# 最後に
限られた時間の中で記事を書いたので誤字脱字や誤りがあれば教えてもらえると幸いです。
また、文字数が多くなったのでコード等の解説を省略しています。
chatgptに処理の流れやコードの解説を細かく聞くと、より理解が深まっておもしろいのでぜひやってみてください。