Compare commits

..

7 Commits

Author SHA1 Message Date
Carlos Antonio da Silva
9932bc5364 Add changelog entry for rack error status 2025-12-31 10:30:44 -03:00
Carlos Antonio da Silva
3d3e75b49d Translate error status to code using Rails if available to avoid warning
Rack has deprecated `:unprocessable_entity` in favor of
`:unprocessable_content`, but the former has been used extensively
for years. Rails is now transparently converting that under the hood to
avoid the warnings, but our failure app wasn't going through the same
handling since it's more low level and responds to Rack directly. This
introduces the Rails translation handling if available on newer
versions, falling back to the Rack conversion which might emit the
warning if using `:unprocessable_entity` on Rack 3.1+

To fully fix it, people can configure their `error_status` to
`:unprocessable_content` on newer versions of Rack. The default for new
Devise apps will also change.

https://github.com/rack/rack/pull/2137
2025-12-31 10:26:02 -03:00
Carlos Antonio da Silva
f8b8092092 Add test for conditional error status 2025-12-31 10:25:57 -03:00
Carlos Antonio da Silva
9fa4d6389b Revert changes to controllers
We'll continue using `:unprocessable_entity` since Rails will
transparently convert this for us for the time being.
2025-12-31 10:05:53 -03:00
Carlos Antonio da Silva
9ac7f5395f Merge branch 'main' into update-rack-unprocessable_content 2025-12-31 09:41:29 -03:00
Taketo Takashima
e7f55961f2 Use :unprocessable_content in generated Devise config for Rack 3.1+
For rack 3.1 and higher, devise config uses `:unprocessable_content` instead of `:unprocessable_entity`.
For rack 3.0 and below, it continues to use `:unprocessable_entity`.
2025-09-12 19:27:55 +09:00
Taketo Takashima
ed625a804f Use Devise.responder.error_status instead of fixed :unprocessable_entity in confirmations and unlocks controllers 2025-09-12 14:54:00 +09:00
21 changed files with 64 additions and 86 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:
@@ -53,9 +52,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
@@ -770,7 +770,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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -27,7 +27,7 @@ class InstallGeneratorTest < Rails::Generators::TestCase
test "responder error_status based on rack version" do
run_generator(["--orm=active_record"])
error_status = Rack::RELEASE >= "3.1" ? :unprocessable_content : :unprocessable_entity
error_status = Rack::VERSION >= "3.1" ? :unprocessable_content : :unprocessable_entity
assert_file "config/initializers/devise.rb", /config\.responder\.error_status = #{error_status.inspect}/
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