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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +77 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +37 -2
- data/Gemfile +29 -8
- data/MIGRATION_GUIDE.md +143 -0
- data/README.md +145 -93
- data/app/controllers/devise/two_factor_authentication_controller.rb +3 -2
- data/app/views/devise/two_factor_authentication/show.html.erb +4 -4
- data/config/locales/de.yml +8 -0
- data/lib/generators/active_record/templates/migration.rb +9 -1
- data/lib/generators/active_record/two_factor_authentication_generator.rb +18 -0
- data/lib/generators/two_factor_authentication/two_factor_authentication_generator.rb +1 -1
- data/lib/two_factor_authentication/controllers/helpers.rb +7 -3
- data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +8 -5
- data/lib/two_factor_authentication/routes.rb +1 -1
- data/lib/two_factor_authentication/schema.rb +7 -7
- data/lib/two_factor_authentication/version.rb +1 -1
- data/spec/controllers/two_factor_authentication_controller_spec.rb +84 -7
- data/spec/features/two_factor_authenticatable_spec.rb +119 -14
- data/spec/generators/active_record/two_factor_authentication_generator_spec.rb +52 -4
- data/spec/generators/two_factor_authentication/two_factor_authentication_generator_spec.rb +37 -0
- data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +89 -7
- data/spec/lib/two_factor_authentication/schema_spec.rb +41 -0
- data/spec/rails_app/app/assets/config/manifest.js +2 -0
- data/spec/rails_app/app/controllers/home_controller.rb +6 -1
- data/spec/rails_app/app/models/guest_user.rb +1 -1
- data/spec/rails_app/app/models/secure_user.rb +13 -0
- data/spec/rails_app/config/application.rb +1 -1
- data/spec/rails_app/config/environments/test.rb +3 -8
- data/spec/rails_app/config/initializers/inflections.rb +4 -0
- data/spec/rails_app/config/routes.rb +2 -0
- data/spec/rails_app/db/migrate/20260929000000_add_otp_columns_to_users.rb +8 -0
- data/spec/rails_app/db/migrate/20260929000001_devise_create_secure_users.rb +22 -0
- data/spec/rails_app/db/schema.rb +26 -8
- data/spec/rails_app/lib/sms_provider.rb +2 -4
- data/spec/requests/two_factor_authentication_helpers_spec.rb +48 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/authenticated_model_helper.rb +5 -30
- data/spec/support/controller_helper.rb +5 -4
- data/spec/support/features_spec_helper.rb +1 -0
- data/two_factor_authentication.gemspec +4 -5
- metadata +43 -26
- data/.travis.yml +0 -28
- data/spec/rails_app/config/initializers/secret_token.rb +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3f83f45de8b5992a3311c7d0d47479f7b7121c7460925fa8661d8b7eb98f4f72
|
|
4
|
+
data.tar.gz: 4df0c76ae151691cec84f1d036d79b93094826c892d01edef5d0a847a92407af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
## [
|
|
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
|
-
[
|
|
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
|
|
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 "
|
|
10
|
-
{github: "rails/rails"}
|
|
9
|
+
when "main"
|
|
10
|
+
{github: "rails/rails", branch: "main"}
|
|
11
11
|
when "default"
|
|
12
|
-
"~>
|
|
12
|
+
"~> 8.1.0"
|
|
13
13
|
else
|
|
14
|
-
"
|
|
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
|
-
|
|
20
|
-
|
|
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 '
|
|
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
|
data/MIGRATION_GUIDE.md
ADDED
|
@@ -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.
|