Add social login to a Rails application

By Exequiel Rozas

Social authentication is an important feature for web and mobile applications because there's a significant amount of users that prefer it over the typical password based authentication.

Brand trust and comfort makes social login a sure election for some users, especially if they're not technically savvy.

In this article, we will explore how to add social login to a Rails application using Devise and the OmniAuth gem to implement Google and GitHub authentication.

Let's start by learning how OAuth and social login work under the hood:

How social login works

Social login is the colloquial name used to refer to the implementation of the OAuth2 protocol to sign users into a given application.

The protocol was created to allow applications to access resources from a third party in behalf of a user without the user providing her third-party's credentials to the application itself.

The third party can share as little as the user's email and profile information, or it can allow access to resources owned by the user.

For our intents and purposes we just want the identity provider to give us a sort of “key” that corroborates that the user approved to share some resources with us, an email and profile for example.

We then use that information to create a session for the user by returning a cookie used to further authenticate requests to our application.

Social login in Rails with Devise and OmniAuth diagram

The main advantages of this approach are:

  • The user can sign in or sign-up with a few clicks without having to think about passwords or yet another username.
  • The user doesn't have to trust us with credentials and the authorization given to us doesn't allow us to act maliciously even if we wanted to. Plus, the authorization can be revoked at will.
  • As risk is minimized, the chances for a successful signup are maximized.

In my experience, around 60% of the users tend to pick social login over password authentication.

If we don't provide the feature to our users, we will probably be missing a considerable amount of signups, specially if the users are not tech-savvy.

Now that we explored how social login with Rails work, let's set our application:

Rails application setup

In order to build this feature, we will use a Rails application with a User model generated with Devise and an authenticated-only Book model where book details are shown.

In order to make social login work we need to use the omniauth gem which allows us to handle multi-provider authentication for Rack based Ruby applications.

Each identity provider (Google, GitHub, Facebook, etc.) is a strategy that implements the necessary logic to handle authentication for its provider. By convention, each strategy is implemented on a gem of its own, even though they're all used together with OmniAuth.

Assuming we have a User model, we need to configure a couple of things to handle social sign-in.

First, we have to add a provider and a uid string fields to our users table. We can create a migration for that or, if you're just creating your user using rails generate devise User you can add the fields in that migration.

Next, we have to tell Devise to add the OAuth features and tell the providers we will be using:

# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticable, :omniauthable, 
        omniauth_providers: [:github, :google_oauth2]
end

However, this won't work unless we have the OmniAuth and strategies gems:

# Gemfile
gem "omniauth"
gem "omniauth-github"
gem "omniauth-google-oauth2"
gem 'omniauth-rails_csrf_protection'

Note that we need to add the omniauth-rails_csrf_protectiongem in order to disable access to the OAuth request using a GET request which is actually a security vulnerability.

We also have to add the following to our routes:

# /config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

Next, we add an empty controller to handle the callbacks:

# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbackController < Devise::OmniauthCallbacksControloler
end

There's one last piece of configuration we have to add to the devise.rb initializer, but we first need to obtain the credentials from the identity providers.

We will start by implementing the Google flow, and then we will do the same thing with GitHub.

Obtaining credentials

Before we can integrate the sign-in flows to our application we need to set the OAuth applications with our providers. In this case, we will explore how to do it with Google and GitHub.

The process is similar with other providers, and you should get the gist by following along:

Google OAuth Consent Screen credentials

We need a Google Developer account and then go to the Google Developer Console, create a project and then register and configure an OAuth Consent Screen.

First, we have to choose whether the screen is to be used exclusively by internal users or if external users may access it too:

Setting an OAuth consent screen with Google

The next step requires us to provide information about the application itself:

Google OAuth consent screen second step

We will need to provide the following information:

  • The app name: this will be shown to users and should be familiar to them at the time of the sign-in attempt.
  • Support email: an email where users can contact us about issues with the consent flow.
  • Application logo: also shown to users at the consent flow. It should also be familiar to users otherwise they might think they're in the wrong place.
  • App domain: it appears on the consent screen associated with the brand and can allow users to go back to where the flow started.
  • Privacy policy and terms of service: a link to the app's privacy policy and terms of service pages.
  • Authorized domains: a list of Google-verified domains that can act as origins for the OAuth flow.
  • Developer contact information: lastly, it asks for a developer email that will be used to receive communications by Google about changes to the project.

