Compare commits

..

1 Commits

Author SHA1 Message Date
Carlos Antonio da Silva
dc1f26ed26 Pass locale with activatable / timeoutable hooks
We need to explicitly pass the `locale` around from the options (passed
to `warden.authenticate!` for instance) or the `I18n.locale` when
logging out and redirecting the user via `throw :warden`, otherwise in a
multi-locale app we'd lose the locale previously set / passed around and
fallback to the default for that flash message.

This is a follow-up of the fixes in #5567 where we implemented the
locale passing logic down to the failure app, but it missed these places
where we were using `throw :warden`.

Closes #5812
2025-12-29 18:22:48 -03:00
26 changed files with 72 additions and 108 deletions

View File

@@ -30,8 +30,6 @@ jobs:
ruby: '3.0'
- gemfile: Gemfile
ruby: '2.7'
- gemfile: gemfiles/Gemfile-rails-main
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-main
ruby: '3.1'
- gemfile: gemfiles/Gemfile-rails-main

View File

@@ -1,4 +1,4 @@
### 5.0.0.rc - 2025-12-31
### Unreleased
* breaking changes
* Drop support to Ruby < 2.7
@@ -8,7 +8,6 @@
* Remove deprecated `scope` second argument from `sign_in(resource, :admin)` controller test helper, use `sign_in(resource, scope: :admin)` instead. [#5803](https://github.com/heartcombo/devise/pull/5803)
* Remove deprecated `Devise::TestHelpers`, use `Devise::Test::ControllerHelpers` instead. [#5803](https://github.com/heartcombo/devise/pull/5803)
* Remove deprecated `Devise::Models::Authenticatable::BLACKLIST_FOR_SERIALIZATION` [#5598](https://github.com/heartcombo/devise/pull/5598)
* Remove deprecated `Devise.activerecord51?` method.
* Remove `SecretKeyFinder` and use `app.secret_key_base` as the default secret key for `Devise.secret_key` if a custom `Devise.secret_key` is not provided.
This is potentially a breaking change because Devise previously used the following order to find a secret key:
@@ -21,16 +20,10 @@
[#5645](https://github.com/heartcombo/devise/pull/5645)
* Change password instructions button label on devise view from `Send me reset password instructions` to `Send me password reset instructions` [#5515](https://github.com/heartcombo/devise/pull/5515)
* Change `<br>` tags separating form elements to wrapping them in `<p>` tags [#5494](https://github.com/heartcombo/devise/pull/5494)
* Replace `[data-turbo-cache=false]` with `[data-turbo-temporary]` on `devise/shared/error_messages` partial. This has been [deprecated by Turbo since v7.3.0 (released on Mar 1, 2023)](https://github.com/hotwired/turbo/releases/tag/v7.3.0).
If you are using an older version of Turbo and the default devise template, you'll need to copy it over to your app and change that back to `[data-turbo-cache=false]`.
* enhancements
* Add Rails 8 support.
- Routes are lazy-loaded by default in test and development environments now so Devise loads them before `Devise.mappings` call. [#5728](https://github.com/heartcombo/devise/pull/5728)
* New apps using Rack 3.1+ will be generated using `config.responder.error_status = :unprocessable_content`, since [`:unprocessable_entity` has been deprecated by Rack](https://github.com/rack/rack/pull/2137).
Latest versions of [Rails transparently convert `:unprocessable_entity` -> `:unprocessable_content`](https://github.com/rails/rails/pull/53383), and Devise will use that in the failure app to avoid Rack deprecation warnings for apps that are configured with `:unprocessable_entity`. They can also simply change their `error_status` to `:unprocessable_content` in latest Rack versions to avoid the warning.
* Add Ruby 3.4 and 4.0 support.
* Reenable Mongoid test suite across all Rails 7+ versions, to ensure we continue supporting it. Changes to dirty tracking to support Mongoid 8.0+. [#5568](https://github.com/heartcombo/devise/pull/5568)
* Password length validator is changed from
@@ -53,9 +46,6 @@
* Use `OmniAuth.config.allowed_request_methods` as routing verbs for the auth path [#5508](https://github.com/heartcombo/devise/pull/5508)
* Handle `on` and `ON` as true values to check params [#5514](https://github.com/heartcombo/devise/pull/5514)
* Fix passing `format` option to `devise_for` [#5732](https://github.com/heartcombo/devise/pull/5732)
* Use `ActiveRecord::SecurityUtils.secure_compare` in `Devise.secure_compare` to match two empty strings correctly. [#4829](https://github.com/heartcombo/devise/pull/4829)
* Respond with `401 Unauthorized` for non-navigational requests to destroy the session when there is no authenticated resource. [#4878](https://github.com/heartcombo/devise/pull/4878)
* Fix incorrect grammar of invalid authentication message with capitalized attributes, e.g.: "Invalid Email or password" => "Invalid email or password". (originally introduced by [#4014](https://github.com/heartcombo/devise/pull/4014), released on v4.1.0) [#4834](https://github.com/heartcombo/devise/pull/4834)
Please check [4-stable](https://github.com/heartcombo/devise/blob/4-stable/CHANGELOG.md)

View File

@@ -11,10 +11,10 @@ GIT
PATH
remote: .
specs:
devise (5.0.0.rc)
devise (5.0.0.beta)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 7.0)
railties (>= 6.0.0)
responders
warden (~> 1.2.3)
@@ -309,4 +309,4 @@ DEPENDENCIES
webrat
BUNDLED WITH
4.0.3
4.0.3

View File

@@ -1,4 +1,4 @@
Copyright (c) 2020-CURRENT Rafael França, Carlos Antonio da Silva
Copyright (c) 2020-2025 Rafael França, Carlos Antonio da Silva
Copyright (c) 2009-2019 Plataformatec
Permission is hereby granted, free of charge, to any person obtaining

View File

@@ -137,17 +137,17 @@ Please note that the command output will show the variable value being used.
#### BUNDLE_GEMFILE
We can use this variable to tell bundler what Gemfile it should use (instead of the one in the current directory).
Inside the [gemfiles](https://github.com/heartcombo/devise/tree/main/gemfiles) directory, we have one for each version of Rails we support. When you send us a pull request, it may happen that the test suite breaks using some of them. If that's the case, you can simulate the same environment using the `BUNDLE_GEMFILE` variable.
For example, if the tests broke using Ruby 3.4 and Rails 8.0, you can do the following:
For example, if the tests broke using Ruby 3.0.0 and Rails 6.0, you can do the following:
```bash
chruby 3.4.0 # or rbenv shell 3.4.0, or rvm use 3.4.0, etc.
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-8-0 bundle install
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-8-0 bin/test
rbenv shell 3.0.0 # or rvm use 3.0.0
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-6-0 bundle install
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-6-0 bin/test
```
You can also combine both of them if the tests broke for Mongoid:
```bash
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-8-0 bundle install
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-8-0 DEVISE_ORM=mongoid bin/test
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-6-0 bundle install
BUNDLE_GEMFILE=gemfiles/Gemfile-rails-6-0 DEVISE_ORM=mongoid bin/test
```
### Running tests
@@ -181,7 +181,7 @@ Once you have solidified your understanding of Rails and authentication mechanis
## Getting started
Devise 5 works with Rails 7 onwards. Run:
Devise 4.0 works with Rails 6.0 onwards. Run:
```sh
bundle add devise
@@ -493,8 +493,7 @@ Devise.setup do |config|
# apps is `200 OK` and `302 Found` respectively, but new apps are generated with
# these new defaults that match Hotwire/Turbo behavior.
# Note: These might become the new default in future versions of Devise.
config.responder.error_status = :unprocessable_content # for Rack 3.1 or higher
# config.responder.error_status = :unprocessable_entity # for Rack 3.0 or lower
config.responder.error_status = :unprocessable_entity
config.responder.redirect_status = :see_other
end
```
@@ -770,7 +769,7 @@ https://github.com/wardencommunity/warden
## License
MIT License.
Copyright 2020-CURRENT Rafael França, Carlos Antonio da Silva.
Copyright 2020-2025 Rafael França, Carlos Antonio da Silva.
Copyright 2009-2019 Plataformatec.
The Devise logo is licensed under [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/).

View File

@@ -27,7 +27,7 @@ class Devise::ConfirmationsController < DeviseController
set_flash_message!(:notice, :confirmed)
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
else
# TODO: use `error_status` when the default changes to `:unprocessable_entity` / `:unprocessable_content`.
# TODO: use `error_status` when the default changes to `:unprocessable_entity`.
respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new }
end
end

View File

@@ -28,7 +28,7 @@ class Devise::SessionsController < DeviseController
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
set_flash_message! :notice, :signed_out if signed_out
yield if block_given?
respond_to_on_destroy(non_navigational_status: :no_content)
respond_to_on_destroy
end
protected
@@ -62,7 +62,7 @@ class Devise::SessionsController < DeviseController
if all_signed_out?
set_flash_message! :notice, :already_signed_out
respond_to_on_destroy(non_navigational_status: :unauthorized)
respond_to_on_destroy
end
end
@@ -72,11 +72,11 @@ class Devise::SessionsController < DeviseController
users.all?(&:blank?)
end
def respond_to_on_destroy(non_navigational_status: :no_content)
def respond_to_on_destroy
# We actually need to hardcode this as Rails default responder doesn't
# support returning empty response on GET request
respond_to do |format|
format.all { head non_navigational_status }
format.all { head :no_content }
format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status }
end
end

View File

@@ -29,7 +29,7 @@ class Devise::UnlocksController < DeviseController
set_flash_message! :notice, :unlocked
respond_with_navigational(resource){ redirect_to after_unlock_path_for(resource) }
else
# TODO: use `error_status` when the default changes to `:unprocessable_entity` / `:unprocessable_content`.
# TODO: use `error_status` when the default changes to `:unprocessable_entity`.
respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new }
end
end

View File

@@ -1,5 +1,5 @@
<% if resource.errors.any? %>
<div id="error_explanation" data-turbo-temporary>
<div id="error_explanation" data-turbo-cache="false">
<h2>
<%= I18n.t("errors.messages.not_saved",
count: resource.errors.count,

View File

@@ -30,6 +30,6 @@ Gem::Specification.new do |s|
s.add_dependency("warden", "~> 1.2.3")
s.add_dependency("orm_adapter", "~> 0.1")
s.add_dependency("bcrypt", "~> 3.0")
s.add_dependency("railties", ">= 7.0")
s.add_dependency("railties", ">= 6.0.0")
s.add_dependency("responders")
end

View File

@@ -517,13 +517,25 @@ module Devise
# constant-time comparison algorithm to prevent timing attacks
def self.secure_compare(a, b)
return false if a.nil? || b.nil?
ActiveSupport::SecurityUtils.secure_compare(a, b)
return false if a.blank? || b.blank? || a.bytesize != b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
def self.deprecator
@deprecator ||= ActiveSupport::Deprecation.new("5.0", "Devise")
end
def self.activerecord51? # :nodoc:
deprecator.warn <<-DEPRECATION.strip_heredoc
[Devise] `Devise.activerecord51?` is deprecated and will be removed in the next major version.
It is a non-public method that's no longer used internally, but that other libraries have been relying on.
DEPRECATION
defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new("5.1.x")
end
end
require 'warden'

View File

@@ -77,9 +77,9 @@ module Devise
flash.now[:alert] = i18n_message(:invalid) if is_flashing_format?
self.response = recall_app(warden_options[:recall]).call(request.env).tap { |response|
status = response[0].in?(300..399) ? Devise.responder.redirect_status : Devise.responder.error_status
# Avoid warnings translating status to code using Rails if available (e.g. `unprocessable_entity` => `unprocessable_content`)
response[0] = ActionDispatch::Response.try(:rack_status_code, status) || Rack::Utils.status_code(status)
response[0] = Rack::Utils.status_code(
response[0].in?(300..399) ? Devise.responder.redirect_status : Devise.responder.error_status
)
}
end
@@ -111,16 +111,11 @@ module Devise
options[:scope] = "devise.failure"
options[:default] = [message]
auth_keys = scope_class.authentication_keys
human_keys = (auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys).map { |key|
scope_class.human_attribute_name(key).downcase
}
options[:authentication_keys] = human_keys.join(I18n.t(:"support.array.words_connector"))
keys = (auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys).map { |key| scope_class.human_attribute_name(key) }
options[:authentication_keys] = keys.join(I18n.t(:"support.array.words_connector"))
options = i18n_options(options)
I18n.t(:"#{scope}.#{message}", **options).then { |msg|
# Ensure that auth keys at the start of the translated string are properly cased.
msg.start_with?(human_keys.first) ? msg.upcase_first : msg
}
I18n.t(:"#{scope}.#{message}", **options)
else
message.to_s
end

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Devise
VERSION = "5.0.0.rc".freeze
VERSION = "5.0.0.beta".freeze
end

View File

@@ -305,7 +305,7 @@ Devise.setup do |config|
# apps is `200 OK` and `302 Found` respectively, but new apps are generated with
# these new defaults that match Hotwire/Turbo behavior.
# Note: These might become the new default in future versions of Devise.
config.responder.error_status = <%= Rack::Utils::SYMBOL_TO_STATUS_CODE.key(422).inspect %>
config.responder.error_status = :unprocessable_entity
config.responder.redirect_status = :see_other
# ==> Configuration for :registerable

View File

@@ -74,7 +74,7 @@ class SessionsControllerTest < Devise::ControllerTestCase
assert_template "devise/sessions/new"
end
test "#destroy doesn't set the flash and returns 204 status if the requested format is not navigational" do
test "#destroy doesn't set the flash if the requested format is not navigational" do
request.env["devise.mapping"] = Devise.mappings[:user]
user = create_user
user.confirm
@@ -87,16 +87,4 @@ class SessionsControllerTest < Devise::ControllerTestCase
assert flash[:notice].blank?, "flash[:notice] should be blank, not #{flash[:notice].inspect}"
assert_equal 204, @response.status
end
test "#destroy returns 401 status if user is not signed in and the requested format is not navigational" do
request.env["devise.mapping"] = Devise.mappings[:user]
delete :destroy, format: 'json'
assert_equal 401, @response.status
end
test "#destroy returns 302 status if user is not signed in and the requested format is navigational" do
request.env["devise.mapping"] = Devise.mappings[:user]
delete :destroy
assert_equal 302, @response.status
end
end

View File

@@ -86,20 +86,15 @@ class DeviseTest < ActiveSupport::TestCase
Devise::CONTROLLERS.delete(:kivi)
end
test 'Devise.secure_compare fails when comparing different strings or nil' do
test 'should complain when comparing empty or different sized passes' do
[nil, ""].each do |empty|
assert_not Devise.secure_compare(empty, "something")
assert_not Devise.secure_compare("something", empty)
assert_not Devise.secure_compare(empty, empty)
end
assert_not Devise.secure_compare(nil, nil)
assert_not Devise.secure_compare("size_1", "size_four")
end
test 'Devise.secure_compare passes when strings are the same, even two empty strings' do
assert Devise.secure_compare("", "")
assert Devise.secure_compare("something", "something")
end
test 'Devise.email_regexp should match valid email addresses' do
valid_emails = ["test@example.com", "jo@jo.co", "f4$_m@you.com", "testing.example@example.com.ua", "test@tt", "test@valid---domain.com"]
non_valid_emails = ["rex", "test user@example.com", "test_user@example server.com"]
@@ -111,4 +106,10 @@ class DeviseTest < ActiveSupport::TestCase
assert_no_match Devise.email_regexp, email
end
end
test 'Devise.activerecord51? deprecation' do
assert_deprecated("`Devise.activerecord51?` is deprecated", Devise.deprecator) do
Devise.activerecord51?
end
end
end

View File

@@ -184,27 +184,17 @@ class FailureTest < ActiveSupport::TestCase
test 'uses the proxy failure message as symbol' do
call_failure('warden' => OpenStruct.new(message: :invalid))
assert_equal 'Invalid email or password.', @request.flash[:alert]
assert_equal 'Invalid Email or password.', @request.flash[:alert]
assert_equal 'http://test.host/users/sign_in', @response.second["Location"]
end
test 'supports authentication_keys as a Hash for the flash message' do
swap Devise, authentication_keys: { email: true, login: true } do
call_failure('warden' => OpenStruct.new(message: :invalid))
assert_equal 'Invalid email, login or password.', @request.flash[:alert]
assert_equal 'Invalid Email, Login or password.', @request.flash[:alert]
end
end
test 'downcases authentication_keys for the flash message' do
call_failure('warden' => OpenStruct.new(message: :invalid))
assert_equal 'Invalid email or password.', @request.flash[:alert]
end
test 'humanizes the flash message' do
call_failure('warden' => OpenStruct.new(message: :invalid))
assert_equal @request.flash[:alert], @request.flash[:alert].humanize
end
test 'uses custom i18n options' do
call_failure('warden' => OpenStruct.new(message: :does_not_exist), app: FailureWithI18nOptions)
assert_equal 'User Steve does not exist', @request.flash[:alert]
@@ -298,7 +288,7 @@ class FailureTest < ActiveSupport::TestCase
test 'uses the failure message as response body' do
call_failure('formats' => Mime[:xml], 'warden' => OpenStruct.new(message: :invalid))
assert_match '<error>Invalid email or password.</error>', @response.third.body
assert_match '<error>Invalid Email or password.</error>', @response.third.body
end
test 'respects the i18n locale passed via warden options when responding to HTTP request' do
@@ -353,7 +343,7 @@ class FailureTest < ActiveSupport::TestCase
}
call_failure(env)
assert_includes @response.third.body, '<h2>Log in</h2>'
assert_includes @response.third.body, 'Invalid email or password.'
assert_includes @response.third.body, 'Invalid Email or password.'
end
test 'calls the original controller if not confirmed email' do
@@ -388,7 +378,7 @@ class FailureTest < ActiveSupport::TestCase
}
call_failure(env)
assert_includes @response.third.body, '<h2>Log in</h2>'
assert_includes @response.third.body, 'Invalid email or password.'
assert_includes @response.third.body, 'Invalid Email or password.'
assert_equal '/sample', @request.env["SCRIPT_NAME"]
assert_equal '/users/sign_in', @request.env["PATH_INFO"]
end
@@ -419,7 +409,7 @@ class FailureTest < ActiveSupport::TestCase
call_failure(env)
assert_equal 422, @response.first
assert_includes @response.third.body, 'Invalid email or password.'
assert_includes @response.third.body, 'Invalid Email or password.'
end
end
@@ -445,7 +435,7 @@ class FailureTest < ActiveSupport::TestCase
call_failure(env)
assert_equal 200, @response.first
assert_includes @response.third.body, 'Invalid email or password.'
assert_includes @response.third.body, 'Invalid Email or password.'
end
test 'users default hardcoded responder `redirect_status` for the status code since responders version does not support configuring it' do

View File

@@ -37,4 +37,5 @@ class DeviseGeneratorTest < Rails::Generators::TestCase
FileUtils.mkdir_p(destination)
FileUtils.cp routes, destination
end
end

View File

@@ -23,12 +23,4 @@ class InstallGeneratorTest < Rails::Generators::TestCase
assert_no_file "config/initializers/devise.rb"
assert_no_file "config/locales/devise.en.yml"
end
test "responder error_status based on rack version" do
run_generator(["--orm=active_record"])
error_status = Rack::RELEASE >= "3.1" ? :unprocessable_content : :unprocessable_entity
assert_file "config/initializers/devise.rb", /config\.responder\.error_status = #{error_status.inspect}/
end
end

View File

@@ -563,7 +563,7 @@ class AuthenticationKeysTest < Devise::IntegrationTest
test 'missing authentication keys cause authentication to abort' do
swap Devise, authentication_keys: [:subdomain] do
sign_in_as_user
assert_contain "Invalid subdomain or password."
assert_contain "Invalid Subdomain or password."
assert_not warden.authenticated?(:user)
end
end
@@ -602,7 +602,7 @@ class AuthenticationRequestKeysTest < Devise::IntegrationTest
swap Devise, request_keys: [:subdomain] do
sign_in_as_user
assert_contain "Invalid email or password."
assert_contain "Invalid Email or password."
assert_not warden.authenticated?(:user)
end
end

View File

@@ -151,7 +151,7 @@ class ConfirmationTest < Devise::IntegrationTest
fill_in 'password', with: 'invalid'
end
assert_contain 'Invalid email or password'
assert_contain 'Invalid Email or password'
assert_not warden.authenticated?(:user)
end
end

View File

@@ -70,7 +70,7 @@ class DatabaseAuthenticationTest < Devise::IntegrationTest
fill_in 'password', with: 'abcdef'
end
assert_contain 'Invalid email or password'
assert_contain 'Invalid Email or password'
assert_not warden.authenticated?(:admin)
end
@@ -82,7 +82,7 @@ class DatabaseAuthenticationTest < Devise::IntegrationTest
end
assert_not_contain 'Not found in database'
assert_contain 'Invalid email or password.'
assert_contain 'Invalid Email or password.'
end
end
end

View File

@@ -52,7 +52,7 @@ class HttpAuthenticationTest < Devise::IntegrationTest
sign_in_as_new_user_with_http("unknown")
assert_equal 401, status
assert_equal "application/json; charset=utf-8", headers["Content-Type"]
assert_match '"error":"Invalid email or password."', response.body
assert_match '"error":"Invalid Email or password."', response.body
end
test 'returns a custom response with www-authenticate and chosen realm' do

View File

@@ -191,9 +191,7 @@ class SessionTimeoutTest < Devise::IntegrationTest
test 'does not crash when the last_request_at is a String' do
user = sign_in_as_user
assert_nothing_raised do
get edit_form_user_path(user, last_request_at: Time.now.utc.to_s)
get users_path
end
get edit_form_user_path(user, last_request_at: Time.now.utc.to_s)
get users_path
end
end

View File

@@ -90,7 +90,7 @@ class ActiveRecordTest < ActiveSupport::TestCase
def send_devise_notification(*); end
end
assert_nothing_raised { klass.create! }
klass.create!
end
end

View File

@@ -112,7 +112,7 @@ class TestControllerHelpersTest < Devise::ControllerTestCase
end
test "defined Warden after_authentication callback should not be called when sign_in is called" do
assert_nothing_raised do
begin
Warden::Manager.after_authentication do |user, auth, opts|
flunk "callback was called while it should not"
end
@@ -126,7 +126,7 @@ class TestControllerHelpersTest < Devise::ControllerTestCase
end
test "defined Warden before_logout callback should not be called when sign_out is called" do
assert_nothing_raised do
begin
Warden::Manager.before_logout do |user, auth, opts|
flunk "callback was called while it should not"
end