:::: MENU ::::

Using multiple omniauth providers with omniauth-identity on the main user model

Yesterday night I was hacking on a small side project with Rails 3.2 and omniauth. I will be offering multiple OAuth connections to my users but also want a simple email and password signup/login, so I decided to go with omnniauth and omniauth-identity.

Most tutorials on omniauth-identity use a separate Identity model, but I already have a User model to take care of a user’s details, and an Authentication model, to handle all the different OAuth connections, so I don’t want a new Identity model.

So how do we make it work?

Let’s first add the omniauth initializer in config/initializers. Add as many providers as you want.

As you can see we’re telling omniauth-identity that we don’t want its Identity model but that we’re instead going to use our User model. We’re also going to send the user to our own controller in case the registration fails (the lamba is a neat catch by Ryan Bates).

Now, let’s make the User model inherit from Omniauth Identity. Before doing that make sure you have a password_digest string field in the users table:

rails g migration add_password_digest_to_users password_digest:string

# == Schema Information
#
# Table name: users
#
#  id                  :integer          not null, primary key
#  name                :string(255)
#  email               :string(255)
#  created_at          :datetime         not null
#  updated_at          :datetime         not null
#  password_digest     :string(255)

class User < OmniAuth::Identity::Models::ActiveRecord
  attr_accessible :email, :name, :password, :password_confirmation

  has_many :authentications
  
  email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

  validates :email, :presence   => true,
            :format     => { :with => email_regex },
            :uniqueness => { :case_sensitive => false }
  
  def self.create_with_omniauth(auth)
    # you should handle here different providers data
    # eg. case auth['provider'] ..
    create(name: auth['info']['name'])
    # IMPORTANT: when you're creating a user from a strategy that
    # is not identity, you need to set a password, otherwise it will fail
    # I use: user.password = rand(36**10).to_s(36)
  end
end

A user has many authentications, where we keep track of all the services he is using:

Now let’s create some helper methods for our login in application_controller, nothing fancy:

To have auth work, we have to set up the routes, again nothing special here:

So to have a user signup with identity, we’re going to send him to /signup and use our users controller’s new action:

Here we are creating a new user, but we’re also checking if there is a user stored in the omniauth.identify env variable, since it will store there failed registration attempts. Pretty cool.

Ok, so what are we displaying? A form that posts to /auth/identity/register with all the info required. If you use simple_form like I do, you have to use input_html and set the same values as the ones we added in the initializer in the fields hash.

So what happens when we send the POST? Well, here is the cool part of all of this.

  1. Omniauth-identiy creates a new User for us in automatic, with all the right data.
  2. Omniauth creates a new Authentication for us, with provider set as identity and uid set as the user_id our application is returning.
  3. We then just have to associate the authentication to the user and we’re done.

Let’s see what our sessions_controller does. Comments are pretty exhaustive here:

And there it is. Omniauth with multiple providers and omniauth-identity on the main user model. I will be adding remember-me, activation emails and such in the future but this should cover the basics and help you understand what omniauth is doing in the background.

Useful articles I used to understand and served as the basis for all of this:


  • Ian Drysdale

    Stefano, firstly thanks for a great post on this topic. I’ve followed your code, as far as I can tell, and can successful sign a new user up (that also creates an authentication), however, if I log out and try to log in again it falls over.

    As far as I understand, it seems that auth = request.env['omniauth.auth'] is nil. Should that be the case?

    Happy to up the code somewhere if it helps diagnose where I might be going wrong?

    • http://bernardi.me/ Stefano Bernardi

      Hey Ian,
      where are you sending the login info?

      The form on sessions#new should be:
      form_tag “/auth/identity/callback”
      Let me know if that works.

      • Ian Drysdale

        Ah hah – ok I’ve added that, but now I just get forwarded to the root, having not been logged in. The development log is here: https://gist.github.com/3875412

        • http://bernardi.me/ Stefano Bernardi

          You are clearly entering wrong credentials, or sending wrong name inputs.
          Started POST “/auth/identity/callback” for 127.0.0.1 at 2012-10-11 21:50:55 +0100

          (identity) Callback phase initiated.
          [1m [36mUser Load (0.1ms) [0m [1mSELECT “users”.* FROM “users”
          WHERE “users”.”email” IS NULL LIMIT 1 [0m
          (identity) Authentication failure! invalid_credentials encountered.

          Started GET “/auth/failure?message=invalid_credentials&strategy=identity” for 127.0.0.1 at 2012-10-11 21:50:55 +0100

          This is what the form should look like:

          -Ste

          • Ian Drysdale

            Ah – that’s it, :auth_key, not :email – well spotted.

            For my next challenge now, I want to consider how, having created a user using identity, they could link to a facebook account. Any pointers on good resources I should be reading would be greatly appreciated.

            Thanks again!

          • http://bernardi.me/ Stefano Bernardi

            Well that’s the goal of all this post. If you used my code, this will be automatically handled.

            Just add the provider in omniauth.rb and then send the user to /auth/facebook, when he comes back he will still be logged in and a new Authentication tied to his account will have been created.

            The create method of sessions_controller will take care of everything for you.

          • Ian Drysdale

            Hi Stefano, I just wanted to say I managed to get this all working as you described – so thank you – an incredibly useful post.

            One thing I’ve noticed is that the Session Controller is starting to get a little complicated with logic as the number of potential authentications starts to grow, and it feels like the wrong place for this logic to live long term. I was wondering if you’d any suggestions or pointers for how this be refactored that I could start playing with?

          • http://bernardi.me/ Stefano Bernardi

            I agree, I moved most of the stuff to the authentication model.

  • CameronPitt

    I’ve followed your instructions near exactly but I keep getting “unknown attribute: password”, any idea on how to solve this. My googling has yet to render me a solution.

    • http://bernardi.me/ Stefano Bernardi

      would need way more info to help you :)

  • http://www.facebook.com/alessandro.vitali.1485 Alessandro Vitali

    Nice tutorial
    I just want to improve it allowing a guest user in order to allow anybody to try my app and then choose to sign up. Any ideas?

  • http://twitter.com/daniel_friis Daniel Friis

    Excellent post! I was wondering how you would go about allowing the user to set a password through the oauth user-creation, instead of setting it randomly?

    • http://bernardi.me/ Stefano Bernardi

      Hey Daniel,

      instead of setting up a random password you would just send the user to an “edit” page where they can set their password and then create the user with that.

      This could either happen before you persist the user to the db, or after and treat it as an edit.

  • bunnahabhain13

    Has anyone got this working using simple_form with validates_acceptance_of for terms? I have got everything working the way that I want it to, but realized that it does not matter whether the “terms” checkbox is checked or not. The new user record is still created.

    I was not using simple_form prior to this, but validates_acceptance_of was working just fine.

    p.s. Stefano, thank you so much for writing this up. It is exactly what I was looking for.

  • Forrest

    Killer post! Thanks for sharing, exactly what I was looking for.

  • http://chandankumar.com/ Chandan Kumar

    In case someone is interested, I have integrated this for Sinatra and source is put up on Github. https://github.com/ch4nd4n/sinatra_omniauth_demo