Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using the same email for different accounts on a multi-tenant app with Devise

TL;DR: in a multi-tenant app with Devise, a user should be able to register with the same email on different tenants/accounts. How can I make Devise use the account_id in User.find_for_database_authentication?


I have a multi-tenant app. Each subdomain belongs to an account, with many users and admins (different tables, no inheritance between them).

class User < ApplicationRecord
  belongs_to :account
  devise :database_authenticatable, :confirmable, :recoverable, :rememberable
  validates :email, uniqueness: true
end

class AdminUser < ApplicationRecord
  belongs_to :account
  devise :database_authenticatable, :confirmable, :recoverable, :rememberable
end

# config/routes.rb
scope module: 'auth' do
  devise_for :users, path: ''
  devise_for :admins, path: 'admin', class_name: 'AdminUser', ...
end

# /sign_in for users, /admin/sign_in for admins
# (you can log as both at the same time)

Thinks worked well, but I need to allow a user to sign in with the same email on different tenants/accounts. First, I fixed the validation:

class User < ApplicationRecord
  validates :email, uniqueness: { scope: :account_id }
end

The problem is that when a user sign up / log in, Devise will search for the first user with some email, regardless of the account_id, so if I share the same email address on subdomain1 and subdomain2, when I log into subdomain2, I get the user info from subdomain1 (which is wrong)

Devise documentation recommends to config request_keys and redefine User.find_for_database_authentication

  # config/initializers/devise.rb
  config.request_keys = [:subdomain]

  # app/models/user.rb
  def self.find_for_database_authentication warden_conditions
    joins(:account).where(
      email: warden_conditions[:email],
      accounts: {subdomain: warden_conditions[:subdomain]}
    ).first
  end

This kind of works, but I found two problems/disadvantages:

  1. When I log out with the user, it does the same with the admin (kind of annoying for development)

  2. I'd like to use account_id on User.find_for_database_authentication, and avoid the JOIN. I don't know how to do it, as subdomain is handled automagicaly by Devise/Warden. warden_conditions keys should be email and account_id.

like image 533
wacko Avatar asked Jan 27 '26 13:01

wacko


1 Answers

According to this Wiki

If you are using column name other than subdomain to scope login to subdomain, you may have to use authentication_keys. For example, if you have subdomains table and are using subdomain_id on your Devise model to scope User, you will have to add authentication_keys: [:email, :subdomain_id]

So I think you should

devise :database_authenticatable, :confirmable, :recoverable, :rememberable, authentication_keys[:account_id]

In addition, I think you should redefine find_first_by_auth_conditions instead of find_for_database_authentication in order to support password recovery

like image 96
dowi Avatar answered Jan 29 '26 03:01

dowi