Skip to content

Reconfigure warden when the middleware stack is rebuilt: #5769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

Edouard-chin
Copy link
Contributor

Reconfigure warden when the middleware stack is rebuilt:

  • Problem

    Starting from Rails 7.2, in tests environment, it is possible that Rails will rebuild the middleware stack, which for consequence will create a new Warden middleware that will not be configured by Devise.

    Context

    Since this commit rails/rails@79fd5b45a27 upstream, when an application uses the with_routing helper to create a temporary route set and defines a route that maps to a controller requiring authentication, an error will be raised on request: "RuntimeError: No Failure App provided"

    The issue is due to a memoization on devise.

    Solution

    When the Devise.warden_config object is modified, reset the memoization.

- ### Problem

  Starting from Rails 7.2, in tests environment, it is possible that
  Rails will rebuild the middleware stack, which for consequence
  will create a new Warden middleware that will not be configured
  by Devise.

  ### Context

  Since this commit rails/rails@79fd5b45a27
  upstream, when an application uses the `with_routing` helper to
  create a temporary route set and defines a route that maps to a
  controller requiring authentication, an error will be raised
  on request: "RuntimeError: No Failure App provided"

  The issue is due to a memoization on devise.

  ### Solution

  When the `Devise.warden_config` object is modified, reset
  the memoization.
@quixoten
Copy link

quixoten commented Mar 3, 2025

@Edouard-chin, I tried your branch on my rails project, but I was still getting the No Failure App provided error. I was able to get the failing test to pass by adding Devise.configure_warden!:

  test "should not allow portal users to access pipelines" do
    Devise.configure_warden! # << added line
    sign_in portal_users(:one)
    get pipelines_url
    assert_response :redirect
    assert_redirected_to new_customer_session_path
  end

I also tried removing the @devise_finalized ||= begin conditional so that Devise.configure_warden! would be called every time finalize! was called, but I still got the error. So at some point a new Devise::RouteSet is getting created but finalize! is not called on it before the next test runs.

I also tried replacing Devise.configure_warden! in the test with other potential solutions, but none worked:

  • app.routes.finalize! (NOTE: app.routes is a Rails::Engine::LazyRouteSet)
  • app.reload_routes!

UPDATE: adding app.routes.finalize! to the test (instead of Devise.configure_warden!) does work if I also remove the @devise_finalized ||= begin conditional from the Devise::RouteSet#finalize! method. In this case, the finalize! method gets called twice. The first call is happening somewhere outside of the test, and the second call is from adding app.routes.finalize! directly to the test. I added the following debug statements to the Devise::RouteSet#finalize! method:

      pp @devise_finalized
      pp Devise.class_variable_get(:@@warden_configured)
      pp Devise.warden_config

On the first call (outside of the test), I see:

nil
true
{:default_scope=>:user,
 :scope_defaults=>{},
 :default_strategies=>{:customer=>[:magic_link_authenticatable], :employee=>[], :portal_user=>[:database_authenticatable, :two_factor_authenticatable]},
 :intercept_401=>false,
 :failure_app=>#<Devise::Delegator:0x0000755912314da0>}

and on the second call (inside the test), I see:

true
nil
{:default_scope=>:default, :scope_defaults=>{}, :default_strategies=>{}, :intercept_401=>true}

I also tried removing both the @@warden_configured ||= begin conditional and the @devise_finalized ||= begin conditional, but the test still failed without the explicit app.routes.finalize! call

@quixoten
Copy link

quixoten commented Mar 3, 2025

Here's a combination of changes in rails and devise that got the test working without needing to change the test itself.


Rails change

diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 7ed155ab1a..5cec99dae3 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -69,7 +69,7 @@ def reset_routes(old_routes, old_integration_session)
             app.instance_variable_set(:@routes, old_routes)
             app.instance_variable_set(:@app, old_rack_app)
             @integration_session = old_integration_session
-            @routes = old_routes
+            @routes = old_routes.tap(&:finalize!)
           end
       end

Devise change

diff --git a/lib/devise.rb b/lib/devise.rb
index 86d5c395..f0ecc6e4 100644
--- a/lib/devise.rb
+++ b/lib/devise.rb
@@ -294,6 +294,7 @@ module Devise
 
   # Private methods to interface with Warden.
   mattr_reader :warden_config
+  mattr_reader :warden_configured
   @@warden_config = nil
   @@warden_config_blocks = []
   def self.warden_config=(config)
diff --git a/lib/devise/rails/routes.rb b/lib/devise/rails/routes.rb
index f43e62fe..637d3774 100644
--- a/lib/devise/rails/routes.rb
+++ b/lib/devise/rails/routes.rb
@@ -7,8 +7,8 @@ module Devise
   module RouteSet
     def finalize!
       result = super
-      @devise_finalized ||= begin
-        if Devise.router_name.nil? && defined?(@devise_finalized) && self != Rails.application.try(:routes)
+      Devise.warden_configured || begin
+        if Devise.router_name.nil? && Devise.warden_configured && self != Rails.application.try(:routes)
           warn "[DEVISE] We have detected that you are using devise_for inside engine routes. " \
             "In this case, you probably want to set Devise.router_name = MOUNT_POINT, where "   \
             "MOUNT_POINT is a symbol representing where this engine will be mounted at. For "   \
@@ -18,7 +18,6 @@ module Devise
 
         Devise.configure_warden!
         Devise.regenerate_helpers!
-        true
       end
       result
     end

@Edouard-chin
Copy link
Contributor Author

Edouard-chin commented Mar 5, 2025

Thanks, I didn't see that Rails' reset_routes also rebuild the middleware stack with the previous route set that is already finalized (and therefore won't trigger devise's monkeypatch).

I'll try to figure out a solution.

This issue happens when there are at least two tests, one using the with_routing helper, and the other doesn't.

@Edouard-chin
Copy link
Contributor Author

I opened a fix in Rails rails/rails#54705, as I believe the with_routing implementation has some caveats that needs to be fixed. If it gets accepted, this PR is not necessary. I'll close for now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants