basecamp / basecamp/activerecord-tenanted

DeviseFailureApp bypasses tenant context, causing NoTenantError on authentication failures

Open
#251 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.