1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails + Devise + OmniAuth で Googleログインを実装する

1
Posted at

はじめに

Railsアプリで「Googleでログイン」を実装する手順を、実際に動いているコードベースをもとに解説します。
Devise と OmniAuth の責務分担を理解しながら進められる構成にしているので、「とりあえず動いたけど何をやってるのか分からない」を卒業したい方向けです。

この記事のゴール:

  • Googleアカウントで新規登録・ログインができる
  • 2回目以降は同じユーザーとして自動ログインされる
  • 既存のメール/パスワード登録ユーザーと衝突した場合は安全に拒否する

環境

  • Ruby 3.3.6
  • Rails 7.2.3
  • PostgreSQL 16
  • Devise 4.x
  • omniauth-google-oauth2

Devise と OmniAuth の役割分担

実装に入る前に、この2つの責務を整理しておきます。ここを押さえると後の設定の意味がスッと入ってきます。

ライブラリ 担当
OmniAuth GoogleとのOAuth 2.0のやり取りそのもの(リダイレクト→認可→トークン交換→ユーザー情報取得)。結果を
request.env["omniauth.auth"] に詰める
Devise アプリ側のセッション管理。:omniauthable モジュールがOmniAuthのコールバックをDeviseのルーティングに繋ぐ
自前コード
「OmniAuthがくれたGoogleユーザー情報を使ってUserをどう作る/探す/ログインさせるか」というアプリ固有のロジック

一言:OmniAuthは「この人はこのGoogleアカウントの人だ」と教えるだけ。アプリのユーザーとして扱うのはDeviseと自前コード。

Step 1. Gemをインストール

Gemfile
gem "devise"
gem "omniauth-google-oauth2"
gem "omniauth-rails_csrf_protection" # OmniAuth 2.0でrequest phaseのCSRF対策に必須
bundle install

omniauth-rails_csrf_protection を入れ忘れると、ログインボタンをGETで踏んだときに OmniAuth::AuthenticityError
が出ます。後述しますがログインリンクは POST で叩く必要があります。

Step 2. Google Cloud Console でOAuthクライアントを作成

