Rails – Twitter and Facebook Authentications with Omniauth and Devise

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:

 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

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

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 :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


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


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.

 user = User.new

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

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)

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)
 if session[:omniauth]

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

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
 session[:omniauth] = nil unless @user.new_record?

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)

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:

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


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

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 ;)


  1. I get error
    NoMethodError in AuthenticationsController#create

    undefined method `find_by_provider_and_uid’ for Authentication:Class
    what could be the solutioN?

    1. find_by_provider_and_uid is an automatically defined method when you create the Authentication model with fields named ‘uid’ and ‘provider’. So if your field names are different, or if you didn’t run ‘bundle install’ command you may get this error. Can you try again ?

  2. Thank you !
    Your article helped me a lot.

    Now I need to allow iphone app to authenticate and use my REST API.
    Any idea how I could implement that ?

  3. Hi Orhan,

    First, thank you for making an up-to-date resource for creating an authentication system.

    I have a question about multiple vs single authentication. What is the difference? I don’t really understand the exact nature of it from your description, except that it can gather more information from the user.

    From the ‘single authentication’ from the railscast 235, you can use multiple logins. You can login with a username, a twitter, or any other provider. I used that to allow me to log in with twitter, a username, and a facebook. When someone registers with an email that is already registered with facebook, it will say ‘error email already exists’. That’s from Devise, not my writing. My authentication system is having a few problems, which is why I’m here to learn more. But the thing that confuses me is that it seems like railscast 235 is a multiple authentication solution. Can you clarify what the difference is? That way I can decide if I should rehaul my login system to something similar to yours, or if i should continue to hack away at mine until I figure out the few errors I’m currently getting.


  4. Thanks a lot, this was exactly what I needed.

    One thing, in this paragraph:
    “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.”

    sth is a typo?

  5. How does your code work without changing the form for the registration. If I just copy the registration form from devise and move it, they keep asking what resource and resource_name are. Arent these devise attributes?

  6. How does your code work without changing the form for the registration. If I just copy the registration form from devise and move it, they keep asking what resource and resource_name are. Arent these devise attributes? Thanks!

  7. Hi Orhan,

    I tried to apply your approach to a MongoId backend and it surfaced a bug in the code you have on github. I haven’t seen the SQL database with your version, but in MongoDB I saw a new user created with every twitter login. I think it happens because you are refering to provider and uid for the user instead of the user’s authenticatons in the authentications_controller:

    user = User.new
    user.provider = omni.provider
    user.uid = omni.uid

    I could be wrong. Even with some tweaks I couldn’t get it to store authentications properly so I went with a different approach.


  8. Great article. Is it possible to fetch twitter feed by adding just the twitter username to our rails app where we can just pass the authentication request for once?

  9. Thanks. I am trying to set up facebook. On facebook, I set up the site URL to be: https://localhost:3000.

    But right now, I get this error when I click the facebook link:

    “error”: {
    “message”: “Invalid redirect_uri: Given URL is not allowed by the Application configuration.”,
    “type”: “OAuthException”,
    “code”: 191

  10. Thanks a lot! it helps! But when I implement your practice, I found there is still one condition you didn’t think about, which is: there an existing user without twitter/facebook auth, but who is not logging currently, and he/she want to login with twitter/facebook auth.(I use “email” as unique identifier) I do a little modification of function def facebook in authentications_controller.rb :

    def facebook
    omni = request.env["omniauth.auth"]
    authentication = Authentication.find_by_provider_and_uid(omni['provider'], omni['uid'])

    if authentication # already have auth before.
    flash[:notice] = "Logged in Successfully"
    sign_in_and_redirect User.find(authentication.user_id)
    elsif current_user # current logging user but still don't have auth before.
    token = omni['credentials'].token
    token_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

    user = User.find_by_email(omni['extra']['raw_info'].email)
    if user.nil?
    user = User.new
    user.name = omni['extra']['raw_info'].name
    user.email = omni['extra']['raw_info'].email


    if user.save
    flash[:notice] = "Logged in."
    sign_in_and_redirect User.find(user.id)
    session[:omniauth] = omni.except('extra')
    redirect_to new_user_registration_path
    else # old user but not logging now
    token = omni['credentials'].token
    token_secret = ""

    user.authentications.create!(:provider => omni['provider'], :uid => omni['uid'], :token => token, :token_secret => token_secret)

    flash[:notice] = "Authentication successful."
    sign_in_and_redirect user

  11. Hi!
    I got this message when trying sing in with provider twitter.

    401 Unauthorized
    How to fix this?

  12. Incredibly helpful post, many thanks!!

    I’ve made one small adjustment to the Facebook action by adding one additional elsif condition. If the user has signed in previously and now they are signing in with Facebook, these two users should be linked if their email addresses match. Additionally, fill in some values to the user table if they haven’t been filled in previously.

    elsif User.find_by_email(omni[‘extra’][‘raw_info’].email)
    user = User.find_by_email(omni[‘extra’][‘raw_info’].email)
    user.first_name ||= omni[‘extra’][‘raw_info’].first_name
    user.last_name ||= omni[‘extra’][‘raw_info’].last_name
    user.username ||= omni[‘extra’][‘raw_info’].name
    user.location ||== omni[‘extra’][‘raw_info’].location
    token = omni[‘credentials’].token
    token_secret = “”
    user.authentications.create!(:provider => omni[‘provider’], :uid => omni[‘uid’], :token => token, :token_secret => token_secret)
    flash[:notice] = “Authentication successful.”
    sign_in_and_redirect user

  13. How do you solve the problem, when your user already registered with email on your server, then tries to login with twitter. It asks for a mail but this mail already exists so you cannot continue. It doesn’t associate your twitter account automatically to existing local account.

  14. is anyone else getting this: The action ‘failure’ could not be found for AuthenticationsController error ? even get it with the clone from git

  15. Orhan selamlar,
    Ben koolayla birlikte sadece omniauth facebook kullanıyorum. Benim siteyi tam açmadan önce facebookun kapatılma haberleri çıktı şimdi devise üzerinden facebook ve twitter omniauth kullanayım diyorum.
    Kullanıcı facebookla kayıt oldu diyelim, gittiler kapattılar facebook’u..
    sence bu durumda kullanıcı tekrar mı kayıt olmalı? facebook tokenleriyle falan devise üzerinden tekrar girebilir mi? böyle bir uygulama duydun mu?

    1. Selamlar Caner,
      Facebook’un kapatilmasi gibi bir durum soz konusu degil. Sadece yurt icinden erisime kapanabilir.
      Bu gibi bir durumda token’lar expire olmaz (sureleri gecmedigi surece). Erisim tekrar acildiginda kullanmaya devam edebilirsin.
      Token expire olmussa kullaniciyi facebook’a onlendirip yeni bir token alman gerekir. Bu durumun erisim engellemesiyle bir alakasi yok, erisim engellenmesede bunu goz onune almalisin.

  16. This is very interesting, You are a very skilled blogger.
    I’ve joined your feed and look forward to seeking more of your fantastic post.
    Also, I’ve shared your website in my social networks!

  17. if you can share this app source code….would help learn a lot when it runs on local system by inspecting it.

    thanks for the great effort on this.


  18. Awesome article. I read two previous tutorials and both didn’t work. This worked. I just needed to check if the User model was valid before saving it.

    valid = user.valid?
    errors = user.errors
    save_result = user.save

    Devise wants you to set a virtual column :password that it encrypts or the model will fail validations and not save.

  19. Are the records in the authorization table sessions that get created and destroyed, or are they permanently stored credentials to use to log in?

  20. You never mentioned creating a registration_controller.rb but it starts talking about it later on. And the code lacks context so it’s not clear where to put it.

    when you say registration controller, do you actually mean authentication controller?

    And in the routes, where you specify

    :controllers => { omniauth_callbacks: “authentications”, registrations: “registrations” }

    but what does registrations: “registration” do? why do we need it? what does it call? which file does it call? did we create it? if not, what’s the devise path for the existing file?

  21. For this code,

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

    should it be password.blank?

    why should a blank password be a criteria? if a password was previously typed in and is not blank, why should the password be no longer required? Seems like an exploit to bypass the password requirement.

  22. The guide starts using @user in the new devise registration view, and in the registration controller (which I believe is actually meant to be authentications controller right?)

    @user is not defined, and the code isn’t working as it should. I imagine it gets tricky because @user would be different for each of the 3 cases. But where it’s not working is the hardest case (the first case… which actually appears as the 3rd explanation in the article… why couldn’t the 3rd case just have been called the 1st case, and you explain it in the same order you mention the cases?)

    1. I still think it’s the best guide out there on the web right now, which is why I’m bothering to ask clarification questions. Thank you.

  23. In the routes you specify registrations: “registrations”

    but later in the guide you say the file is registration_controller.rb

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>