Integrating Social Login in a Ruby on Rails Application

By Noman Ur Rehman

Facebook Developers Page

I remember the old days when people had to register for an account separately on each website.

It was a boring and tedious process to repetitively enter the same information over and over again on each website’s registration page.

Times have changed and so has the way people use their preferred websites and services.

After the advent of the OAuth2 specification, it has become quite a trivial task to allow your users to sign in to your application using a third party service.

Logging in through third party services has become such an important option that if your application does not have it, it seems a bit out-dated.

So, in this tutorial, we are going to learn how to allow your users to log in using their social media accounts.

During the course of this tutorial, you will learn.

  1. Creating an application on Facebook, Github, Google, and Twitter.
  2. Adding login strategies for Facebook, Github, Google, and Twitter to a Rails application.
  3. Writing callbacks to authenticate users upon redirection.

This tutorial assumes you have configured Devise without third party authentication and users are able to use your on-site Devise features. It is beyond the scope of this tutorial to demonsrate how to fully customize Devise and setup it’s on-site features. The repository for this tutorial includes the code you need to fully set up and customize Devise along with the code discussed as part of this tutorial.

Creating Applications

Though it is a bit out of scope, however, to round things up nicely, let us have a look at how to create an application through the respective third party websites.

Before we begin creating applications, there is a small bit regarding callback url that we need to talk about as we will need it when registering an application.

Some of the third party OAuth providers require that you specify a callback url when you create an application.

The callback url is used to redirect the user back to your application after they have granted permissions to your application and added it to their account.

Devise works by providing a callback url for each provider that you use.

The callback url follows the convention //auth//callback where provider is the gem name which is used to account for a specific third party login strategy.

For example, if my application is hosted at http://www.myapp.com and I have created Devise for the users entity whom I wish to allow to log in using their Twitter account, the callback url, considering the gem name that provides the strategy is twitter, would be http://www.myapp.com/users/auth/twitter/callback.

We are going to confirm the callback routes later in this tutorial once we are done setting up the different providers.

Creating a Facebook Application

Log in to your Facebook account and browse to the url https://developers.facebook.com.

I am assuming you have not registered for a Facebook developer account and have never created a Facebook application before.

Click the Register button at the top-right of the page.

Facebook Developer Registration

Accept the Facebook developer agreement(in the modal dialog) by turning the switch to YES and clicking the Register button.

Facebook Create Application

Click the Create App ID button that shows up in the same modal dialog.

Facebook Application Details

Fill in the Display Name, and Contact Email fields and click the Create App ID button.

Once your application is created, you will be taken to the application settings page.

Facebook Application Basic Settings

Choose Settings > Basic from the left menu.

Enter localhost in the App Domains field.

Click the Add Platform button at the bottom of the page.

Facebook Website Platform

Choose Website as the platform.

Enter http://localhost:3000 in the Site URL field.

Click the Save Changes button at the bottom of the page.

Facebook Application Dashboard

Choose Dashboard from the left menu.

Note down the App ID, and App Secret shown on the page as they will be needed later.

Creating a Github Application

Log in to your Github account.

Github Settings

Once you have logged in, click your account avatar at the top-right and choose Settings from the drop-down menu.

Github Register Application

On the Settings page, choose Developer settings > OAuth applications from the left menu.

Click the Register a new application button.

Github New Application

Fill in the Application name, Homepage URL, and Application description fields.

Enter http://localhost:3000/users/auth/github/callback in the Authorization callback URL field.

Click the Register application button.

Github Application Details

Once your application is created, you will be taken to the application page.

Note down the Client ID, and Client Secret shown on the page as they will be needed later.

Creating a Google Plus Application

Log in to your Google account and browse to the url https://console.developers.google.com/apis/library.

Google Developer Dashboard

On the Google developer console, choose Credentials from the left menu.

Google Credentials Page

Click the Create credentials button and choose OAuth client ID from the menu that pops up.

Google Create Credentials

For your Application type, choose Web application.

Google Application Type

Fill in the Name field.

Under the Restrictions section, enter http://localhost:3000 in the Authorized JavaScript origins field.

Enter http://localhost:3000/users/auth/google_oauth2/callback in the Authorized redirect URIs field and click the Create button.

Google Application Page

Once your application is created, you will be shown the client ID, and client secret in a modal dialog.

Google Credentials Modal

Note down the client ID, and client secret shown in the modal dialog as they will be needed later.

Creating a Twitter Application

Log in to your Twitter account and browse to the url https://apps.twitter.com.

Twitter Developers Page

On the Twitter apps page, click the Create New App button.

Twitter Create Application

Fill in the Name, Description, and Website fields.

Enter http://localhost:3000/users/auth/twitter/callback in the Callback URL field.

Accept the Developer Agreement and click the Create your Twitter application button.

