Skip to content
gguerini edited this page Jul 7, 2012 · 89 revisions

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.

Facebook example

The first step then is to add OmniAuth OAuth to our application, this can be done in our Gemfile:

gem "omniauth-facebook"

As of 1.0.0, Omniauth doesn't contain providers strategies anymore. So you should add the strategies as gems on your Gemfile. Generally, the gem name is "omniauth-#{provider}" where provider can be :facebook, :twitter or any other provider. For a full list, please check Omniauth wiki.

Add columns "provider" and "uid" to your User model.

rails g migration AddColumnsToUsers provider:string uid:string
rake db:migrate

Then, if you are using "attr_accessible" in your User Model, remember to add :provider and :uid.

attr_accessible :provider, :uid

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, add the :scope option. See omniauth-facebook wiki.

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. This can be done with the following (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.

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

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 method. 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 authorization:

<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>

If you have a "catch all" route (route globbing):

Omniauth calls through to the app for dynamic configuration settings, so you will need to define a route for /auth/:provider that results in a 404 so Omniauth knows how to proceed if you have a "catch all" route. The reasoning behind this can be found here. Inside your devise_scope :user routes block, add this:

devise_scope :user do
  get '/users/auth/:provider' => 'users/omniauth_callbacks#passthru'
end

And then add this method inside of the Omniauth Callbacks Controller (details follow):

def passthru
  render :file => "#{Rails.root}/public/404.html", :status => 404, :layout => false
  # Or alternatively,
  # raise ActionController::RoutingError.new('Not Found')
end

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"], current_user)

    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
      sign_in_and_redirect @user, :event => :authentication
    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:

  1. All information retrieved from Facebook by OmniAuth is available as a hash at request.env["omniauth.auth"]. Check omniauth docs for more information.

  2. 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.

  3. 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, signed_in_resource=nil)
  user = User.where(:provider => auth.provider, :uid => auth.uid).first
  unless user
    user = User.create(name:auth.extra.raw_info.name,
                         provider:auth.provider,
                         uid:auth.uid,
                         email:auth.info.email,
                         password:Devise.friendly_token[0,20]
                         )
  end
  user
end

The method above simply tries to find an existing user by e-mail or create one with a random password otherwise. Note that this is simply an example. Your application must take precautions if using User.find_by_email to link an existing User with a Facebook account.

Notice that Devise 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.

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

Using OmniAuth without other authentications

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 => 'users/sessions#new', :as => :new_user_session
  get 'sign_out', :to => 'users/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.

Google OAuth2 example

There are two main advantages of using OAuth2 instead of OpenID. First, this is the new and simplified authorization protocol for all Google APIs. Second, when you create an account to have access to the API, you can customize it with your logo and more information about your company or site.

First, you need to get an API key at https://code.google.com/apis/console/.

You also need to update the Redirect's URIs at the API Access. If you are using your localhost to test, use the following URI:

http://localhost:3000/users/auth/google_oauth2/callback

Note: This address can be different from your development machine. Don't forget to edit this URI when your application go live.

The next step is to add Google OAuth2 gem to the application. This can be done in the Gemfile.

gem 'omniauth-google-oauth2'

Don't forget to run bundle install.

Next, you need to declare the provider in your config/initializers/devise.rb and require it:

require "omniauth-google-oauth2"
config.omniauth :google_oauth2, "APP_ID", "APP_SECRET", { access_type: "offline", approval_prompt: "" }

Note: If you want to be prompted for permission every time, set approval_prompt: "force", otherwise leave it empty.

To alter the permissions requested, add the :scope option. See Using OAuth 2.0 for Login.

After configuring the strategy, you need to make your model (e.g. app/models/user.rb) omniauthable:

devise :omniauthable

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

Now you can add the helper to your views.

<%= link_to "Sign in with Google", user_omniauth_authorize_path(:google_oauth2) %>

By clicking on the above link, the user will be redirected to Google. (If this link doesn't exist, try restarting the server.) After inserting their credentials and approving the permission requested, 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 Google OAuth2 provider that we could add to our controller:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
	def google_oauth2
	    # You need to implement the method below in your model (e.g. app/models/user.rb)
	    @user = User.find_for_google_oauth2(request.env["omniauth.auth"], current_user)

	    if @user.persisted?
	      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Google"
	      sign_in_and_redirect @user, :event => :authentication
	    else
	      session["devise.google_data"] = request.env["omniauth.auth"]
	      redirect_to new_user_registration_url
	    end
	end
end

After the controller is defined, we need to implement the find_for_google_oauth2 method in our model (e.g. app/models/user.rb):

def self.find_for_google_oauth2(access_token, signed_in_resource=nil)
    data = access_token.info
    user = User.where(:email => data["email"]).first

    unless user
        user = User.create(name: data["name"],
	    		   email: data["email"],
	    		   password: Devise.friendly_token[0,20]
	    		  )
    end
    user
end

The method above simply tries to find an existing user by e-mail or create one with a random password otherwise. Note that this is simply an example. Your application may need to take other precautions.

And that is all you need!

OpenID and Google examples

Add OmniAuth OpenID to your Gemfile:

gem 'omniauth-openid'

In order to use following features, you have to require openid store in Devise initializer. For example:

require 'openid/store/filesystem'

OpenID

Add following code to Devise initializer

config.omniauth :open_id, :store => OpenID::Store::Filesystem.new('/tmp'), :require => 'omniauth-openid'

Use this in your view:

link_to "sign in with yahoo", user_omniauth_authorize_path(:open_id, :openid_url => "http://yahoo.com")

Google

Add following code to Devise initializer

config.omniauth :open_id, :store => OpenID::Store::Filesystem.new('/tmp'), :name => 'google', :identifier => 'https://www.google.com/accounts/o8/id', :require => 'omniauth-openid'

Add the following definition in models/user.rb

devise :omniauthable #followed by anything else you need

def self.find_for_open_id(access_token, signed_in_resource=nil)
  data = access_token.info
  if user = User.where(:email => data["email"]).first
    user
  else
    User.create!(:email => data["email"], :password => Devise.friendly_token[0,20])
  end
end

Next create a file in app/controllers/users/omniauth_callbacks_controller.rb and give it the following content:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_filter :verify_authenticity_token, :only => [:google]

  def google
    @user = User.find_for_open_id(request.env["omniauth.auth"], current_user)

    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Google"
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.google_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

Skip :verify_authenticity_token to make sure your session doesn't get reset when the token verification fails. The OpenID server never sends it.

You'll also need to change the routing in routes.rb to:

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

Finally use this in your view:

link_to "sign in with google", user_omniauth_authorize_path(:google)

This code allows all users of Google Mail and Google Apps (i.e. with a domain name other than gmail.com, like [email protected]) to sign in. It is not restricted to a particular domain like a Google Apps solution would be.

Google Apps

Add OmniAuth Google Apps to your Gemfile:

gem 'omniauth-google-apps'

Devise initializer:

config.omniauth :google_apps, :store => OpenID::Store::Filesystem.new('/tmp'), :domain => 'gmail.com'

User Model:

def self.find_for_googleapps_oauth(access_token, signed_in_resource=nil)
  data = access_token['info']
  
  if user = User.where(:email => data['email']).first 
    return user
  else #create a user with stub pwd
    User.create!(:email => data['email'], :password => Devise.friendly_token[0,20])
  end
end

def self.new_with_session(params, session)
  super.tap do |user|
    if data = session['devise.googleapps_data'] && session['devise.googleapps_data']['user_info']
      user.email = data['email']
    end
  end
end

View:

link_to 'GMail', user_omniauth_authorize_path(:google_apps)
Clone this wiki locally