Search code examples
ruby-on-railsdeviseomniauthmulti-tenantapartment-gem

Devise OmniAuth with a Multi-tenant Rails 5 App


Here is the situation. I have a multi-tenant rails app using the apartment gem where I need to implement a LinkedIn OmniAuth Strategy.

As you can see by my routes, Devise users, and the associated routes, are only persisted on the individual schemas of the subdomains.

Example Route:

Good: https://frank.example.io/users/sign_in

Bad: https://example.io/users/sign_in

Routes

class SubdomainPresent
  def self.matches?(request)
    request.subdomain.present?
  end
end

class SubdomainBlank
  def self.matches?(request)
    request.subdomain.blank?
  end
end

Rails.application.routes.draw do
  constraints(SubdomainPresent) do

    ...

    devise_for :users, controllers: { 
      omniauth_callbacks: 'omniauth_callbacks'
    }
    devise_scope :user do
      get '/users/:id', to: 'users/registrations#show', as: "show_user"
    end

    ...

  end
end

My specific problem is that LinkedIn does not support wildcards with their callback URLs so I am lost on how I might be able to direct users to the right domain after OAuth authentication.


Solution

  • So it turns out the answer was to pass parameters in the authorization link which would eventually get passed to the callback action throught request.env["omniauth.params"]

    Authorization Link format:

    Here I was having trouble adding the parameters to the Devise URL builder so I just manually added the parameters. This can probably be moved to a url helper

    <%= link_to "Connect your Linkedin", "#{omniauth_authorize_path(:user, :linkedin)}?subdomain=#{request.subdomain}" %>
    

    Routes:

    Then I defined a route constrained by a blank subdomain pointing to the callback action.

    class SubdomainPresent
      def self.matches?(request)
        request.subdomain.present?
      end
    end
    
    class SubdomainBlank
      def self.matches?(request)
        request.subdomain.blank?
      end
    end
    
    Rails.application.routes.draw do
      constraints(SubdomainPresent) do
        ...
        devise_for :users, controllers: {
          omniauth_callbacks: 'omniauth_callbacks'
        }
        resources :users
        ...
      end
    
      constraints(SubdomainBlank) do
        root 'welcome#index'
        ...
        devise_scope :user do
          get 'linkedin/auth/callback', to: 'omniauth_callbacks#linkedin'
        end
        ...
      end
    end
    

    Controller:

    I used this tutorial to set up my callback controllers: Rails 4 OmniAuth using Devise with Twitter, Facebook and Linkedin. My main objective with the callback controller was to have it reside in in the blank subdomain so I only had to give one call back URL to my LinkedIn Dev App. With this controller I search the omniauth params for the subdomain parameter and use that to switch to the proper schema.

    def self.provides_callback_for(provider)
      class_eval %Q{
        def #{provider}
          raise ArgumentError, "you need a subdomain parameter with this route" if request.env["omniauth.params"].empty?
    
          subdomain = request.env["omniauth.params"]["subdomain"]
          Apartment::Tenant.switch!(subdomain)
          ...
        end
      }
    end