two_factor_authentication 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +77 -0
  3. data/.gitignore +3 -0
  4. data/CHANGELOG.md +37 -2
  5. data/Gemfile +29 -8
  6. data/MIGRATION_GUIDE.md +143 -0
  7. data/README.md +145 -93
  8. data/app/controllers/devise/two_factor_authentication_controller.rb +3 -2
  9. data/app/views/devise/two_factor_authentication/show.html.erb +4 -4
  10. data/config/locales/de.yml +8 -0
  11. data/lib/generators/active_record/templates/migration.rb +9 -1
  12. data/lib/generators/active_record/two_factor_authentication_generator.rb +18 -0
  13. data/lib/generators/two_factor_authentication/two_factor_authentication_generator.rb +1 -1
  14. data/lib/two_factor_authentication/controllers/helpers.rb +7 -3
  15. data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +8 -5
  16. data/lib/two_factor_authentication/routes.rb +1 -1
  17. data/lib/two_factor_authentication/schema.rb +7 -7
  18. data/lib/two_factor_authentication/version.rb +1 -1
  19. data/spec/controllers/two_factor_authentication_controller_spec.rb +84 -7
  20. data/spec/features/two_factor_authenticatable_spec.rb +119 -14
  21. data/spec/generators/active_record/two_factor_authentication_generator_spec.rb +52 -4
  22. data/spec/generators/two_factor_authentication/two_factor_authentication_generator_spec.rb +37 -0
  23. data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +89 -7
  24. data/spec/lib/two_factor_authentication/schema_spec.rb +41 -0
  25. data/spec/rails_app/app/assets/config/manifest.js +2 -0
  26. data/spec/rails_app/app/controllers/home_controller.rb +6 -1
  27. data/spec/rails_app/app/models/guest_user.rb +1 -1
  28. data/spec/rails_app/app/models/secure_user.rb +13 -0
  29. data/spec/rails_app/config/application.rb +1 -1
  30. data/spec/rails_app/config/environments/test.rb +3 -8
  31. data/spec/rails_app/config/initializers/inflections.rb +4 -0
  32. data/spec/rails_app/config/routes.rb +2 -0
  33. data/spec/rails_app/db/migrate/20260929000000_add_otp_columns_to_users.rb +8 -0
  34. data/spec/rails_app/db/migrate/20260929000001_devise_create_secure_users.rb +22 -0
  35. data/spec/rails_app/db/schema.rb +26 -8
  36. data/spec/rails_app/lib/sms_provider.rb +2 -4
  37. data/spec/requests/two_factor_authentication_helpers_spec.rb +48 -0
  38. data/spec/spec_helper.rb +1 -0
  39. data/spec/support/authenticated_model_helper.rb +5 -30
  40. data/spec/support/controller_helper.rb +5 -4
  41. data/spec/support/features_spec_helper.rb +1 -0
  42. data/two_factor_authentication.gemspec +4 -5
  43. metadata +43 -26
  44. data/.travis.yml +0 -28
  45. data/spec/rails_app/config/initializers/secret_token.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3d6d6636a281220ef8fd5aee19a4c4aaa4129797
4
- data.tar.gz: 4954c89fa183dcb47c3104bfb14035ceaf6d20f4
2
+ SHA256:
3
+ metadata.gz: 3f83f45de8b5992a3311c7d0d47479f7b7121c7460925fa8661d8b7eb98f4f72
4
+ data.tar.gz: 4df0c76ae151691cec84f1d036d79b93094826c892d01edef5d0a847a92407af
5
5
  SHA512:
