Wednesday, 31 May, 2017 UTC


Summary

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 <your-application-base-url>/<devise-entity>/auth/<provider>/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.
Accept the Facebook developer agreement(in the modal dialog) by turning the switch to YES and clicking the Register button.
Click the Create App ID button that shows up in the same modal dialog.
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.
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.
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.
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.
Once you have logged in, click your account avatar at the top-right and choose Settings from the drop-down menu.
On the Settings page, choose Developer settings > OAuth applications from the left menu.
Click the Register a new application button.
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.
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.
On the Google developer console, choose Credentials from the left menu.
Click the Create credentials button and choose OAuth client ID from the menu that pops up.
For your Application type, choose Web application.
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.
Once your application is created, you will be shown the client ID, and client secret in a modal dialog.
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.
On the Twitter apps page, click the Create New App button.
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.
Enter a mock url in the Privacy Policy URL, and Terms of Service URL field and click the Update Settings button.
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.
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
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 <facebook-app-id>, and <facebook-app-secret> 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
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
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
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
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
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
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.
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.
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!