authentication

I have been struggling with multiple authentications with FaceBook and Twitter + normal user registration process all required in one project. Since rails and apis’ are developing rapidly, almost all tutorials are outdated or not respond all of these requirements at the same time.

In that tutorial, your project will provide:

  • Normal user registration process with Devise
  • Registration and logins with Facebook or Twitter
  • And both at the same time

And for the future usage, the system will keep tokens and token_secrets so that your app will be able to tweet or post something with the authenticated social services.

For that project I mainly updated Ryan Bates’ (rbates on Twitter and ryanb on GitHub) following tutorials:

  • http://railscasts.com/episodes/235-devise-and-omniauth-revised
  • http://railscasts.com/episodes/235-omniauth-part-1
  • http://railscasts.com/episodes/236-omniauth-part-2

and couple of other articles which I’m going to link all of them at the end of the post. But you should thank Ryan Bates for all of them primarily.

So let’s begin.

The first thing that we need to do is, adding Devise, Omniauth, Facebook and Twitter to our Gemfile.

gem 'devise'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
gem 'twitter'
gem 'fb_graph'

And then we call

bundle install

command to apply our changes in Gemfile.

Now before we get into the code, I am going to explain the logic behind the authentications:

Devise prodives us a great User model with the following schema:

</pre>
1
 create_table "users", :force => true do |t|
 t.string "email", :default => "", :null => false
 t.string "encrypted_password", :default => "", :null => false
 t.string "reset_password_token"
 t.datetime "reset_password_sent_at"
 t.datetime "remember_created_at"
 t.integer "sign_in_count", :default => 0
 t.datetime "current_sign_in_at"
 t.datetime "last_sign_in_at"
 t.string "current_sign_in_ip"
 t.string "last_sign_in_ip"
 t.datetime "created_at", :null => false
 t.datetime "updated_at", :null => false
 end

However as you see in that schema, there is no signal that we can attach Facebook's or Twitter's authentication information.

Since we want to provide multiple authentications for each user, but a user may not have any authentications also we should have separate table which keeps track of authentications.

 create_table "authentications", :force => true do |t|
 t.string "user_id"
 t.string "provider"
 t.string "uid"
 t.string "token"
 t.string "token_secret"
 t.datetime "created_at", :null => false
 t.datetime "updated_at", :null => false
 end

So in this schema, user_id is our foreign key so that if any user authenticates him/herself with either Facebook or Twitter will have a record in that database. Token and token_secret fields are really important for us. In the future usage, we will be able to authenticate our application for Facebook and Twitter to post any update, or and feed from app to the authenticated social media.

The other thing is, if you only want to provide single authentication with any of these providers, you don't need to create a separate table and work on this stuff. The more efficient solution is, adding provider, uid, token and token_secret fields into the User model.

So after we have this infrastructure, we will direct rails to fulfill these terms.

We should tell Devise that we are going to use Facebook and Twitter authentications. Hence in /config/initializiers/devise.rb file, there is a section starts with "# ==> OmniAuth". Add:

config.omniauth :twitter, ENV["TWITTER_CONSUMER_KEY"], ENV["TWITTER_CONSUMER_SECRET"]
config.omniauth :facebook, 'APP_ID', 'APP_SECRET' 

Let's first create User model from devise with

devise generate Users

command. After you execute this command, you will have a file called User.rb in your views folder which is nicely coded by Devise.

To have the Authentication models and controllers, run this command:

rails g nifty:scaffold authentication user_id:integer provider:string uid:string index create destroy

You may encounter an error because you don't have nifty:scaffold in your project. To solve this issue, open your Gemfile and add

gem 'nifty-generators'

line and run

bundle install

code.

So we have all models now but we should make them connected with a few lines adding onto them.

Since every single User may have many Authentication. We should add

has_many :authentications

line into the user.rbfile. And

 belongs_to :user 

line to the authentication.rbfile.

And since we're going to get help from Devise, we should add :omniauthable keyword into the user model:

devise :database_authenticatable, :registerable, :omniauthable,
:recoverable, :rememberable, :trackable, :validatable

This provides really useful functions and strong support in the background and now we'll be able to add our login links.

Let's create a really really really simple, ugly login page with the following code:

 <% if user_signed_in? %>
 Logged in as <strong><%= current_user.email %></strong>.
 <%= link_to 'Edit profile', edit_user_registration_path %> |
 <%= link_to 'Authentications', authentications_path %> |
 <%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% else %>
 <%= link_to "Sign up", new_user_registration_path %> |
 <%= link_to "Login", new_user_session_path %>
 <li><%= link_to "Twitter", user_omniauth_authorize_path(:twitter) %></li>
 <li><%= link_to "Facebook", user_omniauth_authorize_path(:facebook) %></li>