6
- metadata.gz: c559cf0c9c2c21519efdf25ce615ca5930dfc657564e4209b77298944ea3a2342414e7772b493db65c657c617641d0af4398a31bca0d3fbf81025e417fb688c2
7
- data.tar.gz: 7e0d5abb909bb53f04d3d0cb887a4e869944eda656318c7e782562ecdf7bd635c8079d144506b1633e71c83368604b989dd9b4a21f2d8c21cb41d1f2849b3b58
6
+ metadata.gz: cef6991e8c5032e5779ee0c5ec0348ad1b0010f947748b49da40b42dabb9e2d83ff43106b6be09acae3acbf3cefa20fbc1ef257d10012098d287d03dbd94635a
7
+ data.tar.gz: 509142892d180c5fc74d9da25166a12e45e6570893386a5bba70feb50e91d80212b67ee78dd46451c8d58e7d88a37ed6f081872519bf919fa3757f257fdfdd02
@@ -0,0 +1,77 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ name: Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }}
13
+ runs-on: ubuntu-latest
14
+ continue-on-error: ${{ matrix.rails == 'main' }}
15
+ timeout-minutes: 20
16
+
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ include:
21
+ - ruby: "2.3"
22
+ rails: "5.2"
23
+ - ruby: "2.4"
24
+ rails: "5.2"
25
+ - ruby: "2.5"
26
+ rails: "5.2"
27
+ - ruby: "2.5"
28
+ rails: "6.0"
29
+ - ruby: "2.7"
30
+ rails: "6.0"
31
+ - ruby: "2.5"
32
+ rails: "6.1"
33
+ - ruby: "3.1"
34
+ rails: "6.1"
35
+ - ruby: "2.7"
36
+ rails: "7.0"
37
+ - ruby: "3.2"
38
+ rails: "7.0"
39
+ - ruby: "2.7"
40
+ rails: "7.1"
41
+ - ruby: "3.3"
42
+ rails: "7.1"
43
+ - ruby: "3.1"
44
+ rails: "7.2"
45
+ - ruby: "4.0"
46
+ rails: "7.2"
47
+ - ruby: "3.2"
48
+ rails: "8.0"
49
+ - ruby: "4.0"
50
+ rails: "8.0"
51
+ - ruby: "3.2"
52
+ rails: "8.1"
53
+ - ruby: "4.0"
54
+ rails: "8.1"
55
+ - ruby: "4.0"
56
+ rails: main
57
+
58
+ env:
59
+ RAILS_VERSION: ${{ matrix.rails }}
60
+
61
+ steps:
62
+ - name: Check out repository
63
+ uses: actions/checkout@v7
64
+ with:
65
+ persist-credentials: false
66
+
67
+ - name: Set up Ruby
68
+ uses: ruby/setup-ruby@v1
69
+ with:
70
+ ruby-version: ${{ matrix.ruby }}
71
+ bundler-cache: true
72
+
73
+ - name: Set up test database
74
+ run: bundle exec rake app:db:setup
75
+
76
+ - name: Run specs
77
+ run: bundle exec rake spec
data/.gitignore CHANGED
@@ -21,3 +21,6 @@ dump.rdb
21
21
  .rbenv-version
22
22
  .ruby-gemset
23
23
  .ruby-version
24
+
25
+ # Generator spec output
26
+ /tmp
data/CHANGELOG.md CHANGED
@@ -1,8 +1,43 @@
1
1
  # Change Log
2
2
 
3
- ## [Unreleased](https://github.com/Houdini/two_factor_authentication/tree/HEAD)
3
+ ## [v3.0.0](https://github.com/Houdini/two_factor_authentication/tree/v3.0.0) (2026-10-01)
4
+ [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v2.2.0...v3.0.0)
4
5
 
