diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ccb0882a4..ba9fae64cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### Unreleased + +* bug fixes + * Fix `Devise::FailureApp` not committing the CSRF token to the session on Rails 7.1+, which dropped the session and CSRF token on authentication failure. [#5851](https://github.com/heartcombo/devise/pull/5851) + ### 5.0.4 - 2026-05-08 * security fixes diff --git a/lib/devise/failure_app.rb b/lib/devise/failure_app.rb index 70cf6d2f31..3a6c189009 100644 --- a/lib/devise/failure_app.rb +++ b/lib/devise/failure_app.rb @@ -11,6 +11,12 @@ class FailureApp < ActionController::Metal include ActionController::UrlFor include ActionController::Redirecting + # Rails 7.1+ defers writing the CSRF token to the session and commits it at + # the end of the request through the controller instance. FailureApp becomes + # that instance, so it needs +commit_csrf_token+ from RequestForgeryProtection + # to avoid silently dropping the token (and the session) on auth failure. + include ActionController::RequestForgeryProtection + include Rails.application.routes.url_helpers include Rails.application.routes.mounted_helpers diff --git a/test/failure_app_test.rb b/test/failure_app_test.rb index c9e4a56ce1..7f7003f97c 100644 --- a/test/failure_app_test.rb +++ b/test/failure_app_test.rb @@ -479,4 +479,19 @@ def call_failure(env_params = {}) assert_equal 'http://test.host/users/sign_in', @response.second['Location'] end end + + # Only on Rails 7.1+, where the CSRF token is deferred and committed via the controller instance. + if ActionDispatch::Request.method_defined?(:commit_csrf_token) + context "Committing the CSRF token" do + test "stores the deferred CSRF token in the session" do + csrf_token = "a-csrf-token" + request = ActionDispatch::Request.new( + "rack.session" => {}, + ActionController::RequestForgeryProtection::CSRF_TOKEN => csrf_token + ) + Devise::FailureApp.new.commit_csrf_token(request) + assert_equal csrf_token, request.session[:_csrf_token] + end + end + end end