クライアントの作成は以下の記事を参考に作成しました。
(https://qiita.com/Tohru-f/items/da29aafed88b85ff939e)

Step 3. 環境変数を設定

.env
GOOGLE_CLIENT_ID=your_client_id_here
GOOGLE_CLIENT_SECRET=your_client_secret_here

.env は 必ず .gitignore に追加してください。
your_client_id_hereとyour_client_secret_hereには、クライアント作成の際に発行された、クライアントIDとクライアントシークレットを入れてください。

Step 4. usersテーブルに provider / uid カラムを追加

OAuth経由のユーザーを識別するためのカラムを足します。

bin/rails g migration AddOmniauthToUsers provider:string uid:string
db/migrate/xxxx_add_omniauth_to_users.rb
class AddOmniauthToUsers < ActiveRecord::Migration[7.2]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_index  :users, [:provider, :uid], unique: true
  end
end
bin/rails db:migrate

なぜ (provider, uid) に複合ユニークインデックス?

  • uid はGoogle側でユーザーを一意に識別する不変ID
  • provider は、どのサービスのuidかを区別するためのラベルです。 Googleのuid=12345とLINEのuid=12345は別人なので、providerとセットで保存することができ、初めて『この人』を正しく特定できます
  • 将来GitHub等を追加しても衝突しない設計

Step 5. Userモデルを更新

app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :omniauthable, :trackable,
         omniauth_providers: [:google_oauth2]

  validates :name, presence: true

  def self.from_omniauth(auth)
    # ① Google側でメール認証が済んでいないアカウントは拒否(なりすまし対策)
    return nil if auth.provider == "google_oauth2" && !auth.info.email_verified
    # ② このアプリはemail必須なので、空なら拒否
    return nil if auth.info.email.blank?

    # ③ (provider, uid) で既存ユーザーを検索 → 2回目以降のログインはここで終了
    user = find_by(provider: auth.provider, uid: auth.uid)
    return user if user

    # ④ 同じメアドの別ユーザーがいたら拒否(アカウント乗っ取り防止)
    return nil if User.exists?(email: auth.info.email)

    # ⑤ 初回ログイン → 新規作成
    create do |u|
      u.provider = auth.provider
      u.uid      = auth.uid
      u.email    = auth.info.email
      u.name     = auth.info.name
      u.password = SecureRandom.hex(16)
    end
  end

  # OAuthユーザーにはパスワード入力を要求しない
  def password_required?
    super && provider.blank?
  end

  def password_changeable?
    provider.blank?
  end
end

ポイント解説

行 意図
:omniauthable DeviseでOAuthログインを有効化。これで user_google_oauth2_omniauth_authorize_path 等のヘルパが生える

|
| omniauth_providers: [:google_oauth2] | 使うプロバイダを明示。複数追加する場合はここに足す |
| email_verified チェック | Gmail側で本人確認されていないアカウントを排除。なりすまし対策の要 |
| (provider, uid) で検索 |
2回目以降のログインはここでヒット。emailではなくuidで引くことで、ユーザーがGmailを変更しても追従できる |
| 同一email衝突で nil | パスワード登録済みユーザーに勝手に紐づけない。乗っ取り防止 |
| SecureRandom.hex(16) | OAuthユーザーはパスワードを使わないが、validatable が要求するので埋めておく |
| password_required? の上書き | OAuthユーザーはパスワード入力なしで登録できるようにする |

Step 6. devise.rb にプロバイダを登録

config/initializers/devise.rb
Devise.setup do |config|
  # ... 既存の設定 ...

  config.omniauth :google_oauth2,
                  ENV["GOOGLE_CLIENT_ID"],
                  ENV["GOOGLE_CLIENT_SECRET"],
                  scope: "email,profile",
                  prompt: "select_account"
end

オプションの意図

  • scope: "email,profile" — 必要最小限の情報だけを要求(住所や電話番号は取らない)
  • prompt: "select_account" — ログインのたびにアカウント選択画面を出す。複数Googleアカウント持ちのUX対策

本番環境の注意点

リバースプロキシ配下(Render, Heroku等)では full_host が正しく取得できず、コールバックURLが http://
になってリダイレクトが失敗することがあります。その場合は callback_url を明示的に指定してください。

config.omniauth :google_oauth2,
                ENV["GOOGLE_CLIENT_ID"],
                ENV["GOOGLE_CLIENT_SECRET"],
                scope: "email,profile",
                prompt: "select_account",
                callback_url: ENV["GOOGLE_OAUTH_CALLBACK_URL"] # 本番のみ環境変数で固定

Step 7. ルーティングを設定

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    omniauth_callbacks: "users/omniauth_callbacks"
  }
  # ... その他のルート ...
end

なぜ controllers: で上書きするのか?

デフォルトの Devise::OmniauthCallbacksController には google_oauth2 アクションが存在しません。「コールバック後に何
をするか」はアプリ固有のロジックなので、自分でコントローラを作って指定する必要があります。

Step 8. OmniauthCallbacksController を作成

app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  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,
                  alert:
"Googleログインに失敗しました。メールアドレスが既に登録済みか、Google側でメール認証が完了していない可能性があります。"
    end
  end

  def failure
    redirect_to new_user_session_path, alert: "認証に失敗しました。もう一度お試しください。"
  end
end

処理の流れ

  1. request.env["omniauth.auth"] から AuthHash を取得(OmniAuthが詰めてくれる)
  2. User.from_omniauth でユーザーを検索 or 作成
  3. 成功なら sign_in_and_redirect でDeviseがセッションを確立してログイン完了
  4. 失敗なら新規登録画面にリダイレクトしてアラート表示
  5. failure アクションはユーザーがGoogleの同意画面でキャンセルした時等のフォールバック

