Setting up Devise with Twitter and Facebook and other Omniauth schemes without email addresses
Connect connect connect
- tags
- rails
- oauth
- happy_seed
- ruby
Contents
Adding social login to your sites really makes it easier to get users onboard. Devise is great to help get an authentication system up and running, but there are a few tricky things to get right. The first challenge is that you don’t always get the user’s email address when the first connect. The second challenge is that we want to request the minimum permissions first so that the user is more likely to sign up, and gradually ask more as the time arises.
This post is going to go through the strategy that happy_seed uses to support these use cases. The easiest way to get started is to use seed to get things up and running, but we’ll walk through how to do it all in detail below.
Install devise and omniauth
Let’s install a few gems. We’ll go through how to install twitter, facebook, and google.
Gemfile
:
|
|
Now we need to run the devise
generators. I like to copy over the views so that I can fix them up to look like the rest of my app. If you are using seed, these views will be generated with HAML
and the bootstrap helpers
gem.
First install devise:
|
|
devise:install
copies over config/initializers/devise.rb
and a localized message file. We will configure devise here. Follow the outputed instuctions to setup flashes and mailer configs.
Now create a model, call it user:
|
|
This creates a User
model and configures devise routes to use it. We will edit both of these.
Finally, copy over the views so you can style them as you need:
|
|
Make sure regular login in works
Lets create a basic controller to see if our login works:
|
|
Edit routes.rb
:
|
|
And change your WeclomeController
to require user authentication:
|
|
Now if you go to http://localhost:3000/ you should get redirected to a login page. If you create a user you should be able to then see the protected page.
Let’s add a logout button to the index page for testing: app/views/welcome/index.html.erb
:
<%= link_to "Signout", destroy_user_session_path, method: :delete %>
Now we can go back and forth. The method: :delete
part is something that I often forget about.
Configure Omniauth
We need to tell devise and omniauth how to talk to the various outside services. The first thing you’ll need to do is configure those services and collect their app ids and app secrets. Then you put that information inside of config/initializers/devise.rb
:
|
|
Since we are building nice 12 Factor Apps we pull the config from the environment. seed uses the dotenv gem to keep track of these things in a .env
file, and when you deploy to heroku you will use heroku config variables.
We also pass in scope
s to a few strategies, which is where we can configure omniauth to request specific permissions. Sometimes you need to enable them on the remote side before you can request things (e.g. google, twitter) so make sure that things are setup there.
We’ll go into how to dynamically set that scope later on.
Tell Devise about omniauthable
Open up app/models/user.rb
and add :omniauthable
to your devise
line and remove :validatable
:
|
|
Now you should see that there are a list of connect with our services when you go to your sign in or login pages.
Create a FormUser to handle validations
Not all services return email addresses, and by default the devise validations expect them. Let’s move those validations out of the base User
class into a FormUser
class.
- Remove
:validatable
fromapp/models/user.rb
(which you’ve done above) - Tell devise to use our new model.
- Create the forms_user.rb class.
Inside of config/routes.rb
:
|
|
And app/models/form_user.rb
should look like:
|
|
The class_name
inside of the devise config will tell it to use this class for building forms, and we have the validations on this class so our error messages will work on the site, but we’ll be able to save objects without it.
Create Identity model to store access_keys and metadata
Now we are ready to plug in oauth authentications. The flow is:
- User requests
/users/auth/:provider
, where provider one of the strategies that you defined above. - Omniauth does magic and directs the user to the remote service.
- The user grants us access and is redirected to the callback path.
- The OmniauthCallbacks controller is called on our application with the relavent info.
We will use this info to create the user. We are also going to store it to be able to access the service on behalf of the user, and we’ll need to store the access_token
in order to do so.
Google is slightly more complicated and we’ll need to store a refresh_token
as well.
Lets create that model now:
|
|
And flesh out app/models/identity.rb
|
|
Now we need to tell devise to use this model.
Create OmniauthCallbacksController to pull in data
We’re going to build one method to handle the different authentication callbacks, called generic_callback
. The logic of this controller is:
- Find or create an
Identity
object for the incoming oauth data. Update it with the latest info. - If there is no user associated with the Identity, associate it with the current_user.
- If there is no current_user, create a new User object.
- If the User object doesn’t have an email address set yet, but we do have one from the remote service, set the email address to that.
- Log the user in and let the continue on their way.
First we need to tell devise about our controller in routes.rb
|
|
Create app/controllers/omniauth_callback_controller.rb
:
|
|
Override RegistrationsController to handle adding email address and password
We want to let the user add an email address if they haven’t already, and also let them set a password if they haven’t already set one. (Otherwise it requires the user to enter in current_password
.) Lets first tell devise about our new controller:
|
|
And then create that controller:
|
|
And now we should be good! Give it a go and see how it looks!
Adding methods to User to get to the clients
This goes into app/models/user.rb
:
|
|
Now you can access a configured API client using things like current_user.twitter
.
Passing dynamic scopes to omniauth
In the current scheme above, you have to hard code the scopes that you want to request for the user which doesn’t always work. It would be better to only request write access if the user really needs to have it, and by default only get read-only access. In order to do this we can leverage the Omniauth
setup property. Inside of devise.rb
add setup: true
to each of the services you want to be able to upgrade.
|
|
Let’s add a few routes in routes.rb
that we can have the user link to:
|
|
The first route is something that we’ll have the user link to, using user_omniauth_upgrade_path( :google_oauth2 )
for example. The second setup
route is what omniauth will call internally that we can use to change the scope
parameter. These go into omniauth_callbacks_controller.rb
.
The first upgrade
method looks at the provider and sets a flash
variable for the additional access. In this case, we are asking for the https://www.googleapis.com/auth/admin.directory.user
also.
def upgrade
scope = nil
if params[:provider] == "google_oauth2"
scope = "email,profile,offline,https://www.googleapis.com/auth/admin.directory.user"
end
redirect_to user_omniauth_authorize_path( params[:provider] ), flash: { scope: scope }
end
Then it directs the user back to the normal flow.
When you specify setup: true
inside of the omniauth configuration, there is magic that calls the setup_path
by default, and this is the method where we change the scope from the default in the strategy:
|
|
Now in your views, you can do
<%= link_to "Upgrade Access", user_omniauth_upgrade_path( :google_oauth2 ) %>
And the user will go through the oauth flow again requesting the additional access.
Conclusion
Making all of this work is possible, but there are a lot of fiddly little bits to make it work. Both devise and all of the many omniauth strategies out there make it easy to add this functionality to your application.
Check out seed to quickly create an application will all of this stuff done for you!
Previously
Next