Search code examples
ruby-on-railsdevisewarden

How do you add additional scoping to Devise user_signed_in? helper?


I have authentication that needs to be independent across:

subdomain1.domain.com subdomain2.domain.com etc.

Right now with the devise user_signed_in? helper - if someone authenticates on subdomain1, it is working on subdomain2. I would like to add a tenant_id scope to prevent this from happening.

I have been able to do this on login authentication in my model using:

def self.find_for_database_authentication(warden_conditions)
      where(:email => warden_conditions[:email], :tenant_id => warden_conditions[:tenant_id]).first
end

But I am not clear on how to do this on the logged in check.

Thanks!


Solution

  • In order do to do this you need to narrow the scope of the authentication to just the subdomain.

    Here is a Docs page on how to accomplish this. I'll add it here for reference:

    Overview

    1. Modify the Devise-generated migration to remove the index on email uniqueness constraint
    2. Change login keys to include :subdomain
    3. Override Devise hook method find_for_authentication

    Modify migration

    First, you must remove the email index uniqueness constraint. Because we'll be turning this into a scoped query, we will scope the new index to subdomain. If this is a brand new Devise model, you can open the Devise migration and change the following:

    # db/migrate/XXX_devise_create_users.rb
    def change
      # Remove this line
      add_index :users, :email, :unique => true
    
      # Replace with
      add_index :users, [:email, :subdomain], :unique => true
    end
    

    If this is an existing project, you'll need to create a new migration removing the old index and adding a new one:

    rails g migration reindex_users_by_email_and_subdomain
    
    
    # db/migrate/XXX_reindex_users_by_email_and_subdomain.rb
    def change
      remove_index :users, :email
      add_index :users, [:email, :subdomain], :unique => true
    end
    

    Change login keys

    In your Devise model, add :subdomain to :request_keys. By default :request_keys is set to [].

       # app/models/user.rb
    class User
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :trackable, request_keys: [:subdomain]
    end
    

    If you have multiple Devise models and you would like all of them to have the same :request_keys configuration, you can set that globally in config/initializers/devise.rb

    config.request_keys = [:subdomain] # default value = []
    

    If additionally you want to still be able to log in using a URL without a subdomain, :request_keys can also take a hash with booleans indicating if the key is required or not.

    config.request_keys = { subdomain: false }
    

    Check that you do not have :validatable in the devise call on the Model

    If you do, :validatable will prevent more than one record having the same email, even in different subdomains. If you want to keep some of the validations, you can copy the ones you want from https://github.com/plataformatec/devise/blob/master/lib/devise/models/validatable.rb

    Override Devise auth finder hook

    For Authenticatable, Devise uses the hook method Model.find_for_authentication. Override it to include your additional query parameters:

    # app/models/user.rb
    class User < ActiveRecord::Base
      def self.find_for_authentication(warden_conditions)
        where(:email => warden_conditions[:email], :subdomain => warden_conditions[:subdomain]).first
      end
    end
    

    Congrats, User login is now scoped to subdomain!