Step 9. ログインボタンを設置

app/views/devise/sessions/new.html.erb
<%= link_to "Googleでログイン",
            user_google_oauth2_omniauth_authorize_path,
            data: { turbo: false },
            method: :post,
            class: "btn btn-google" %>

data: { turbo: false } が必要な理由

HotwireのTurboはリダイレクトを横取りするので、外部ドメイン(Google)への遷移が壊れます。Turboを無効化することで通常のフォ
ーム送信としてGoogleに飛ばします。

method: :post が必要な理由

OmniAuth 2.0以降、/auth/:provider への request phase エンドポイントは
POSTのみ受け付けます(omniauth-rails_csrf_protection
が強制)。これはCSRF対策で、GETリンクを踏ませる攻撃を防ぐためです。

全体の流れを再確認

[ユーザー]  「Googleでログイン」をクリック
   │   POST /users/auth/google_oauth2
   ▼
[OmniAuth] request phase: Googleの認可エンドポイントへリダイレクト
   ▼
[Google]   ユーザーがアカウント選択&同意
   │   GET /users/auth/google_oauth2/callback?code=...&state=...
   ▼
[OmniAuth] callback phase:
   │     - state検証(CSRF対策)
   │     - codeをアクセストークンに交換
   │     - ユーザー情報を取得
   │     - request.env["omniauth.auth"] にAuthHashを詰める
   ▼
[Rails]    Users::OmniauthCallbacksController#google_oauth2 を呼ぶ
   │
   ▼
[User.from_omniauth]
   ├─ email_verified チェック
   ├─ (provider, uid) で既存ユーザー検索
   ├─ email衝突チェック
   └─ 新規作成 or 既存ユーザー返却
   ▼
[Devise]   sign_in_and_redirect でセッション確立
   ▼
[ユーザー]  ログイン完了 🎉

セキュリティのポイントまとめ

実装する上で意識した点を整理します。

対策 実装箇所
なりすまし防止 email_verified チェックで未認証Gmailを拒否
アカウント乗っ取り防止 既存emailと衝突時は新規作成せず登録画面へ誘導
DB整合性の最終防衛 (provider, uid) と email にユニークインデックスを張る
CSRF対策 omniauth-rails_csrf_protection + POSTでrequest phaseを叩く
クレデンシャル漏洩防止 client_id/secretは環境変数管理、.env はgitignore
認可スコープ最小化 scope: "email,profile" で必要最小限

よくあるハマりどころ

1. OmniAuth::AuthenticityError が出る

→ omniauth-rails_csrf_protection が入っていない、もしくはログインリンクがGETになっている。POSTで叩く設定にする。

2. 本番でリダイレクトが失敗する

→ リバースプロキシ配下で full_host が http:// になっている。callback_url を環境変数で明示する。

3. redirect_uri_mismatch エラー

→ Google Cloud Consoleの「承認済みリダイレクトURI」と、Railsが生成するコールバックURLが一致していない。プロトコル(http/ht
tps)、ドメイン、パスを完全一致させる。

4. 2回目のログインで別ユーザーが作られる

→ (provider, uid) で検索せず email だけで判定している可能性。Googleのemail変更にも追従できなくなるので、必ず uid
をキーにする。

まとめ

  • OmniAuth がOAuthのプロトコル部分(トークン交換・state検証・ユーザー情報取得)を全部やってくれる
  • Devise の :omniauthable がそれをアプリの認証層につなぐ
  • 開発者が書くのは「AuthHashを受け取ってUserを作って sign_in_and_redirect する」薄いグルーコードだけ
  • セキュリティ的には email_verified チェック・(provider,
    uid)での識別・email衝突拒否・ユニークインデックス
    の4点を押さえれば堅い

OmniAuthの仕組みを理解すると、LINEログインやGitHubログインを追加するのもストラテジgemを差し替えるだけで済むようになり
ます。ぜひ試してみてください!

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?