basecamp / basecamp/activerecord-tenanted
DeviseFailureApp bypasses tenant context, causing NoTenantError on authentication failures
- Dominant language
- Ruby
- Stars
- 617
- Forks
- 29
- PR merge metrics
- No merged PRs in 30d
Description
Environment:
- Rails: edge (8.2.0.alpha)
- activerecord-tenanted: 0.5.0
- Devise: 5.0.0
- Architecture: Database-per-tenant with subdomain-based tenancy (one primary db, and many secondary (tenanted DBs)
Context:
I'm using this gem to tenant my application and I use devise to authenticate. I have a rack middleware that I insert after `ActiveRecord::Tenanted::TenantSelector` that I use to set `Current.organization`
config/initializer.organization_resolver.rb
```ruby
Rails.application.config.middleware.insert_after ActiveRecord::Tenanted::TenantSelector, OrganizationResolver
```
lib/organization_resolver.rb
```ruby
# frozen_string_literal: true
class OrganizationResolver
def initialize(app)
@app = app
end
def call(env)
tenant = ApplicationRecord.current_tenant
if tenant.present?
organization = Organization.find_by(subdomain: tenant)
Current.organization = organization if organization
end
@app.call(env)
end
end
```
Problem:
When I sign in on my tenanted app, everything works as expected. However, if my sign in fails (incorrect password, email,etc) . `ActiveRecord::Tenanted::NoTenantError` is raised because both `current_tenant` and `Current.organization` is `nil`
Here is how you can reproduce this issue:
- Setup Devise and activerecord-tenanted
```yml
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
primary:
<<: *default
database: storage/dummy_development.sqlite3
secondary:
<<: *default
database: "storage/tenants/%{tenant}/main.sqlite3"
tenanted: true
cache:
<<: *default
database: storage/dummy_development_cache.sqlite3
migrations_paths: db/cache_migrate
queue:
<<: *default
database: storage/dummy_development_queue.sqlite3
migrations_paths: db/queue_migrate
cable:
<<: *default
database: storage/dummy_development_cable.sqlite3
migrations_paths: db/cable_migrate
```
- Setup authentication
- Submit wrong password
- Controller tries to render → any query triggers NoTenantError
I think the reason why this happens is because `Devise::FailureApp` is a Rack app that bypasses the middleware stack:
```ruby
module Devise
class FailureApp < ActionController::Metal
def self.call(env)
@respond ||= action(:respond)
@respond.call(env) # Direct Rack call - TenantSelector never runs!
end
def respond
# Tries to render/redirect WITHOUT tenant context
end
end
end
```
So the flow becomes like this:
- User submits wrong password
- Warden invokes the failure app directly as a Rack endpoint
- The Rails middleware stack (`TenantSelector` → `OrganizationResolver`) is not executed
- Both `ApplicationRecord.current_tenant` and `Current.organization` remain nil
- Any database query in the sign-in view triggers the error
I have fixed this solution by patching DeviseFailureApp by patching the FailureApp
```ruby
# frozen_string_literal: true
class CustomDeviseFailureApp < Devise::FailureApp
def respond
subdomain = request.subdomain
if subdomain.present? && !admin_subdomain?
ApplicationRecord.with_tenant(subdomain) do
organization = Organization.find_by(subdomain: subdomain)
Current.organization = organization if organization
super
end
else
super
end
end
private
def admin_subdomain?
request.subdomain == "admin"
end
end
```
My Questions:
1. Is this a known behaviour? Should users of activerecord-tenanted + Devise always create a custom `FailureApp` like this?
2. Is there a better pattern? Am I missing a capability that would handle this more elegantly?
3. If this is the expected approach, documenting it in the gem's README would save users significant debugging time, especially since Devise is extremely common in Rails apps.
4. We may need documentation on handling Rack middleware that bypasses the Rails stack
Contributor guide
Research direction
Start by reproducing the failed-Devise-login flow with the documented Rails middleware configuration and inspect the README guidance around tenant selection. Confirm whether Devise::FailureApp bypasses the tenant context, then document the supported integration pattern and what users should expect when rendering authentication failures. Done means the README answers the four questions raised without relying on an unresolved workaround.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rails, ruby
- Domain
- backend, documentation
- Issue type
- Documentation
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100