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 return false when provider.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