devise-otp 1.1.0 → 2.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.erb_lint.yml +30 -0
  3. data/.rubocop.yml +18 -0
  4. data/CHANGELOG.md +42 -1
  5. data/Gemfile +7 -1
  6. data/README.md +5 -1
  7. data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +30 -7
  8. data/app/controllers/devise_otp/devise/otp_persistence_controller.rb +53 -0
  9. data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +2 -37
  10. data/app/views/devise/otp_credentials/refresh.html.erb +6 -6
  11. data/app/views/devise/otp_credentials/show.html.erb +10 -13
  12. data/app/views/devise/otp_tokens/_token_secret.html.erb +9 -9
  13. data/app/views/devise/otp_tokens/_trusted_devices.html.erb +7 -7
  14. data/app/views/devise/otp_tokens/edit.html.erb +9 -9
  15. data/app/views/devise/otp_tokens/recovery.html.erb +6 -6
  16. data/app/views/devise/otp_tokens/show.html.erb +6 -6
  17. data/bin/erb_lint +27 -0
  18. data/bin/rubocop +27 -0
  19. data/config/locales/en.yml +9 -7
  20. data/devise-otp.gemspec +3 -1
  21. data/lib/devise/strategies/database_authenticatable.rb +4 -17
  22. data/lib/devise-otp/version.rb +1 -1
  23. data/lib/devise-otp.rb +0 -1
  24. data/lib/devise_otp_authenticatable/controllers/helpers.rb +1 -2
  25. data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +1 -2
  26. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +7 -2
  27. data/lib/devise_otp_authenticatable/hooks/refreshable.rb +0 -1
  28. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +18 -12
  29. data/lib/devise_otp_authenticatable/routes.rb +7 -7
  30. data/test/dummy/app/controllers/admin_posts_controller.rb +0 -1
  31. data/test/dummy/app/controllers/base_controller.rb +0 -2
  32. data/test/dummy/app/controllers/non_otp_posts_controller.rb +0 -1
  33. data/test/dummy/app/models/admin.rb +0 -1
  34. data/test/dummy/app/models/lockable_user.rb +8 -0
  35. data/test/dummy/app/models/non_otp_user.rb +0 -1
  36. data/test/dummy/app/models/rememberable_user.rb +8 -0
  37. data/test/dummy/app/views/layouts/application.html.erb +4 -2
  38. data/test/dummy/app/views/posts/show.html.erb +0 -1
  39. data/test/dummy/app/views/shared/_navbar.html.erb +15 -0
  40. data/test/dummy/config/application.rb +3 -0
  41. data/test/dummy/config/initializers/devise.rb +2 -2
  42. data/test/dummy/config/routes.rb +3 -0
  43. data/test/dummy/db/migrate/20250731000001_create_lockable_users.rb +9 -0
  44. data/test/dummy/db/migrate/20250731000002_add_devise_to_lockable_users.rb +52 -0
  45. data/test/dummy/db/migrate/20250731000003_devise_otp_add_to_lockable_users.rb +28 -0
  46. data/test/dummy/db/migrate/20250817221304_create_rememberable_users.rb +50 -0
  47. data/test/dummy/db/migrate/20250818030305_add_devise_otp_to_rememberable_users.rb +28 -0
  48. data/test/dummy/db/schema.rb +67 -1
  49. data/test/integration/disable_token_test.rb +0 -2
  50. data/test/integration/enable_otp_form_test.rb +12 -1
  51. data/test/integration/lockable_test.rb +143 -0
  52. data/test/integration/non_otp_user_models_test.rb +0 -2
  53. data/test/integration/otp_drift_test.rb +35 -0
  54. data/test/integration/persistence_test.rb +59 -4
  55. data/test/integration/refresh_test.rb +23 -14
  56. data/test/integration/rememberable_test.rb +143 -0
  57. data/test/integration/reset_token_test.rb +0 -2
  58. data/test/integration/sign_in_test.rb +15 -8
  59. data/test/integration/trackable_test.rb +0 -2
  60. data/test/integration_tests_helper.rb +22 -1
  61. data/test/models/otp_authenticatable_test.rb +48 -11
  62. data/test/test_helper.rb +1 -0
  63. metadata +34 -4
@@ -6,6 +6,10 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
6
6
  new_user
7
7
  end
8
8
 
9
+ def teardown
10
+ Timecop.return
11
+ end
12
+
9
13
  test "new users do not have a secret set" do
10
14
  user = User.first
11
15
 
@@ -15,7 +19,7 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
15
19
  end
16
20
 
17
21
  test "new users have OTP disabled by default" do
