-
Notifications
You must be signed in to change notification settings - Fork 0
OmniAuth: Overview
Since version 1.2, Devise supports integration with OmniAuth. This wiki page will cover the basics to have this integration working using an OAuth provider as example.
Since version 1.5, Devise supports OmniAuth 1.0 forward which will be the version covered by this tutorial.
The first step then is to add an OmniAuth gem to your application. This can be done in our Gemfile:
gem 'omniauth-facebook'
Here we'll use Facebook as an example, but you are free to use whatever and as many OmniAuth gems as you'd like. Generally, the gem name is "omniauth-provider" where provider can be "facebook" or "twitter", for example. For a full list of these providers, please check OmniAuth's list of strategies.
Next up, you should add the columns "provider" and "uid" to your User model.
rails g migration AddColumnsToUsers provider uid
rake db:migrate
Next, you need to declare the provider in your config/initializers/devise.rb and require it (if it wasn't required automatically by bundler yet):
require "omniauth-facebook"
config.omniauth :facebook, "APP_ID", "APP_SECRET"
To alter the permissions requested, check that omniauth-* gem's README.
If for some reason Devise cannot load your strategy class, you can set it explicitly with the :strategy_class
option:
config.omniauth :facebook, "APP_ID", "APP_SECRET", :strategy_class => OmniAuth::Strategies::Facebook
And if you run into an OpenSSL error like this:
OpenSSL::SSL::SSLError (SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed):
Then you need to explicitly tell OmniAuth where to locate your ca_certificates file. Either use the certified gem or this method: (depending on the OS you are running on):
config.omniauth :facebook, "APP_ID", "APP_SECRET",
:client_options => {:ssl => {:ca_path => '/etc/ssl/certs'}}
If your app is running on Heroku (and you have been pulling your hair out for hours), the config section needs to look like this:
config.omniauth :facebook, "APP_ID", "APP_SECRET",
{:scope => 'email, offline_access', :client_options => {:ssl => {:ca_file => '/usr/lib/ssl/certs/ca-certificates.crt'}}}
On Engine Yard Cloud servers, the CA file is located at /etc/ssl/certs/ca-certificates.crt
.
On OS/X, for development only, it may be easiest just to disable certificate verification because the certificates are stored in the keychain, not the file system:
require "omniauth-facebook"
OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE if Rails.env.development?
config.omniauth :facebook, "APP_ID", "APP_SECRET"
A deeper discussion of this error can be found here: https://github.com/intridea/omniauth/issues/260
After configuring your strategy, you need to make your model (e.g. app/models/user.rb) omniauthable:
devise :omniauthable, :omniauth_providers => [:facebook]
Note: If you're running a rails server, you'll need to restart it to recognize the change in the Devise Initializer or adding :omniauthable to your User model will create an error.
Currently, Devise only allows you to make one model omniauthable. After making a model named User
omniauthable and if devise_for :users
was already added to your config/routes.rb, Devise will create the following url methods:
- user_omniauth_authorize_path(provider)
- user_omniauth_callback_path(provider)
Note that devise does not create *_url methods. While you will never use the callback helper above directly, you only need to add the first one to your layouts in order to provide facebook authentication:
<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>
Note: The symbol passed to the user_omniauth_authorize_path method matches the symbol of the provider passed to Devise's config block. Please check that omniauth-* gem's README to know which symbol you should pass.
By clicking on the above link, the user will be redirected to Facebook. (If this link doesn't exist, try restarting the server.) After inserting their credentials, they will be redirected back to your application's callback method. To implement a callback, the first step is to go back to our config/routes.rb file and tell Devise in which controller we will implement Omniauth callbacks:
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
Now we just add the file "app/controllers/users/omniauth_callbacks_controller.rb":
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
The callback should be implemented as an action with the same name as the provider. Here is an example action for our facebook provider that we could add to our controller:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
# You need to implement the method below in your model (e.g. app/models/user.rb)
@user = User.find_for_facebook_oauth(request.env["omniauth.auth"])
if @user.persisted?
sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
This action has a few aspects worth describing:
-
All information retrieved from Facebook by OmniAuth is available as a hash at
request.env["omniauth.auth"]
. Check the OmniAuth docs and each omniauth-* gem's README to know which information is being returned. -
In case a valid user is given from our model, we should sign it in. Notice we set a flash message using one of Devise's default messages, but that is up to you. Next, we sign the user in and redirect it. We pass the :event => :authentication to the sign_in_and_redirect method to force all authentication callbacks to be called.
-
In case the user is not persisted, we store the OmniAuth data in the session. Notice we store this data using "devise." as key namespace. This is useful because Devise removes all the data starting with "devise." from the session whenever a user signs in, so we get automatic session clean up. At the end, we redirect the user back to our registration form.
After the controller is defined, we need to implement the find_for_facebook_oauth
method in our model (e.g. app/models/user.rb):
def self.find_for_facebook_oauth(auth)
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.name = auth.info.name # assuming the user model has a name
user.image = auth.info.image # assuming the user model has an image
end
end
end
The method above simply tries to find an existing user by provider
and uid
or create one with a random password otherwise.
Notice that Devise's RegistrationsController by default calls "User.new_with_session" before building a resource. This means that, if we need to copy data from session whenever a user is initialized before sign up, we just need to implement new_with_session
in our model. Here is an example that copies the facebook email if available:
class User < ActiveRecord::Base
def self.new_with_session(params, session)
super.tap do |user|
if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
user.email = data["email"] if user.email.blank?
end
end
end
end
Finally, if you want to allow your users to cancel sign up with Facebook, you can redirect them to "cancel_user_registration_path". This will remove all session data starting with "devise." and the new_with_session
hook above will no longer be called.
config/routes.rb
devise_scope :user do
get 'sign_out', :to => 'devise/sessions#destroy', :as => :destroy_user_session
end
And that is all you need! After you get your integration working, it's time to write some tests:
https://github.com/intridea/omniauth/wiki/Integration-Testing
If you are using ONLY omniauth authentication, you need to define a route named new_user_session (if not defined, root will be used). Below is an example of such routes (you don't need to include it if you are also using database or other authentication with omniauth):
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
devise_scope :user do
get 'sign_in', :to => 'devise/sessions#new', :as => :new_user_session
get 'sign_out', :to => 'devise/sessions#destroy', :as => :destroy_user_session
end
In the example above, the sessions controller doesn't need to do anything special. For example, showing a link to the provider authentication will suffice.