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.
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_protection
gem 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:
The next step requires us to provide information about the application itself:
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:
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:
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.
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:
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.
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.
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!