Google OAuth — Implementation
Scope: Google OAuth2 login via Devise + OmniAuth Tests: All passing (0 failures, 0 errors)
1. Gems Added
File: Gemfile
gem "omniauth"
gem "omniauth-google-oauth2"
gem "omniauth-rails_csrf_protection"
- omniauth + omniauth-google-oauth2 — Google OAuth2 login via Devise
- omniauth-rails_csrf_protection — CSRF protection for OmniAuth POST routes
2. Database Migration
db/migrate/20260228142542_add_omniauth_to_users.rb (NEW)
Adds OAuth provider tracking columns to the users table:
class AddOmniauthToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :provider, :string
add_column :users, :uid, :string
add_index :users, [:provider, :uid], unique: true
end
end
Schema result: users table gains provider (string) and uid (string) with a unique composite index.
3. Models
3a. app/models/user/omniauth_handler.rb (NEW)
Resolves OmniAuth auth hash to a User (find by provider+uid, find by email and link, or create new):
class User::OmniauthHandler
private attr_reader :auth
def initialize(auth)
@auth = auth
end
def resolve
find_by_provider || find_and_link_by_email || create_new_user
end
private
def find_by_provider
User.find_by(provider: auth.provider, uid: auth.uid)
end
def find_and_link_by_email
user = User.find_by(email: auth.info.email)
return unless user
user.update!(provider: auth.provider, uid: auth.uid)
user
end
def create_new_user
User.create!(
email: auth.info.email,
name: auth.info.name,
provider: auth.provider,
uid: auth.uid,
password: Devise.friendly_token[0, 20]
)
end
end
3b. Modified: app/models/concerns/user/authentication.rb
- Added
:omniauthable, omniauth_providers: [:google_oauth2]to Devise modules - Added
password_required?override to returnfalsewhenprovider.present?
4. Controllers
app/controllers/users/omniauth_callbacks_controller.rb (NEW)
Handles the Google OAuth2 callback:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
auth = request.env["omniauth.auth"]
@user = User::OmniauthHandler.new(auth).resolve
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"] = auth.except(:extra)
redirect_to new_user_registration_url, alert: @user.errors.full_messages.join("\n")
end
end
def failure
redirect_to root_path, alert: "Authentication failed: #{failure_message}"
end
end
5. Views
Modified: app/views/devise/shared/_links.html.erb
Updated OmniAuth provider links section to gracefully handle missing provider routes (e.g., when Google OAuth credentials are not configured in development/test). Uses begin/rescue to check if omniauth_authorize_path is available before rendering the button.
6. Routes
Modified: config/routes.rb
devise_for :users, controllers: {
registrations: "users/registrations",
sessions: "users/sessions",
omniauth_callbacks: "users/omniauth_callbacks" # NEW
}
Route Summary
| Route | Method | Path | Controller#Action |
|---|---|---|---|
| OmniAuth authorize | POST | /users/auth/google_oauth2 |
(OmniAuth middleware) |
| OmniAuth callback | GET/POST | /users/auth/google_oauth2/callback |
users/omniauth_callbacks#google_oauth2 |
7. Initializers
Modified: config/initializers/devise.rb
OmniAuth Google provider: Conditionally configured:
if credentials.dig(:google_oauth, :client_id).present?
config.omniauth :google_oauth2,
credentials.dig(:google_oauth, :client_id),
credentials.dig(:google_oauth, :client_secret),
scope: "email,profile",
prompt: "select_account"
end
8. Summary of Files
New Files (3)
| File | Purpose |
|---|---|
db/migrate/20260228142542_add_omniauth_to_users.rb |
Add provider/uid to users |
app/models/user/omniauth_handler.rb |
Google OAuth user resolver |
app/controllers/users/omniauth_callbacks_controller.rb |
Google OAuth callback |
Modified Files (4)
| File | Changes |
|---|---|
Gemfile |
Added omniauth, omniauth-google-oauth2, omniauth-rails_csrf_protection |
app/models/concerns/user/authentication.rb |
Added omniauthable, password_required? override |
config/initializers/devise.rb |
Added Google OAuth provider config |
config/routes.rb |
Added omniauth_callbacks controller |
app/views/devise/shared/_links.html.erb |
Graceful OmniAuth provider route handling |