Version 3.0.0 brings the gem up to date with current Ruby, Rails and Devise, and fixes several bugs that made 2.2.0 unusable on Rails 6.1+, Ruby 3.2+ and PostgreSQL. Most apps only need to bump the gem version, but a few behaviours changed. Go through the checklist below.
- Rails 5.0 or newer is now required (it was
>= 3.1.1). If you are on Rails 3.x or 4.x, stay on 2.2.0. - The gem is tested on Ruby 2.3 – 4.0 with Rails 5.2, 6.0, 6.1, 7.0, 7.1, 7.2, 8.0 and 8.1. Rails 5.0 and 5.1 are allowed by the gemspec but are not tested.
Migrations generated by 2.2.0 and earlier inherit from
ActiveRecord::Migration without a version. Rails 5.1+ refuses to run them,
which breaks db:migrate on a fresh database (for example in CI). Add the
Rails version the migration was written for:
# Before
class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration
# After
class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration[5.0]Migrations that have already run are not executed again, so this only matters when the database is rebuilt from migrations.
The generator (rails g two_factor_authentication MODEL) now writes a
versioned migration. On PostgreSQL it also builds the
encrypted_otp_secret_key index with algorithm: :concurrently and
disable_ddl_transaction! so adding it does not lock the table.
is_fully_authenticated? used to read the :user warden session no matter
which model was signed in. For any other model (e.g. Admin) it returned
true before the second factor had been entered, so a
confirm_two_factor_authenticated guard did not protect that model.
It now checks:
- the scope of the current Devise controller (
resource_name), or Devise.default_scopeoutside Devise controllers, or- the scope you pass in.
Inside a Devise controller, such as the RegistrationsController guard in the
README, nothing changes for :user and other models are now protected. In your
own controllers, pass the scope explicitly if you have more than one 2FA model,
or if your default scope is not the model you want to check:
# Before: always checked :user
return if is_fully_authenticated?
# After
return if is_fully_authenticated?(:admin)When a submitted code is wrong, or the attempt limit is reached, the
update action now responds with 422 Unprocessable Entity instead of 200.
Turbo (the default since Rails 7) ignores a 200 response to a form submission,
so the error page was never shown.
-
Update any request or controller specs that expect a 200 after a wrong code.
-
If you override
after_two_factor_fail_forin a custom controller, render withstatus: 422as well:def after_two_factor_fail_for(resource) # ... render :show, status: 422 end
If you copied app/views/devise/two_factor_authentication/show.html.erb into
your app, your copy does not get these changes:
-
Sign out now uses
button_towithDevise.sign_out_via.link_to ..., method: :deleteneedsrails-ujs, which Rails 7+ apps no longer ship, so the link sends a GET and fails.<%# Before %> <%= link_to "Sign out", destroy_user_session_path, :method => :delete %> <%# After %> <%= button_to "Sign out", destroy_user_session_path, :method => :delete %>
-
The code field is autofocused:
text_field_tag :code, '', autofocus: true. -
The stray
action: :getoption was removed from the "Resend Code" links.
When a signed-in user still needs the second factor, a JSON request used to
get an empty 401. It now gets a 401 with a body that tells the client
where to send the user:
{ "redirect_to": "/users/two_factor_authentication" }The user's return location is set to root_path(format: :html), so your app
must define a root route to use this. HTML requests are still redirected,
and every other format still gets an empty 401.
These were broken in 2.2.0 and need no action, but you can remove any workarounds you added for them:
create_direct_otp,send_new_otpandclear_direct_otpno longer raiseNoMethodErroron Rails 6.1+ (they called the removedupdate_attributes).- A successful TOTP login no longer fails on PostgreSQL with
PG::DatetimeFieldOverflow.totp_timestampis now stored as aTime; existing values keep working. - The schema helpers (
t.second_factor_attempts_count,t.totp_timestamp, ...) work again increate_tableandchange_table. rails g two_factor_authentication MODELworks on Ruby 3.2+ and no longer has to be run from the app root.- The routes load on Rails 8.1 (which rejected
resend_codeinonly:). - ROTP 5.0, which renamed
ROTP::Base32.random_base32torandom, is supported. User.totp_timestampis no longer defined as a class method. It read a Devise setting that does not exist and always raised; thetotp_timestampcolumn on each record is unaffected.
See Upgrading from version 1.X to 2.X in the README.