<% end %>

I created a file called home.html.erb in views/authentications folder, added the code above and modified my routes.rb with the following line:

root to: 'authentications#home'

And of course in my authentications_controller.rb I added home action which does nothing:

 def home

end

OK. Now it is time to handle to authentication part.

When we click a "Twitter" or "Facebook" button in our home page it actually calls the user_omniauth_authorize_path(:provider) method. So in that case we need to do sth, because now we have our own authentications controller since we are going to provide multiple authentications.

To redirect this call into our controller we need to change routes.rb file

 devise_for :users, path_names: {sign_in: "login", sign_out: "logout"},
 controllers: {omniauth_callbacks: "authentications", registrations: "registrations"}

With this line, we are telling to our app that instead of devise to handle our omniauth_callbacks, our authentications controller would handle these calls.

So now it's time to click "Twitter" button in the home page ! When we click that button, after we authenticate our app from Twitter, we'll get the following error:

Unknown action

The action 'twitter' could not be found for AuthenticationsController

Which means that, when we click that button, Devise redirects us to the our Authentication controller's twitter action. But we don't have any, then let's create.

To test and see what is the incoming hash from twitter, first write the following code into the authentications_conttoller.rb and click "Twitter" link again.

 def twitter
 raise omni = request.env["omniauth.auth"].to_yaml

What did you see? This output includes lots of information about our Twitter authentication and we only need to extract a few of them. Now, firstly we need to figure out how many possibilities for twitter authentication.

  1. There exists a user, who is not authenticated his/her twitter account yet.
  2. There maybe a person, who is going to register to our website by authenticating the Twitter.
  3. There maybe a user, who is already authenticated before.

So 3rd one is the easiest one, because we know that in Authentications table, there exists a record for a user, who's provider is twitter,  and uid is omni['uid']. Where omni variable is the hash which is retrieved from the following line:

 omni = request.env["omniauth.auth"]

So to handle the easiest case, we can search that whether there exists a record with the following inputs or not.

authentication = Authentication.find_by_provider_and_uid(omni['provider'], omni['uid'])

if there is an any Authentication record, the variable authentication will be initialized with that so that we can process the authentication:

 if authentication
 flash[:notice] = "Logged in Successfully"
 sign_in_and_redirect User.find(authentication.user_id)

Pretty easy ha ?

Let's look at for the 2nd case, where there exists a logged-in user but there is no twitter Authentication record for that user.

In that case, we would have a variable called "current_user" since the user is currently logged in. But user maybe logged in with Facebook or normal log in process. Therefore we need to extract the necessary information from twitter hash, create the Authentication record and redirect user.

So, if there is a logged in user, in our current web site, the variable "current_user" will return TRUE in if statement.

 elsif current_user
 token = omni['credentials'].token
 token_secret = omni['credentials'].secret

current_user.authentications.create!(:provider => omni['provider'], :uid => omni['uid'], :token => token, :token_secret => token_secret)
 flash[:notice] = "Authentication successful."
 sign_in_and_redirect current_user

So basically, we're extracting the token and token_secret information from the Omniauth hash and create an Authentication record with the following inputs, and redirecting the user.

Aand the last and the most tricky case, if there is a NEW user wants to register by authenticating his/her twitter account.

So let's think the use case scenario first before we start writing any lines of code.

We don't have any information about new-coming user, there is any record in neither Users nor Authentications table. Therefore, we need to create both User and Authentication records. And there are couple of problems in here, the first one is, since the user will sign up with twitter account, there won't be any password. The second, twitter api will not provide user's e-mail address, therefore we need to ask the user to fill registration form again with our own registration controller. That is the reason we have our own registration controller rather than Devise's ! Here's the all code for that, then we'll focus on line by line.

 else
 user = User.new
 user.apply_omniauth(omni)

if user.save
 flash[:notice] = "Logged in."
 sign_in_and_redirect User.find(user.id)
 else
 session[:omniauth] = omni.except('extra')
 redirect_to new_user_registration_path
 end
 end

As you can see, we have a function called apply_omniauth called with user instance, therefore we need to create a method called apply_omniauth in user.rb file.

def apply_omniauth(omni)
 authentications.build(:provider => omni['provider'],
 :uid => omni['uid'],
 :token => omni['credentials'].token,
 :token_secret => omni['credentials'].secret)
 end

So this method isn't doing something fancy, I just separated it for the sake of simplicity. Firstly we're creating a User object, then we're building an Authentication object, but you should know that, since we use "build" method, there won't be any Authentication record until the User record is successfully saved. If you try to use create instead of build, then you'll get error because then you're trying to create an object which's super class is not successfully created yet !