Next, we have to set up the required scopes for our consent flow by clicking on the Add or Remove Scopes button:

Setting scopes for Google OAuth Consent flow

In our case we will just pick the first two values which are auth/userinfo.email and auth/userinfo.profile. If you need to add more scopes you can add them manually:

Google OAuth consent screen: adding scopes

After we set the scopes, we have to add Test users to our consent flow. You can use your emails or the emails for early users or testers. Remember that to go live the app must be verified.

Once we're finished adding the users, we need to create the app. At first, it will be in the Testing status, but that's ok because that's all we need for now.

Next, we have to go to the Credentials section of the console and create an OAuth 2.0 Client ID credentials in order to identify our application.

Google OAuth Consent create OAuth client ID

Here, we need to choose Web application for the application type, give it a name to later identify it in the console, and then we need to add Authorized JavaScript origins which are the HTTP origins that host our application.

I would recommend adding the Rails default http://localhost:3000 and any other ports you might use and, if you can, add a fixed-domain tunnel to help with testing.

We also have to add Authorized Redirect URIs. With any OAuth implementations, these are the URIs that “listen” to the identity provider callback and execute the provider-specific code within Users::OmniauthCallbacks.

If we followed the steps above to add OAuth to devise and added Google as a provider, Devise gives us the /users/auth/google_oauth2/callback route to handle the last part of the flow so, unless we've customized the user route prefix we can add http://localhost:3000/users/auth/google_oauth2/callback.

You can always do bin/rails routes | grep callback if you want to be sure about which URL is being generated.

After we add those and confirm, the OAuth ID will be created and Google will give us the CLIENT_ID and CLIENT_SECRET we need.

Please remember, these credentials are restricted to the test users we added after setting our app's scopes.

If you want to deploy the Google sign-in to production you will have to comply with Google's OAuth policies and have your application approved by Google.

GitHub OAuth App credentials

To obtain our GitHub credentials for the sign-in flow, we need to go to the Developer settings section, specifically the OAuth Apps page:

GitHub Developers create new OAuth app

We click on the New OAuth app button and register a new app with the following information:

  • Application Name: shown to users. It should be familiar to our users, otherwise they might abandon the sign-in process.
  • Homepage URL: this is also shown to users to go back to our homepage in case they don't want to continue.
  • Application description: a brief description about our application. It's an optional field, but it may help users trust our auth flow a bit more.
  • Authorization callback URL by default it's /users/auth/github/callback.
  • Enable device flow: we can leave this set to false unless we want a headless authentication experience like using a CLI.

After filling the form an OAuth App is successfully created with GitHub, and we're redirected to it. From there we can see our client_id value, to fetch the client_secret we need to click on the Generate a new client secret button.

GitHub OAuth App client_id and secret

Once we have this configured, we can add the GitHub login to our application.

Implementing social login

Now that we have the credentials ready, we can jump into the meaty part of implementing the feature:

The first step is to securely store our credentials. We can use Rails credentials or environment variables. For the sake of simplicity I'm going to be using ENV variables:

# config/initializers/devise.rb
config.omniauth :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], scope: 'user'
config.omniauth :google_oauth2, ENV["GOOGLE_OAUTH_CLIENT_ID"], ENV["GOOGLE_OAUTH_CLIENT_SECRET"]

Consider that you can add more permissions to the GitHub scope but user is more than enough for what we need. Google permissions are explicitly set when we create the consent screen and can't be overwritten by configuration.

Next, we will add the sign-in form with the social login buttons included.

Rails sign in form using Devise and OmniAuth

This is actually a typical Devise sign-in form with the addition of the OmniAuth buttons. The following is the code for the buttons:

