Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 5, Apartment and Devise: sign in with subdomains are not working

I’ve setup a Rails 5 application with Apartment (1.2.0) and Devise (4.2.0). Due to some DDNS issues there is the constraint that the app is only reachable under app.myapp.com (note the subdomain app). myapp.com redirects to app.myapp.com.

My use case is that every user (tenant) signed up to the app should access their specific data through their subdomain (e.g. tenant.myapp.com). The users should not be scoped to their subdomain. Basically it should be possible to sign in from any subdomain. Redirection to the correct subdomain for the tenant is handled by ApplicationController. As per Devise standard the login page is found at app.myapp.com/users/sign_in. That’s where the problems start:

The user cannot login because of ”email or password incorrect“ error.

In development I played around a bit. Signing in from lvh.me works perfectly well. The user is logged in and is redirected to their subdomain. Trying the same with app.lvh.me leads to the afore mentioned problem.

I’ve set the session store to:

# /config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: '_myapp_session', domain: {
  production:   '.app.myapp.com',
  staging:      '.app.myapp.com',
  development:  '.app.lvh.me'
}.fetch(Rails.env.to_sym, :all)

I’ve also tried the follwing which doesn’t work either:

Rails.application.config.session_store :cookie_store, key: '_myapp_session', domain: :all

What do I need to do so that logging in from any subdomain works?

A test case would be:

User user1 visits the url app.myapp.com/users/sign_in provides their credentials and is therefor signed in and redirected to user1.myapp.com.

Bonus: user1 visits the the url another_user.myapp.com/users/sign_in provides their credentials and is therefor signed in and redirected to user1.myapp.com.

Edit

Other relevant configurations:

# /config/initializers/apartment.rb
config.excluded_models = %w{ User }
config.tenant_names = lambda { User.pluck :subdomain }
config.tld_length = 2
Rails.application.config.middleware.insert_before Warden::Manager, Apartment::Elevators::FirstSubdomain
Apartment::Elevators::FirstSubdomain.excluded_subdomains = ExcludedSubdomains.subdomains

and

# /app/classes/excluded_subdomains.rb
class ExcludedSubdomains
  def self.subdomains
    %w( www admin test public private staging app web net )
  end # subdomains
end # class

and

# /app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable

  after_create :create_tenant
  after_destroy :delete_tenant

  # other stuff
  def create_tenant
    Apartment::Tenant.create(subdomain)
  end # create_tenant


  def delete_tenant
    Apartment::Tenant.drop(subdomain)
  end # delete_tenant
end # class

and

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :authenticate_user!
  before_action :redirect_to_subdomain

  private
  def redirect_to_subdomain
    return if self.is_a?(DeviseController) || self.is_a?(Users::OnboardingController)

    if current_user.present? && request.subdomain != current_user.subdomain
      redirect_to main_index_url(subdomain: current_user.subdomain)
    end # if
  end # redirect_to_subdomain

  def after_sign_in_path_for(resource_or_scope)
    users_onboarding_start_url(subdomain: resource_or_scope.subdomain)
  end # after_sign_in_path_for

  def after_sign_out_path_for(resource_or_scope)
    successful_logout_url(subdomain: '')
  end # after_sign_out_path_for
end # class
like image 645
Ray Wojciechowski Avatar asked Feb 26 '17 11:02

Ray Wojciechowski


2 Answers

I’ve found the problem! Instead of prepending the app domain with .app the domain should have no subdomains prepended but a dot. Like so:

# /config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: '_myapp_session', domain: {
  production:   '.myapp.com',
  staging:      '.myapp.com',
  development:  '.lvh.me'
}.fetch(Rails.env.to_sym, :all)

It was so hard for me to find this simple mistake that I even created an example app. As this example already exists I thought I can share it. It can be found at GitHub.

like image 106
Ray Wojciechowski Avatar answered Nov 14 '22 03:11

Ray Wojciechowski


One can configure the apartment to exclude certain models from multi-tenancy.

config.excluded_models = ["User"]

The above directive allows you to maintain a common Users across all tenants.

How to scope this user to a particular tenant and redirect them to the correct subdomain needs some re-engineering on devise model and controllers front. You can consider adding additional columns like subdomain, tenant_name into User model for association to a particular client and augment/extend the devise controllers to register, login and redirect the users correctly.

Instead of your current entry in session_store.rb, try adding the following line in session_store.rb, by replacing .testdomain.com with your domain name.

Rails.application.config.session_store :cookie_store, key: '_jcrop_app_session', domain: ".testdomain.com"

Though, I did successfully login by using any subdomain like myapp1 or app, I did not try the redirection part of the code.

like image 1
fossil Avatar answered Nov 14 '22 05:11

fossil