On the application page, that is shown next, click the Settings tab.

Twitter Settings Tab

Enter a mock url in the Privacy Policy URL, and Terms of Service URL field and click the Update Settings button.

Twitter Permissions Tab

Click the Permissions tab and change the Access type to Read only.

Check the Request email addresses from users field under the Additional Permissions section and click the Update Settings button.

Twitter Keys Tab

Click the Keys and Access Tokens tab.

Note down the Consumer Key (API Key), and Consumer Secret (API Secret) shown on the page as they will be needed later.

Adding Gems

We are going to need a number of gems to make authentication through third party providers work.

Apart from that, we are also going to add two additional gems.

The first one will help us store user sessions in the database while the second one will only be used in the development environment to set environment variables.

The reason we will allow our application to save user sessions in the database is because there is a limit to how much data you can store in a session which is four kilo-bytes. Using database as the session store will overcome this limitation.

As for using a gem to set environment variables in the development environment, it is because we will be using a lot of third party application information that needs to be kept secret.

Therefore, it is recommended to expose this information to our application as environment variables instead of adding it directly to a configuration file.

Open the file Gemfile and add the following gems.

# Use Devise for authentication
gem 'devise', '~> 4.2'
# Use Omniauth Facebook plugin
gem 'omniauth-facebook', '~> 4.0'
# Use Omniauth Github plugin
gem 'omniauth-github', '~> 1.1', '>= 1.1.2'
# Use Omniauth Google plugin
gem 'omniauth-google-oauth2', '~> 0.4.1'
# Use Omniauth Twitter plugin
gem 'omniauth-twitter', '~> 1.2', '>= 1.2.1'
# Use ActiveRecord Sessions
gem 'activerecord-session_store', '~> 1.0'

We have started off by adding the Devise gem.

Devise gem supports integration with Omniauth which is a gem that standardizes third party authentication for Rails applications.

Therefore, following the Devise gem, we have simply added the Omniauth strategies we need, namely, facebook, github, google-oauth2, and twitter.

Database sessions are facilitated by the activerecord-session_store gem which has been added towards the bottom.

The last gem we need to add is the dotenv gem.

However, since this gem will only be used in the development environment, we need to add it to the development group in the Gemfile.

Open the Gemfile, locate the group :development do declaration, and append the following gem.

group :development do
            .
            .
            .
  # Use Dotenv for environment variables
  gem 'dotenv', '~> 2.2.1'
end

All our gems have been added.

Execute the following command at the root of your project to install the added gems.

$ bundle install --with development

Bundle Install Command

We are done as far as the gems for our project are concerned.

Setting Environment Variables

The dotenv gem we added earlier allows us to create a .env file at the root of our project and set environment variables easily.

However, if you are using source control like Git, make sure the .env file is ignored and not committed to your repository as it will contain confidential information.

You can however, add a .env.example file with placeholder data for the environment variables and commit it to the repository to show other developers on the project how information needs to be added to the .env file.

Also recall, when creating the third party applications, I instructed you to note down the respective client id and secret which we will be using here.

Create a .env file at the root of your project and add the following code.

FACEBOOK_APP_ID=<facebook-app-id>
FACEBOOK_APP_SECRET=<facebook-app-secret>
GITHUB_APP_ID=<github-app-id>
GITHUB_APP_SECRET=<github-app-secret>
GOOGLE_APP_ID=<google-app-id>
GOOGLE_APP_SECRET=<google-app-secret>
TWITTER_APP_ID=<twitter-app-id>
TWITTER_APP_SECRET=<twitter-app-secret>

The , and needs to be replaced with your application id and secret.

Similarly, replace the remaining placholders with the information provided to you by the respective third parties.

Configuration

For our application configuration, we only need to touch a couple of areas, Devise and the session configuration.

Configuring Devise

Once we have added our provider application information as environment variables, we need to configure Devise to use it as part of the corresponding provider strategy.

Open the file config/initializers/devise.rb and add the following code.

# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.

config.omniauth :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], scope: 'public_profile,email'
config.omniauth :github, ENV['GITHUB_APP_ID'], ENV['GITHUB_APP_SECRET'], scope: 'user,public_repo'
config.omniauth :google_oauth2, ENV['GOOGLE_APP_ID'], ENV['GOOGLE_APP_SECRET'], scope: 'userinfo.email,userinfo.profile'
config.omniauth :twitter, ENV['TWITTER_APP_ID'], ENV['TWITTER_APP_SECRET']

You can use the comments in the above code snippet to locate the section of the configuration file where you need to add the Omniauth strategy settings.

The config.omniauth method lets you add and configure an Omniauth strategy.

In our case, we have simply passed the name of the strategy, and the application id and secret using environment variables.

There is also an additional scope parameter which has been added to some of the providers. It helps us specify the amount of control we wish to have over the authenticated user’s data.