18
- assert !User.first.otp_enabled
22
+ assert_not User.first.otp_enabled
19
23
  end
20
24
 
21
25
  test "populating otp secrets should populate all required fields" do
@@ -42,8 +46,8 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
42
46
  user.enable_otp!
43
47
  user.generate_otp_challenge!
44
48
  user.update(
45
- :otp_failed_attempts => 1,
46
- :otp_recovery_counter => 1
49
+ otp_failed_attempts: 1,
50
+ otp_recovery_counter: 1
47
51
  )
48
52
 
49
53
 
@@ -75,7 +79,7 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
75
79
 
76
80
  u.reset_otp_persistence!
77
81
  assert(otp_auth_secret == u.otp_auth_secret)
78
- assert !(otp_persistence_seed == u.otp_persistence_seed)
82
+ assert_not (otp_persistence_seed == u.otp_persistence_seed)
79
83
  assert u.otp_enabled
80
84
  end
81
85
 
@@ -95,7 +99,8 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
95
99
  u.populate_otp_secrets!
96
100
  u.enable_otp!
97
101
  challenge = u.generate_otp_challenge!(1.second)
98
- sleep(2)
102
+
103
+ Timecop.travel(Time.now + 2)
99
104
 
100
105
  w = User.find_valid_otp_challenge(challenge)
101
106
  assert_nil w
@@ -106,7 +111,7 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
106
111
  u.populate_otp_secrets!
107
112
  u.enable_otp!
108
113
  challenge = u.generate_otp_challenge!(1.second)
109
- sleep(2)
114
+ Timecop.travel(Time.now + 2)
110
115
  assert_equal false, u.otp_challenge_valid?
111
116
  end
112
117
 
@@ -129,17 +134,49 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
129
134
  assert_equal true, u.validate_otp_token(token)
130
135
  end
131
136
 
132
- test "generated otp token, out of drift window, should be NOT valid for the user" do
137
+ test "generated otp token within the drift window should be valid for the user" do
133
138
  u = User.first
134
139
  u.populate_otp_secrets!
135
140
  u.enable_otp!
136
141
 
137
142
  secret = u.otp_auth_secret
143
+ token = ROTP::TOTP.new(secret).at(Time.now)
138
144
 
139
- [3.minutes.from_now, 3.minutes.ago].each do |time|
140
- token = ROTP::TOTP.new(secret).at(time)
141
- assert_equal false, u.valid_otp_token?(token)
142
- end
145
+ Timecop.freeze(Time.now + 90)
146
+ assert_equal true, u.valid_otp_token?(token)
147
+
148
+ Timecop.return
149
+ Timecop.freeze(Time.now - 90)
150
+ assert_equal true, u.valid_otp_token?(token)
151
+ end
152
+
153
+ test "generated otp token outside of drift window should NOT be valid for the user" do
154
+ # Since the otp_drift_window defines steps (not just time), and these steps
155
+ # begin at the 30 and 60 second marks of each minute, it is possible for
156
+ # an OTP token to be used up to 119 seconds after generation with a 3 step
157
+ # drift window. For example, a token generated at 12:00:00PM could be used
158
+ # within the timeframe for any of the following steps:
159
+ # - 12:00:00~12:00:29 (current step)
160
+ # - 12:00:30~12:00:59 (drift of 1 step)
161
+ # - 12:01:00~12:00:29 (drift of 2 steps)
162
+ # - 12:01:30~12:01:59 (drift of 3 steps)
163
+ #
164
+ # As a result, we need to test for 120 seconds to ensure that the test
165
+ # always passes.
166
+
167
+ u = User.first
168
+ u.populate_otp_secrets!
169
+ u.enable_otp!
170
+
171
+ secret = u.otp_auth_secret
172
+ token = ROTP::TOTP.new(secret).at(Time.now)
173
+
174
+ Timecop.freeze(Time.now + 120)
175
+ assert_equal false, u.valid_otp_token?(token)
176
+
177
+ Timecop.return
178
+ Timecop.freeze(Time.now - 120)
179
+ assert_equal false, u.valid_otp_token?(token)
143
180
  end
144
181
 
145
182
  test "recovery secrets should be valid, and valid only once" do
data/test/test_helper.rb CHANGED
@@ -4,6 +4,7 @@ require "dummy/config/environment"
4
4
  require "rails/test_help"
5
5
  require "capybara/rails"
6
6
  require "minitest/reporters"
7
+ require "timecop"
7
8
 
8
9
  Minitest::Reporters.use!
9
10
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-otp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lele Forzani
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2025-07-20 00:00:00.000000000 Z
13
+ date: 2025-10-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -66,14 +66,28 @@ dependencies:
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '2.0'
69
+ version: '3.0'
70
70
  type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