OK. Now, we know that "user.save" will return FALSE, because we haven't initialized user's email since Twitter doesn't provide to us. Therefore we need to redirect a user to the new registration page. However we also not to lose the hash information from omniauth, so that we add these hash into our session with:

session[:omniauth] = omni.except('extra')

.except helps us to get rid of a lot of unnecessary information from hash, so that our session hash will not get rejected.

Now we need our registration controller to handle this request. But you know, Devise already has the new registration page as you remember, but do you know where they are ? They're not actually visible at the beginning, but to copy all of these files and modify them with respect to your needs, you need to run this command:

rails g devise:views

This command copies all files from Devise engine into views/devise directory. And in views/devise/registrations directory, has two files called edit.html.erb and new.html.erb . You should move these files from there to views/registrations directory, to use by ourselves.

And to handle registrations if the session variable exists with the help of Devise, here's the code for registration_controller.rb

 def build_resource(*args)
 super
 if session[:omniauth]
 @user.apply_omniauth(session[:omniauth])
 @user.valid?
 end
 end

But unfortunately, it is still not enough to have perfect login system, because if you try to register now, you will be redirected to the new registration page with the session, and you will see these two errors:

  • Email can't be blank
  • Password can't be blank

Which is not bad actually, we only want to force our user to enter an e-mail address, not the password. So to handle this add this method into user.rb file

 def password_required?
 (authentications.empty? || !password.blank?) && super
 end

Now we created a function that returns FALSE if there is an authentication. Let's use it !

The errors were in views, therefore our aim is to use this method in new.html.erb in /views/registrations directory.

 <h2>Sign up</h2>

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
 <%= devise_error_messages! %>

<div><%= f.label :email %><br />
 <%= f.email_field :email %></div>

<% if @user.password_required? %>
 <div><%= f.label :password %><br />
 <%= f.password_field :password %></div>

<div><%= f.label :password_confirmation %><br />
 <%= f.password_field :password_confirmation %></div>
 <% end %>

<div><%= f.submit "Sign up" %></div>
<% end %>

<%= render "devise/shared/links" %>

There is one last thing that we need to do in Registrations Controller. Which is deleting the omniauth hash from session. We can easily handle this issue by overriding the create action

 def create
 super
 session[:omniauth] = nil unless @user.new_record?
 end

Yes it is DONE, except one small problem :) When a user tries to edit his/her settings, there is a current password field which needs to be filled in. However there is no password for these users, therefore we need to override the update_with_password method in user.rb:

def update_with_password(params, *options)
 if encrypted_password.blank?
 update_attributes(params, *options)
 else
 super
 end
end

OK. Twitter is done. What do you think about Facebook, or the others ? What do you think we need to change to implement Facebook ? It is actually nothing at all ! Because we already have a working system which is capable of handling many authentications. We just need to know the  structure of the omniauth hash, maybe Facebook will provide user's e-mail ha ? And yes ! It is easier than Twitter, because Facebook already provide all the necessary information. The only thing is, you need to know how to ask for Facebook :) And here's how:

In your devise.rb file after you add your app id and app secret, you also need to add scope:

config.omniauth :facebook, 'APP_ID', 'APP_SECRET', {:scope => 'publish_stream, email'}

And this is the permission for, posting on behalf of the user and reaching the user's email.

And in Facebook case, we need to have an action called Facebook in authentications_controller.rb, and the only difference from the twitter action is the new user registration case:

else
 user = User.new
 user.email = omni['extra']['raw_info'].email

user.apply_omniauth(omni)

if user.save
 flash[:notice] = "Logged in."
 sign_in_and_redirect User.find(user.id)
 else
 session[:omniauth] = omni.except('extra')
 redirect_to new_user_registration_path
 end
 end

Yes, it's DONE. Congratulations ! I intentionally separated these two actions so that you can see how easy to implement any service just within a seconds !

One week ago I didn't know anything about Ruby on Rails,  but I had a competition to finish in three days provided by RailsArena.
It is a simple web applications that allows you to add friends and lend or borrow moneys to them. You will be able to keep track of the transactions and post them to a social media. You can reach the app from here: http://moltosoldi.herokuapp.com/ and source code from my github.

And the hardest part of my project was handling the multiple authentications and posting or tweeting something to the social media. Because the APIs' are changing so rapidly and most of the tutorials were for the old versions of these gems or they weren't what I was looking for.

So I gathered all these tutorials, and finally created successful authentication model. Then I want to share this information with everyone.

And finally, if you want to see the source code, you can reach it from my  github ;) I purposely didn't add anything unnecessary than the authentication, so don't be shocked when you see the bad design :) since every web site has it's own style, you can only use the functionality easily ;)