The reason the scope parameter is optional is some of the providers allow you to specify the scope when you create an application so there is no need to be explicit in such a case.

Also notice, the strategy names(facebook, github, google_oauth2, and twitter) are the same as the gem name for the respective strategy.

Configuring Sessions

Open the file config/initializers/session_store.rb and replace the Rails.application.config.session_store directive with the following code, completely replacing the single line of code contained in the file.

Rails.application.config.session_store :active_record_store, key: '_devise-omniauth_session'

And we are done!

Writing Migrations

In order to allow our users to login using third party providers, we need to update the users table, more generally, the entity table you have generated that Devise uses to authenticate users.

I am going to assume the Devise entity is user but you can very well replace this entity name for your case.

We are also going to create a table to store user sessions.

Update Users Table Migration

Execute the following command at the root of your project to generate a update users table migration.

$ rails generate migration update_users

Rails Generate Migration Command

Open the file db/migrate/update_users.rb and add the following code.

class UpdateUsers < ActiveRecord::Migration[5.0]
  def change
    add_column(:users, :provider, :string, limit: 50, null: false, default: '')
    add_column(:users, :uid, :string, limit: 500, null: false, default: '')
  end
end

The provider and uid fields help to identify a user uniquely as this pair will always have unique values.

For our case, the provider can be Facebook, Github, Google, or Twitter and the uid will be the user id assigned to a user by any of these third parties.

Create Sessions Table Migration

Execute the following command at the root of your project to generate a create sessions table migration.

$ rails generate migration create_sessions

Rails Generate Migration Command

Open the file db/migrate/create_sessions.rb and add the following code.

class CreateSessions < ActiveRecord::Migration
  def change
    create_table :sessions do |t|
      t.string :session_id, null: false
      t.text :data
      t.timestamps
    end

    add_index :sessions, :session_id, unique: true
    add_index :sessions, :updated_at
  end
end

Our sessions table stores the session id and data with timestamps.

We have also added an index to the session_id and updated_at fields respectively as it will help with searching user sessions when they return to our application.

Migrating the Database

Execute the following command at the root of your project to migrate the database.

$ rails db:migrate

Rails Database Migration Command

You may go ahead and browse the database to make sure the respective tables were created and updated.

Updating User Model

We are going to add a method to our user model that will create the user record in the database using the data provided by the third party provider.

We also need to register the Omniauth strategies in our user model so that they are picked up by Devise.

Again, your Devise entity may be different and so will be the model’s file name.

Open the file app/models/user.rb and add the following code.

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise  :database_authenticatable, :registerable,
        :recoverable, :rememberable, :trackable, :validatable,
        :confirmable, :lockable, :timeoutable,
        :omniauthable, omniauth_providers: [:facebook, :github, :google_oauth2, :twitter]

  def self.create_from_provider_data(provider_data)
    where(provider: provider_data.provider, uid: provider_data.uid).first_or_create do | user |
      user.email = provider_data.info.email
      user.password = Devise.friendly_token[0, 20]
      user.skip_confirmation!
    end
  end
end

The omniauth_providers array passed to the devise method helps us register the Omniauth strategies.

The array contains symbolized names of the strategies. These names come from and should be same as the gem name for the respective Omniauth strategy.

The create_from_provider_data method is passed the data provided by the third party and is used to create the user in the database.

The user is first searched using the provider string and user id(uid) by the first_or_create method.

The first_or_create method would either fetch the user if it found in the database or create it if it is not present.

Inside the first_or_create block, we have simply set the user attributes from the provider data, which for our case is only the user’s email.

There are two parts worth mentioning inside the block.

The first one is the user.password = Devise.friendly_token[0, 20] which sets an arbitrary password for the user since it is not exposed by the provider and is required to create a user.

The second one is the user.skip_confirmation! declaration which skips the user email verification process since it has already been verified by the respective provider.

If you have added other fields to your Devise entity table such as first_name, last_name, and date of birth, you can set these fields to the corresponding field values in the third party provider data.

The Callbacks Controller

What we need to work on next is to add the controller that will be handling the third party redirects back to our application.

Execute the following command to generate an Omniauth callbacks controller.

$ rails generate controller users/omniauth

Rails Generate Controller Command

I have appended users/ before the controller name to generate it under a directory same as the Devise entity.

You can change it based on your Devise entity or if you are using multiple Devise entities, you can altogether skip adding the controller under a separate directory by simply executing rails generate controller omniauth.

It is a Devise convention to create a controller method named as the strategy that it will be handling the callback for so we will need to add four methods named facebook, github, google_oauth2, and twitter respectively to our Omniauth controller.

The controller actions that follow should be added to the app/controllers/users/omniauth_controller.rb file that we have just created.

Facebook Callback

