← All posts

August 31, 2023

How to add Google Sign-In to Rails without Devise

In another article, I explained how to implement Google OAuth by using Devise. Devise has its advantages if you're planning to implement more complex login flows or options, such as login via email, recovering passwords, expiring sessions, etc.

However, if you want to stick with the bare minimum, relying purely on the Omniauth gem is the approach described here.

Add dependencies to your Gemfile

gem 'omniauth-google-oauth2'

Then run bundle install to install the dependency.

Create the :users table

The absolute minimum is email, provider, and uid, but a few extra columns such as image, first name, and last name are recommended.

Generate your migration file with:

rails g model User first_name last_name email avatar_url provider uid

This will create a migration file:

# db/migrate/20250329060602_create_users.rb

class CreateUsers < ActiveRecord::Migration[8.0]
  def change
    create_table :users do |t|
      t.string :first_name
      t.string :last_name
      t.string :email
      t.string :avatar_url
      t.string :provider
      t.string :uid

      t.timestamps
    end
  end
end

Now run the migration to create the table:

rails db:migrate

Update the User model

Inside the User model, provide a few helpers, such as name, to concatenate both first and last names into a single string.

# app/models/user.rb

class User < ApplicationRecord
  def name
    [first_name, last_name].join(' ')
  end

  def update_dynamic_attributes(auth)
    self.first_name = auth.info.first_name
    self.last_name = auth.info.last_name
    self.email = auth.info.email
    self.avatar_url = auth.info.image if auth.info.image?
  end

  class << self
    def find_or_create_with_omniauth(auth)
      user = find_by(auth.slice(:provider, :uid)) || initialize_from_omniauth(auth)
      user.update_dynamic_attributes(auth)
      user.save!
      user
    end

    def initialize_from_omniauth(auth)
      new do |user|
        user.provider = auth.provider
        user.uid = auth.uid
      end
    end
  end
end

Rails Routes

Add a required callback route, but also two custom routes for the convenience of using sign_in_path and sign_out_path:

# config/routes.rb

Rails.application.routes.draw do
  # AUTHENTICATION ROUTES
  get 'auth/:provider/callback', to: 'sessions#create'
  get '/sign_out', to: 'sessions#destroy', as: :sign_out
  get '/auth/google_oauth2', as: :sign_in
  get '/auth/failure' => 'sessions#failure'
end

Create your Omniauth initializer file. It has to live inside config/initializers, or Rails never loads it:

# config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2,
           Rails.application.credentials.dig(:google, :client_id),
           Rails.application.credentials.dig(:google, :client_secret)
end

OmniAuth.config.allowed_request_methods = %i[get]
OmniAuth.config.silence_get_warning = true

The last line matters because OmniAuth 2 logs a warning on every boot when GET is allowed. You made that call on purpose, no need to hear about it forever.

On your controller, handle sign in, sign out, and eventual failures:

# app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def create
    user = User.find_or_create_with_omniauth(request.env['omniauth.auth'])
    reset_session
    session[:user_id] = user.id
    redirect_to after_sign_in_path, notice: "Signed in as #{user.name}"
  end

  def failure
    # Sentry.capture_message(params[:message])
    redirect_to root_url, alert: 'Authentication failed.'
  end

  def destroy
    reset_session
    redirect_to root_url, notice: 'Signed out'
  end

  private

  def after_sign_in_path
    # Try to get the custom path passed as a param (e.g., ?after_sign_in_path=/dashboard)
    after_sign_in_param = request.env["omniauth.params"]&.[]("after_sign_in_path")

    # Fallback to the original URL the user came from before being redirected to sign in
    origin_url = request.env["omniauth.origin"]

    # Return the first available option: custom param > origin > root
    after_sign_in_param || origin_url || root_path
  end
end

The two reset_session calls matter. On sign in, rotating the session id blocks session fixation: an attacker who planted a session cookie before login would otherwise end up sharing the authenticated session. On sign out, wiping the whole session is safer than nulling a single key.

Add a current_user method

Create a convenient method, current_user:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  helper_method :current_user

  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end
end

Allow users to sign in and sign out

Add a couple of links to your views so that users can log in and log out:

<% if current_user %>
  Signed in as <%= current_user.name %>
  <%= link_to 'Sign out', sign_out_path %>
<% else %>
  <%= link_to 'Sign in with google', sign_in_path %>
<% end %>

Add Devise when you need email/password login, password reset, or multiple providers. For Google-only auth, this is everything.