<div class="mt-3 flex justify-center items-center gap-4 w-full">
  <%= button_to user_google_oauth2_omniauth_authorize_path, class: "w-full inline-flex justify-center py-2 px-6 bg-white border border-gray-500 rounded-md text-sm font-semibold text-gray-600 hover:bg-gray-100", method: :post, data: { turbo: false } do %>
    <span class="sr-only">Sign in with Google</span>
    <%= image_tag "google-oauth-icon.png", alt: "Google", class: "w-5 h-5 mr-2 fill-gray-100" %>
    <span>Google</span>
  <% end %>
  <%= button_to user_github_omniauth_authorize_path, class: "w-full inline-flex justify-center py-2 px-6 bg-white border border-gray-500 rounded-md text-sm font-medium text-gray-800 hover:bg-gray-900", method: :post, data: { turbo: false } do %>
    <span class="sr-only">Sign in with Github</span>
    <%= inline_svg_tag "github-icon-black.svg", alt: "GitHub", class: "w-5 h-5 mr-2 fill-gray-100" %>
    <span>GitHub</span>
  <% end %>
</div>

As you can see, the buttons have to include the method: :post attribute in order to make a POST request to their corresponding endpoint in order to receive the redirection to the authorization dialog screen.

It's better to use a button instead of a link because search engine crawlers follow links and they might encounter errors for this specific links.

They should also add the data-turbo=false attribute in order to avoid a CORS issue: by default, Turbo tries to make a request to the GitHub authorization page which fails.

If everything's correctly set up, clicking the button will redirect us to the consent screen. If we approve the sign-in process, the identity provider, Google or GitHub in this case, will send a request to our callback URL with information about the user that just approved the flow.

Then, we need to process this request within our OmniauthCallbacksController. The callback request will map to a method in the callback's controller with the same name as the provider.

The following code can handle the authorization flow:

# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def github
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: "Github") if is_navigational_format?
    else
      session["devise.github_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  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_path)
    end
  end
end

The .from_omniauth is defined in the User class, and it's supposed to return a user instance. We can implement it like so:

# app/models/user.rb
class User < ApplicationRecord
  def self.from_omniauth(auth)
    if where(email: auth.info.email).exists?
      return_user = where(email: auth.info.email).first
      return_user.provider = auth.provider
      return_user.uid = auth.uid
    else
      return_user = 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.username = auth.info.name
      end
    end
    return_user
  end
end

The from_omniauth code is supposed to create a new User if it doesn't exist or return a user if it does, based on and email provided by the auth variable returned by the OAuth callback.

There are a lot of changes or improvements we can implement to make this code better, but this is the basic flow to implement social login using Rails and Devise.

Now that we know how to implement the feature, let's talk about best practices around it:

Social login best practices

Whenever you're adding social login options to your app, consider the following:

  • Always have password authentication as an alternative: some users really hate password auth, but some users prefer it over social login. As rare as it may sound, some users don't even have any social media accounts, we don't want to leave them out.
  • Consider adding Apple auth: if you ever need to make a mobile app, Apple will require you to add their own OAuth authentication flow if at least one other OAuth flow is present. It will also please Apple users because they have the option to hide their e-mail address from use, substituting it with an Apple provided address that redirects to their own inbox.
  • Don't over do it: providing more sign-in options can seem like a good idea, but don't over do it: paralysis by analysis is a thing and can actually decrease your conversions.

Adding other providers

If you need to add other providers you can do so using a very similar flow to the one explained in this article.

Actually, the OmniAuth gem lists 337 strategies of which 75 are official. This means that the provider you're trying to integrate is likely in that list.

Otherwise, you can implement your own strategy following OmniAuth's strategy contribution guide

The flows are going to be very similar. Sometimes, obtaining the credentials is actually the most difficult part.

If you want us to make a specific tutorial for any other provider, let us know.

Summary

Implementing social login with Rails is a good way to increment our sign-up conversion: users tend to trust known brands more than they trust us with their credentials.

Adding social login to a Rails application is mostly about obtaining credentials to interact with the identity providers and following a couple of steps that are mostly the same for every provider.

If we need customization, we can just slightly change how our .from_omniauth method works or adding the customization to every individual provider action in the OmniAuth callbacks controller.

Hope this article helped you implement and understand social login for your Rails application. If you have any doubts about it, let us know!

Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.