# facebook callback
def facebook
  @user = User.create_from_provider_data(request.env['omniauth.auth'])
  if @user.persisted?
    sign_in_and_redirect @user
    set_flash_message(:notice, :success, kind: 'Facebook') if is_navigational_format?
  else
    flash[:error] = 'There was a problem signing you in through Facebook. Please register or try signing in later.'
    redirect_to new_user_registration_url
  end 
end

The user data provided by the third party is available to our application in the request environment variable request.env['omniauth.auth'] so we have passed it to the create_from_provider_data method we created earlier.

If the user is saved to the database, we set a flash message using the set_flash_message helper method provided by Devise, sign the user in and redirect them to their homepage.

In case the user is not saved to the database, a flash error message is set and the user is redirected to the registration page.

The code for the remaining provider callbacks is very similar, other than the flash message text.

Github Callback

# github callback
def github
  @user = User.create_from_github_data(request.env['omniauth.auth'])
  if @user.persisted?
    sign_in_and_redirect @user
    set_flash_message(:notice, :success, kind: 'Github') if is_navigational_format?
  else
    flash[:error] = 'There was a problem signing you in through Github. Please register or try signing in later.'
    redirect_to new_user_registration_url
  end
end

Google Callback

# google callback
def google_oauth2
  @user = User.create_from_google_data(request.env['omniauth.auth'])
  if @user.persisted?
    sign_in_and_redirect @user
    set_flash_message(:notice, :success, kind: 'Google') if is_navigational_format?
  else
    flash[:error] = 'There was a problem signing you in through Google. Please register or try signing in later.'
    redirect_to new_user_registration_url
  end 
end

Twitter Callback

# twitter callback
def twitter
  @user = User.create_from_twitter_data(request.env['omniauth.auth'])
  if @user.persisted?
    sign_in_and_redirect @user
    set_flash_message(:notice, :success, kind: 'Twitter') if is_navigational_format?
  else
    flash[:error] = 'There was a problem signing you in through Twitter. Please register or try signing in later.'
    redirect_to new_user_registration_url
  end 
end

Failure Callback

Apart from the respective provider callbacks, we also need to add a failure callback which Devise will execute for all cases where authentication fails for some reason.

It could be that the redirection failed or the user did not grant permissions to your application.

Add the following failure callback, below the provider callbacks we added earlier.

def failure
  flash[:error] = 'There was a problem signing you in. Please register or try signing in later.' 
  redirect_to new_user_registration_url
end

Adding the Sign In Links

You might think that we need to add the appropriate links to redirect users to third party applications to allow them to sign in to our application but this is taken care of by Devise.

Open the file app/views/devise/shared/_links.html.erb and locate the following code snippet.

<%- if devise_mapping.omniauthable? %>
  <%- resource_class.omniauth_providers.each do |provider| %>
    <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br />
  <% end -%>
<% end -%>

The above code snippet checks your Omniauth setup and auto-generates the required links.

Since this shared view is rendered on the sessions/new view, your users have the option to sign in using your configured providers.

Isn’t Devise a thing of beauty?

Adding Routes

The last piece of the puzzle is to set up the application routes.

Throughout this post, I have assumed that you have an on-site Devise implementation configured and fully functional.

So, there is a possibility you may already have the following route added to your routes file.

However, what you need to focus on is the additional controllers parameter which is used to specify the callbacks controller and will not be present in the route declaration that you have already added.

Rails.application.routes.draw do
                .
                .
                .
                .
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth' }
end

Once you have configured the routes, you can execute the following command to make sure the callback urls were set up correctly.

$ rails routes

Rails Routes

Voila! we are all set up to test our application.

Time to Socialize

We have successfully added third party login through Facebook, Github, Google, and Twitter to our application.

It is time to take it out for a test drive.

Recall that we are using the dotenv gem in our development environment so the command to execute our rails application changes slightly based on that since we also want to set the environment variables to be available to our application.

Execute the following command to start your rails application.

$ dotenv rails server

Rails Server Command

Browse to Devise’s user login page and you should see the text “Sign in with…” for each of the providers we set up.

Here is a screenshot of how it looks with Devise’s primitive set up.

Devise User Login Page

Go ahead and try signing in.

You will be taken to the third party provider’s webpage where you will be prompted to grant your application access to the user’s data.

Once you have done that, you will be taken back to your application, to the user’s homepage, with a flash message notifying you of successful sign in.

Here is a screenshot of when the Sign in with Facebook link is clicked through.

Facebook Permissions Page

Curtains

Adding a third party login option to your application is a nice touch and further enhances your application.

Though we have targeted four of the most famous of the lot, you are free to get your hands dirty and try the others available.

The Omniauth gem’s wiki has a comprehensive list of the strategies available and you should probably get to playing around with them.

I hope you found this tutorial interesting and knowledgeable. Until my next piece, happy coding!

Source:: scotch.io