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
.
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
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.