Skip to content

Mixing logging in with OmniAuth with regular OmniAuth usage

Pascal Lindelauf edited this page Jan 9, 2020 · 1 revision

The OmniAuth: overview states at the beginning:

Remember that config.omniauth adds omniauth provider middleware to your application. This means you should not add this provider middleware again in config/initializers/omniauth.rb as they'll clash with each other and result in always-failing authentication.

This raises the question: what if you want to use OmniAuth for more than just logging in with Devise, like accessing an API though OAuth? The answer is to not use Devise's OmniAuth helpers. These add just a very thin layer of extra functionality which you can easily take care of yourself with out-of-the-box OmniAuth functionality. This article shows you how.

What we don't need

We don't need any of the following:

  1. :omniauthable in the model
  2. config.omniauth in config/initializers/devise.rb
  3. devise_for :users, only: :omniauth_callbacks, controllers: { omniauth_callbacks: 'omniauth_callbacks' } in routes.rb

How it's done

We start with the assumption that you have already created an OAuth2 app on the provider side. Make sure the app is configured with the default OmniAuth callback path /auth/google_oauth2/callback (in case of provider google_oauth2).

OmniAuth provider configuration

The OmniAuth provider configuration is taken care of by default in the config/initializers/omniauth.rb file and will look something like this (in its basic form):

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
end

This is fine as it is and allows us to initiate the OAuth login flow with Google as well as accept the callback from Google.

Initiating the login

Where Devise's OmniAuth implementation offers you a helper like user_google_oauth2_omniauth_authorize_path to set the correct link for the "Log in with Google" button, we need to set a path of our own. We can simply use OmniAuth's default /auth/google_oauth2. However, when handling the callback later on, we need to be able to distinguish a callback for the login flow from a callback for the OAuth API flow. Therefore we'll add the origin parameter like so: /auth/google_oauth2?origin=login.

Routing the callback

We need to be able to handle both callbacks from the login flow as well as the OAuth API flow. We use the origin parameter to determine to which controller we should route the callback request in routes.rb, like so:

get '/auth/:provider/callback', to: "oauth_accounts#create_or_update", constraints: lambda { |req| !(req.env['omniauth.origin'] =~ /login/) }
get '/auth/failure', to: 'oauth_accounts#error', constraints: lambda { |req| !(req.env['omniauth.origin'] =~ /login/) }
devise_scope :user do
  get '/auth/google_oauth2/callback', to: 'omniauth_callbacks#google_oauth2'
  get '/auth/failure', to: 'omniauth_callbacks#failure'
end

Put the OAuth API callback routing before the login routing and check if omniauth.origin (which contains the value of our initial URL origin query parameter) does not contain login. In that case, route to the controller handling the OAuth API accounts. In all other cases, we route to Devise's omniauth_callbacks controller, where we add an action for each provider.

Note that adding a constraint to the get definition in the devise_scope block does not seem to work. Hence we solve that by putting the OAuth API callback routing before the login routing.

That's it

From here you can follow the instructions in the OmniAuth: overview to create the appropriate controller action per provider.

Dynamic scope

Say you use OmniAuth both for logging in with Google as well as for reading the Google Calendar API somewhere in your application, then most likely you don't want to ask for Calendar read permissions right when the user logs in. You want to do that later in the OAuth API flow. You can use OmniAuth's dynamic setup phase to use a different scope for each flow, like so:

GOOGLE_SETUP_PROC = lambda do |env|
  request = Rack::Request.new(env)
  scope = %w[email profile]
  scope << "calendar.readonly" if request.params["origin"] != "login"
  env['omniauth.strategy'].options[:scope] = scope.join(",")
end

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], setup: GOOGLE_SETUP_PROC
end
Clone this wiki locally