5
- [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1.5...HEAD)
6
+ See the [Migration Guide](MIGRATION_GUIDE.md#upgrading-from-220-to-300) for upgrade steps.
7
+
8
+ **Breaking changes:**
9
+
10
+ - Require Rails 5.0 or newer (was 3.1.1). Tested on Ruby 2.3–4.0 with Rails 5.2 through 8.1
11
+ - `is_fully_authenticated?` checks the current Devise scope (or `Devise.default_scope`) instead of always `:user`, and accepts a scope: `is_fully_authenticated?(:admin)`. Before, it returned `true` for other models before the second factor had been entered
12
+ - A rejected code or reaching the attempt limit now responds with 422 instead of 200, so Turbo renders the error
13
+ - JSON requests that still need the second factor get a 401 with a `{"redirect_to": ...}` body instead of an empty 401 [\#143](https://github.com/Houdini/two_factor_authentication/pull/143) ([Kevinrob](https://github.com/Kevinrob))
14
+
15
+ **Fixed bugs:**
16
+
17
+ - Replace `update_attributes` (removed in Rails 6.1) with `update`, which broke `create_direct_otp`, `send_new_otp` and `clear_direct_otp`
18
+ - Store `totp_timestamp` as a `Time`; PostgreSQL rejected the Integer, so every TOTP login failed
19
+ - The generator writes a versioned migration (`ActiveRecord::Migration[x.y]`), which Rails 5.1+ requires
20
+ - The generator no longer uses `File.exists?` (removed in Ruby 3.2) and finds the model relative to the destination root
21
+ - Remove `resend_code` from the route's `only:`, which Rails 8.1 rejects
22
+ - The schema table helpers (`t.second_factor_attempts_count`, `t.totp_timestamp`, ...) no longer call the removed `apply_devise_schema`
23
+ - Remove `totp_timestamp` from the Devise model config; `User.totp_timestamp` raised `NoMethodError`
24
+ - "Sign out" on the two factor page uses `button_to` with `Devise.sign_out_via`, so it works without rails-ujs
25
+ - Support ROTP 5.0, which renamed `ROTP::Base32.random_base32` to `random` [\#171](https://github.com/Houdini/two_factor_authentication/pull/171) ([jaspervandenberg](https://github.com/jaspervandenberg))
26
+
27
+ **Implemented enhancements:**
28
+
29
+ - Add the encrypted OTP secret index concurrently on PostgreSQL [\#197](https://github.com/Houdini/two_factor_authentication/pull/197) ([Lackoftactics](https://github.com/Lackoftactics))
30
+ - Autofocus the code field [\#174](https://github.com/Houdini/two_factor_authentication/pull/174) ([gustavokitman](https://github.com/gustavokitman))
31
+ - Add German translations [\#166](https://github.com/Houdini/two_factor_authentication/pull/166) ([JanBussieck](https://github.com/JanBussieck))
32
+
33
+ **Other changes:**
34
+
35
+ - Replace Travis CI with GitHub Actions
36
+ - Test TOTP sign-in, resending the code, the attempt counter reset and an encrypted-secret model end to end
37
+ - README updates [\#168](https://github.com/Houdini/two_factor_authentication/pull/168) ([MarkFChavez](https://github.com/MarkFChavez)), [\#139](https://github.com/Houdini/two_factor_authentication/pull/139) ([rmm5t](https://github.com/rmm5t))
38
+
39
+ ## [v2.0](https://github.com/Houdini/two_factor_authentication/tree/v2.0) (2017-05-12)
40
+ [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1.5...v2.0)
6
41
 
7
42
  **Merged pull requests:**
8
43
 
data/Gemfile CHANGED
@@ -1,27 +1,48 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in devise_ip_filter.gemspec
3
+ # Specify your gem's dependencies in two_factor_authentication.gemspec
4
4
  gemspec
5
5
 
6
6
  rails_version = ENV["RAILS_VERSION"] || "default"
7
7
 
8
8
  rails = case rails_version
9
- when "master"
10
- {github: "rails/rails"}
9
+ when "main"
10
+ {github: "rails/rails", branch: "main"}
11
11
  when "default"
12
- "~> 5.2"
12
+ "~> 8.1.0"
13
13
  else
14
- "~> #{rails_version}"
14
+ requirement = rails_version.split('.').length == 2 ? "#{rails_version}.0" : rails_version
15
+ "~> #{requirement}"
15
16
  end
16
17
 
17
18
  gem "rails", rails
18
19
 
19
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0')
20
- gem "test-unit", "~> 3.0"
20
+ # Rails main freezes controller default_url_options; rspec-rails 8.0.4 still
21
+ # mutates it in feature specs. Drop once a release includes rspec/rspec-rails#2907.
22
+ gem "rspec-rails", github: "rspec/rspec-rails", branch: "main" if rails_version == "main"
23
+
24
+ ruby_version = Gem::Version.new(RUBY_VERSION)
25
+
26
+ gem "test-unit", "~> 3.0"
27
+
28
+ if ruby_version < Gem::Version.new('2.5.0')
29
+ gem 'nokogiri', '~> 1.10.10'
30
+ elsif ruby_version < Gem::Version.new('2.6.0')
31
+ gem 'nokogiri', '~> 1.12.5'
21
32
  end
22
33
 
34
+ gem 'loofah', '< 2.21' if ruby_version < Gem::Version.new('2.5.0')
35
+ gem 'psych', '< 5' if rails_version == '7.1' && ruby_version < Gem::Version.new('3.0.0')
36
+
23
37
  group :test, :development do
24
- gem 'sqlite3'
38
+ gem 'ostruct' if ruby_version >= Gem::Version.new('4.0.0')
39
+ case rails_version
40
+ when '5.2', '6.0', '6.1', '7.0'
41
+ gem 'sqlite3', '~> 1.4'
42
+ else
43
+ gem 'sqlite3'
44
+ end
45
+ gem 'sprockets-rails'
25
46
  end
26
47
 
27
48
  group :test do
@@ -0,0 +1,143 @@
1
+ # Migration Guide
2
+
3
+ ## Upgrading from 2.2.0 to 3.0.0
4
+
5
+ Version 3.0.0 brings the gem up to date with current Ruby, Rails and Devise, and
6
+ fixes several bugs that made 2.2.0 unusable on Rails 6.1+, Ruby 3.2+ and
7
+ PostgreSQL. Most apps only need to bump the gem version, but a few behaviours
8
+ changed. Go through the checklist below.
9
+
10
+ ### 1. Check your Ruby and Rails versions
11
+
12
+ - **Rails 5.0 or newer is now required** (it was `>= 3.1.1`). If you are on
13
+ Rails 3.x or 4.x, stay on 2.2.0.
14
+ - The gem is tested on Ruby 2.3 – 4.0 with Rails 5.2, 6.0, 6.1, 7.0, 7.1,
15
+ 7.2, 8.0 and 8.1. Rails 5.0 and 5.1 are allowed by the gemspec but are not
16
+ tested.
17
+
18
+ ### 2. Fix old unversioned migrations
19
+
20
+ Migrations generated by 2.2.0 and earlier inherit from
21
+ `ActiveRecord::Migration` without a version. Rails 5.1+ refuses to run them,
22
+ which breaks `db:migrate` on a fresh database (for example in CI). Add the
23
+ Rails version the migration was written for:
24
+
25
+ ```ruby
26
+ # Before
27
+ class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration
28
+
29
+ # After
30
+ class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration[5.0]
31
+ ```
32
+
33
+ Migrations that have already run are not executed again, so this only matters
34
+ when the database is rebuilt from migrations.
35
+
36
+ The generator (`rails g two_factor_authentication MODEL`) now writes a
37
+ versioned migration. On PostgreSQL it also builds the
38
+ `encrypted_otp_secret_key` index with `algorithm: :concurrently` and
39
+ `disable_ddl_transaction!` so adding it does not lock the table.
40
+
41
+ ### 3. Pass a scope to `is_fully_authenticated?` for non-`:user` models
42
+
43
+ `is_fully_authenticated?` used to read the `:user` warden session no matter
44
+ which model was signed in. For any other model (e.g. `Admin`) it returned
45
+ `true` before the second factor had been entered, so a
46
+ `confirm_two_factor_authenticated` guard did not protect that model.
47
+
48
+ It now checks:
49
+
50
+ 1. the scope of the current Devise controller (`resource_name`), or
51
+ 2. `Devise.default_scope` outside Devise controllers, or
52
+ 3. the scope you pass in.
53
+
54
+ Inside a Devise controller, such as the `RegistrationsController` guard in the
55
+ README, nothing changes for `:user` and other models are now protected. In your
56
+ own controllers, pass the scope explicitly if you have more than one 2FA model,
57
+ or if your default scope is not the model you want to check:
58
+
59
+ ```ruby
60
+ # Before: always checked :user
61
+ return if is_fully_authenticated?
62
+
63
+ # After
64
+ return if is_fully_authenticated?(:admin)
65
+ ```
66
+
67
+ ### 4. Expect `422` from the code form
68
+
69
+ When a submitted code is wrong, or the attempt limit is reached, the
70
+ `update` action now responds with **422 Unprocessable Entity** instead of 200.
71
+ Turbo (the default since Rails 7) ignores a 200 response to a form submission,
72
+ so the error page was never shown.
73
+
74
+ - Update any request or controller specs that expect a 200 after a wrong code.
75
+ - If you override `after_two_factor_fail_for` in a custom controller, render
76
+ with `status: 422` as well:
77
+
78
+ ```ruby
79
+ def after_two_factor_fail_for(resource)
80
+ # ...
81
+ render :show, status: 422
82
+ end
83
+ ```
84
+
85
+ ### 5. Update overridden views
86
+
87
+ If you copied `app/views/devise/two_factor_authentication/show.html.erb` into
88
+ your app, your copy does not get these changes:
89
+
90
+ - **Sign out** now uses `button_to` with `Devise.sign_out_via`.
91
+ `link_to ..., method: :delete` needs `rails-ujs`, which Rails 7+ apps no
92
+ longer ship, so the link sends a GET and fails.
93
+
94
+ ```erb
95
+ <%# Before %>
96
+ <%= link_to "Sign out", destroy_user_session_path, :method => :delete %>
97
+
98
+ <%# After %>
99
+ <%= button_to "Sign out", destroy_user_session_path, :method => :delete %>
100
+ ```
101
+
102
+ - The code field is autofocused: `text_field_tag :code, '', autofocus: true`.
103
+ - The stray `action: :get` option was removed from the "Resend Code" links.
104
+
105
+ ### 6. Check JSON clients
106
+
107
+ When a signed-in user still needs the second factor, a JSON request used to
108
+ get an empty `401`. It now gets a `401` with a body that tells the client
109
+ where to send the user:
110
+
111
+ ```json
112
+ { "redirect_to": "/users/two_factor_authentication" }
113
+ ```
114
+
115
+ The user's return location is set to `root_path(format: :html)`, so **your app
116
+ must define a `root` route** to use this. HTML requests are still redirected,
117
+ and every other format still gets an empty `401`.
118
+
119
+ ### 7. Things that now work without changes
120
+
121
+ These were broken in 2.2.0 and need no action, but you can remove any
122
+ workarounds you added for them:
123
+
124
+ - `create_direct_otp`, `send_new_otp` and `clear_direct_otp` no longer raise
125
+ `NoMethodError` on Rails 6.1+ (they called the removed `update_attributes`).
126
+ - A successful TOTP login no longer fails on PostgreSQL with
127
+ `PG::DatetimeFieldOverflow`. `totp_timestamp` is now stored as a `Time`;
128
+ existing values keep working.
129
+ - The schema helpers (`t.second_factor_attempts_count`, `t.totp_timestamp`,
130
+ ...) work again in `create_table` and `change_table`.
131
+ - `rails g two_factor_authentication MODEL` works on Ruby 3.2+ and no longer
132
+ has to be run from the app root.
133
+ - The routes load on Rails 8.1 (which rejected `resend_code` in `only:`).
134
+ - ROTP 5.0, which renamed `ROTP::Base32.random_base32` to `random`, is
135
+ supported.
136
+ - `User.totp_timestamp` is no longer defined as a class method. It read a
137
+ Devise setting that does not exist and always raised; the
138
+ `totp_timestamp` column on each record is unaffected.
139
+
140
+ ## Upgrading from 1.x to 2.x
141
+
142
+ See [Upgrading from version 1.X to 2.X](README.md#upgrading-from-version-1x-to-2x)
143
+ in the README.