Skip to content

Latest commit

 

History

History
143 lines (107 loc) · 5.28 KB

MIGRATION_GUIDE.md

File metadata and controls

143 lines (107 loc) · 5.28 KB

Migration Guide

Upgrading from 2.2.0 to 3.0.0

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.

1. Check your Ruby and Rails versions

  • 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.

2. Fix old unversioned migrations

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.

3. Pass a scope to is_fully_authenticated? for non-:user models

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:

  1. the scope of the current Devise controller (resource_name), or
  2. Devise.default_scope outside Devise controllers, or
  3. 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)

4. Expect 422 from the code form

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_for in a custom controller, render with status: 422 as well:

    def after_two_factor_fail_for(resource)
      # ...
      render :show, status: 422
    end

5. Update overridden views

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_to with Devise.sign_out_via. link_to ..., method: :delete needs rails-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: :get option was removed from the "Resend Code" links.

6. Check JSON clients

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.

7. Things that now work without changes

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_otp and clear_direct_otp no longer raise NoMethodError on Rails 6.1+ (they called the removed update_attributes).
  • A successful TOTP login no longer fails on PostgreSQL with PG::DatetimeFieldOverflow. totp_timestamp is now stored as a Time; existing values keep working.
  • The schema helpers (t.second_factor_attempts_count, t.totp_timestamp, ...) work again in create_table and change_table.
  • rails g two_factor_authentication MODEL works 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_code in only:).
  • ROTP 5.0, which renamed ROTP::Base32.random_base32 to random, is supported.
  • User.totp_timestamp is no longer defined as a class method. It read a Devise setting that does not exist and always raised; the totp_timestamp column on each record is unaffected.

Upgrading from 1.x to 2.x

See Upgrading from version 1.X to 2.X in the README.