Skip to content

DeviseFailureApp bypasses tenant context, causing NoTenantError on authentication failures #251

@keshavbiswa

Description

@keshavbiswa

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

Rails.application.config.middleware.insert_after ActiveRecord::Tenanted::TenantSelector, OrganizationResolver

lib/organization_resolver.rb

# 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
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:

  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 (TenantSelectorOrganizationResolver) 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

# 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions