Quan Nguyen

Google SSO with Devise in Ruby on Rails 7

Published on

NOTE

This is a repost of my Medium article originally published by the Dev Genius publication here.

This article covers the minimum needed to install Google SSO with Devise. It doesn’t cover other good practices along the way. For example, after you run install Devise, it prompts you to include flash messages in your application layout — I won’t be covering these aspects.

Ensure that you have Rails 7 and the compatible Ruby version installed. Below are my versions running on a MacOS Monterey:

$ rails --version && ruby --version
Rails 7.0.3
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]

Initial gem installation and configuration

Add the following gems to your Gemfile:

Gemfile
gem "dotenv-rails"
gem "devise"
gem "omniauth"
gem "omniauth-google-oauth2"
gem "omniauth-rails_csrf_protection"

Install these gems:

$ bundle install

Install Devise:

$ rails g devise:install

In devise.rb, add the following configuration and pass in your credentials (we will cover obtaining these later on):

config/initializers/devise.rb
config.omniauth :google_oauth2, ENV[GOOGLE_OAUTH_CLIENT_ID], ENV[GOOGLE_OAUTH_CLIENT_SECRET]

Set up Devise’s User model

Create the User model:

$ rails g devise user

Hold off on running a migration. We’ll add some tweaks first.

Add the omniauth module to user.rb:

app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
  :recoverable, :rememberable, :validatable,
  **# for Google OmniAuth
  :omniauthable, omniauth_providers: [:google_oauth2]**
end

Go to the migration file in the db/migrate/ directory. Add additional columns to the users database:

db/migrate/20220709132706_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|      **## Custom columns**
      **t.string :full_name
      t.string :uid
      t.string :avatar_url
      t.string :provider**      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""      ...

NOTE

Mine is called 20220709132706_devise_create_users.rb, but yours will have a slightly different name based on the timestamp of when you created the model.

Migrate the database:

$ rails db:migrate

Set up Devise’s controllers

Create controllers for Devise users:

$ rails g devise:controllers users

Specify these controllers in Devise’s configuration in routes.rb:

config/routes.rb
Rails.application.routes.draw do
  devise_for :users**, controllers: {
    omniauth_callbacks: 'users/omniauth_callbacks',
    sessions: 'users/sessions',
    registrations: 'users/registrations'**
  }  ...

Set up methods in the Devise controllers

In the registrations_controller.rb file, add:

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController  **def update_resource(resource, params)
    if resource.provider == 'google_oauth2'
      params.delete('current_password')
      resource.password = params['password']
      resource.update_without_password(params)
    else
      resource.update_with_password(params)
    end
  end**end

Next, in the sessions_controller.rb file, add:

app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  **def after_sign_out_path_for(_resource_or_scope)
    new_user_session_path
  end**  **def after_sign_in_path_for(resource_or_scope)
    stored_location_for(resource_or_scope) || root_path
  end**
end

Then in the omniauth_callbacks_controller.rb file, add:

app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController  **def google_oauth2
    user = User.from_omniauth(auth)**    **if user.present?
      sign_out_all_scopes
      flash[:success] = t 'devise.omniauth_callbacks.success', kind: 'Google'
      sign_in_and_redirect user, event: :authentication
    else
      flash[:alert] =
        t 'devise.omniauth_callbacks.failure', kind: 'Google', reason: "#{auth.info.email} is not authorized."
      redirect_to new_user_session_path
    end
  end**  **protected**  **def after_omniauth_failure_path_for(_scope)
    new_user_session_path
  end**  **def after_sign_in_path_for(resource_or_scope)
    stored_location_for(resource_or_scope) || root_path
  end**  **private**  **def auth**    [**@**](http://twitter.com/auth)**auth ||= request.env['omniauth.auth']
  end**
end

See the from_omniauth method called in the file above? We’ll define it in the User model inside user.rb. This method grabs the user data obtained from the authentication request and create a new record in the User model if it doesn’t exist:

app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         # for Google OmniAuth
         :omniauthable, omniauth_providers: [:google_oauth2]
  **
  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
      user.full_name = auth.info.name # assuming the user model has a name
      user.avatar_url = auth.info.image # assuming the user model has an image
    end
  end**
end

Obtain the Google Sign In credentials

Go to the Google Cloud Console website, create a new project, then select/open the newly created project.

Navigate to the OAuth consent screen page. Opt for the External user type. In the next window, enter your app’s name, user support email, and developer contact information. For my hobby projects, I enter my personal email address for both email fields. Click through to the end to finish setting up the OAuth consent screen.

Navigate to the Credentials page. Click on + Create Credentials and select OAuth client ID. Select Web application for the “Application type”. Enter your app’s name. Under “Authorized redirect URIs”, add the following URI:

[http://localhost:3000/users/auth/google_oauth2/callback](http://localhost:3000/users/auth/google_oauth2/callback)

If/when you have deployed an app to production, return to this page and add your app’s domain as another URI with the same format:

[http://**yourwebsite.com**/users/auth/google_oauth2/callback](http://localhost:3000/users/auth/google_oauth2/callback)

Voila. You should see a popup with your Google Client ID and Client Secret.

Create a file called .env in your app’s root directory and place your Google Client ID and Secret in it:

.env
GOOGLE_OAUTH_CLIENT_ID=Your client id
GOOGLE_OAUTH_CLIENT_SECRET=Your client secret

Add /.env to your .gitignore file so that you don’t expose these credentials to bad actors.

Set up the Google Sign In button

Create Devise views:

$ rails g devise:views

Download a Google Sign In button image file into your app/assets/images/ directory. I recommend downloading the files provided on Google’s Sign-In Branding Guidelines page — my favorite is this one as it works in both light and dark modes: captionless image

Update the login link in _links.html.erb to properly configure it with Google OmniAuth and turn it into a clickable button:

app/views/devise/shared/_links.html.erb
...<%- if devise_mapping.omniauthable? %>
  <%- resource_class.omniauth_providers.each do |provider| %>
    **<%= form_for "Login",
      url: omniauth_authorize_path(resource_name, provider),
      method: :post,
      data: {turbo: "false"} do |f| %>
        <%= f.submit "Login", type: "image", src: url_for("/assets/btn_google_signin_dark_normal_web.png") %>
    <% end %>**
  <% end %>
<% end %>...

Update the image path above (_/assets/btn_google_signin_dark_normal_web.png_) to match the path of the button image you end up picking.

Finally, place the below code in temporarily on your home page view, or wherever you want to surface these links, to test your newly added Google sign in / sign out features:

<% if current_user %>
  <h2><%= current_user.email %></h2>
    <%= image_tag(current_user.avatar_url) %>
    <%= link_to "Edit Account", edit_user_registration_path %>
    <%= button_to "Logout", destroy_user_session_path, data: {turbo: "false"}, method: :delete %>
  <% else %>
    <%= link_to "Login", new_user_session_path %>
  <% end %>