はじめに
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をインストール
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. 環境変数を設定
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
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モデルを更新
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 にプロバイダを登録
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. ルーティングを設定
Rails.application.routes.draw do
devise_for :users, controllers: {
omniauth_callbacks: "users/omniauth_callbacks"
}
# ... その他のルート ...
end
なぜ controllers: で上書きするのか?
デフォルトの Devise::OmniauthCallbacksController には google_oauth2 アクションが存在しません。「コールバック後に何
をするか」はアプリ固有のロジックなので、自分でコントローラを作って指定する必要があります。
Step 8. OmniauthCallbacksController を作成
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
処理の流れ
-
request.env["omniauth.auth"]から AuthHash を取得(OmniAuthが詰めてくれる) -
User.from_omniauthでユーザーを検索 or 作成 - 成功なら
sign_in_and_redirectでDeviseがセッションを確立してログイン完了 - 失敗なら新規登録画面にリダイレクトしてアラート表示
-
failureアクションはユーザーがGoogleの同意画面でキャンセルした時等のフォールバック
Step 9. ログインボタンを設置
<%= 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を差し替えるだけで済むようになり
ます。ぜひ試してみてください!