73
  requirements:
74
74
  - - "~>"
75
75
  - !ruby/object:Gem::Version
76
- version: '2.0'
76
+ version: '3.0'
77
+ - !ruby/object:Gem::Dependency
78
+ name: timecop
79
+ requirement: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 0.9.10
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 0.9.10
77
91
  description: OTP authentication for Devise
78
92
  email:
79
93
  - lele@windmill.it
@@ -83,8 +97,10 @@ executables: []
83
97
  extensions: []
84
98
  extra_rdoc_files: []
85
99
  files:
100
+ - ".erb_lint.yml"
86
101
  - ".github/workflows/ci.yml"
87
102
  - ".gitignore"
103
+ - ".rubocop.yml"
88
104
  - Appraisals
89
105
  - CHANGELOG.md
90
106
  - Gemfile
@@ -93,6 +109,7 @@ files:
93
109
  - Rakefile
94
110
  - app/assets/stylesheets/devise-otp.css
95
111
  - app/controllers/devise_otp/devise/otp_credentials_controller.rb
112
+ - app/controllers/devise_otp/devise/otp_persistence_controller.rb
96
113
  - app/controllers/devise_otp/devise/otp_tokens_controller.rb
97
114
  - app/views/devise/otp_credentials/refresh.html.erb
98
115
  - app/views/devise/otp_credentials/show.html.erb
@@ -102,6 +119,8 @@ files:
102
119
  - app/views/devise/otp_tokens/recovery.html.erb
103
120
  - app/views/devise/otp_tokens/recovery_codes.text.erb
104
121
  - app/views/devise/otp_tokens/show.html.erb
122
+ - bin/erb_lint
123
+ - bin/rubocop
105
124
  - config/locales/en.yml
106
125
  - devise-otp.gemspec
107
126
  - gemfiles/rails_7.1.gemfile
@@ -136,8 +155,10 @@ files:
136
155
  - test/dummy/app/helpers/posts_helper.rb
137
156
  - test/dummy/app/mailers/.gitkeep
138
157
  - test/dummy/app/models/admin.rb
158
+ - test/dummy/app/models/lockable_user.rb
139
159
  - test/dummy/app/models/non_otp_user.rb
140
160
  - test/dummy/app/models/post.rb
161
+ - test/dummy/app/models/rememberable_user.rb
141
162
  - test/dummy/app/models/user.rb
142
163
  - test/dummy/app/views/admin_posts/index.html.erb
143
164
  - test/dummy/app/views/base/home.html.erb
@@ -148,6 +169,7 @@ files:
148
169
  - test/dummy/app/views/posts/index.html.erb
149
170
  - test/dummy/app/views/posts/new.html.erb
150
171
  - test/dummy/app/views/posts/show.html.erb
172
+ - test/dummy/app/views/shared/_navbar.html.erb
151
173
  - test/dummy/config.ru
152
174
  - test/dummy/config/application.rb
153
175
  - test/dummy/config/boot.rb
@@ -174,6 +196,11 @@ files:
174
196
  - test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb
175
197
  - test/dummy/db/migrate/20250718092451_create_non_otp_users.rb
176
198
  - test/dummy/db/migrate/20250718092536_add_devise_to_non_otp_users.rb
199
+ - test/dummy/db/migrate/20250731000001_create_lockable_users.rb
200
+ - test/dummy/db/migrate/20250731000002_add_devise_to_lockable_users.rb
201
+ - test/dummy/db/migrate/20250731000003_devise_otp_add_to_lockable_users.rb
202
+ - test/dummy/db/migrate/20250817221304_create_rememberable_users.rb
203
+ - test/dummy/db/migrate/20250818030305_add_devise_otp_to_rememberable_users.rb
177
204
  - test/dummy/db/schema.rb
178
205
  - test/dummy/db/seeds.rb
179
206
  - test/dummy/lib/assets/.gitkeep
@@ -184,9 +211,12 @@ files:
184
211
  - test/dummy/script/rails
185
212
  - test/integration/disable_token_test.rb
186
213
  - test/integration/enable_otp_form_test.rb
214
+ - test/integration/lockable_test.rb
187
215
  - test/integration/non_otp_user_models_test.rb
216
+ - test/integration/otp_drift_test.rb
188
217
  - test/integration/persistence_test.rb
189
218
  - test/integration/refresh_test.rb
219
+ - test/integration/rememberable_test.rb
190
220
  - test/integration/reset_token_test.rb
191
221
  - test/integration/sign_in_test.rb
192
222
  - test/integration/trackable_test.rb