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
@@ -2,7 +2,6 @@ require "test_helper"
2
2
  require "integration_tests_helper"
3
3
 
4
4
  class DisableTokenTest < ActionDispatch::IntegrationTest
5
-
6
5
  def setup
7
6
  # log in 1fa
8
7
  @user = enable_otp_and_sign_in
@@ -52,5 +51,4 @@ class DisableTokenTest < ActionDispatch::IntegrationTest
52
51
  assert_not_nil @user.otp_recovery_secret
53
52
  assert_equal @user.otp_recovery_secret, recovery_secret
54
53
  end
55
-
56
54
  end
@@ -48,7 +48,7 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
48
48
 
49
49
  visit "/"
50
50
  within "#alerts" do
51
- assert !page.has_content?('The Confirmation Code you entered did not match the QR code shown below.')
51
+ assert_not page.has_content?('The Confirmation Code you entered did not match the QR code shown below.')
52
52
  end
53
53
  end
54
54
 
@@ -71,4 +71,15 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
71
71
  assert_not user.otp_enabled?
72
72
  end
73
73
 
74
+ test "failed confirmation code should return a 422 'unprocessable entity' status" do
75
+ user = sign_user_in
76
+
77
+ visit edit_user_otp_token_path
78
+
79
+ fill_in "confirmation_code", with: "123456"
80
+
81
+ click_button "Continue..."
82
+
83
+ assert_equal 422, page.status_code
84
+ end
74
85
  end
@@ -0,0 +1,143 @@
1
+ require "test_helper"
2
+ require "integration_tests_helper"
3
+
4
+ class SignInTest < ActionDispatch::IntegrationTest
5
+ def setup
6
+ @lockable_user = create_lockable_user
7
+ @lockable_user.populate_otp_secrets!
8
+ @lockable_user.update(otp_enabled: true)
9
+
10
+ sign_user_in(@lockable_user)
11
+ assert_equal lockable_user_otp_credential_path, current_path
12
+ end
13
+
14
+ def teardown
15
+ Capybara.reset_sessions!
16
+ end
17
+
18
+ test "a normal User should not get locked out for entering incorrect OTP tokens" do
19
+ enable_otp_and_sign_in
20
+
21
+ 6.times do
22
+ fill_in "token", with: "123456"
23
+ click_button "Submit Token"
24
+ end
25
+
26
+ assert page.has_content? "The token you provided was invalid."
27
+ end
28
+
29
+ test "a Lockable User should increment failed_attempts for each incorrect OTP token" do
30
+ fill_in "token", with: "123456"
31
+ click_button "Submit Token"
32
+ assert page.has_content? "The token you provided was invalid."
33
+
34
+ @lockable_user.reload
35
+ assert_equal 1, @lockable_user.failed_attempts
36
+
37
+ fill_in "token", with: "123456"
38
+ click_button "Submit Token"
39
+ assert page.has_content? "The token you provided was invalid."
40
+
41
+ @lockable_user.reload
42
+ assert_equal 2, @lockable_user.failed_attempts
43
+ end
44
+
45
+ test "a Lockable User should reset failed_attempts after a correct OTP token" do
46
+ fill_in "token", with: ROTP::TOTP.new(@lockable_user.otp_auth_secret).at(Time.now)
47
+ click_button "Submit Token"
48
+ assert_equal root_path, current_path
49
+
50
+ @lockable_user.reload
51
+ assert_equal 0, @lockable_user.failed_attempts
52
+ end
53
+
54
+ test "a Lockable User should get locked out for entering incorrect OTP token too many times" do
55
+ # Enter incorrect token
56
+ 5.times do
57
+ fill_in "token", with: "123456"
58
+ click_button "Submit Token"
59
+ end
60
+
61
+ @lockable_user.reload
62
+ assert @lockable_user.access_locked?
63
+ end
64
+
65
+ test "a locked out user should be redirected to the sign in form" do
66
+ # Enter incorrect token
67
+ 5.times do
68
+ fill_in "token", with: "123456"
69
+ click_button "Submit Token"
70
+ end
71
+
72
+ assert_equal new_lockable_user_session_path, current_path
73
+ end
74
+
75
+ test "a locked out user should see the default 'Your account is locked' message" do
76
+ # Enter incorrect token
77
+ 5.times do
78
+ fill_in "token", with: "123456"
79
+ click_button "Submit Token"
80
+ end
81
+
82
+ assert page.has_content? "Your account is locked."
83
+ end
84
+
85
+ test "the OTP credentials form should display the 'one more attempt' message before being locked out" do
86
+ # Enter incorrect token
87
+ 4.times do
88
+ fill_in "token", with: "123456"
89
+ click_button "Submit Token"
90
+ end
91
+
92
+ assert page.has_content? "You have one more attempt"
93
+ end
94
+
95
+ test "the OTP credentials form should not work for a locked out user (in case of URL revisit)" do
96
+ # Save challenge path
97
+ uri = URI.parse(current_url)
98
+ challenge_path = "#{uri.path}?#{uri.query}"
99
+
100
+ # Enter incorrect token
101
+ 5.times do
102
+ fill_in "token", with: "123456"
103
+ click_button "Submit Token"
104
+ end
105
+
106
+ visit challenge_path
107
+
108
+ # Enter correct token
109
+ fill_in "token", with: ROTP::TOTP.new(@lockable_user.otp_auth_secret).at(Time.now)
110
+ click_button "Submit Token"
111
+
112
+ assert page.has_content? "Your account is locked."
113
+ end
114
+
115
+ test "manually revisiting the sign_in form should not reset the failed attempts for the OTP form" do
116
+ # Enter incorrect token
117
+ 4.times do
118
+ fill_in "token", with: "123456"
119
+ click_button "Submit Token"
120
+ end
121
+
122
+ @lockable_user.reload
123
+ assert_equal 4, @lockable_user.failed_attempts
124
+
125
+ # Attempt to reset failed attempts via sign_in form
126
+ reset!
127
+ visit posts_path
128
+ assert_equal new_user_session_path, current_path
129
+ sign_user_in(@lockable_user)
130
+
131
+ @lockable_user.reload
132
+ assert_equal 4, @lockable_user.failed_attempts
133
+
134
+ # Enter incorrect token again
135
+ fill_in "token", with: "123456"
136
+ click_button "Submit Token"
137
+
138
+ @lockable_user.reload
139
+ assert_equal 5, @lockable_user.failed_attempts
140
+
141
+ assert page.has_content? "Your account is locked."
142
+ end
143
+ end
@@ -2,7 +2,6 @@ require "test_helper"
2
2
  require "integration_tests_helper"
3
3
 
4
4
  class NonOtpUserModelsTest < ActionDispatch::IntegrationTest
5
-
6
5
  def teardown
7
6
  Capybara.reset_sessions!
8
7
  end
@@ -17,5 +16,4 @@ class NonOtpUserModelsTest < ActionDispatch::IntegrationTest
17
16
 
18
17
  assert_equal non_otp_posts_path, current_path
19
18
  end
20
-
21
19
  end
@@ -0,0 +1,35 @@
1
+ require "test_helper"
2
+ require "integration_tests_helper"
3
+
4
+ class OtpDriftTest < ActionDispatch::IntegrationTest
5
+ def teardown
6
+ Capybara.reset_sessions!
7
+ Timecop.return
8
+ end
9
+
10
+ test "should allow OTP token usage up within the OTP drift window" do
11
+ user = enable_otp_and_sign_in
12
+
13
+ copied_token = ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
14
+
15
+ Timecop.freeze(Time.now + 90)
16
+
17
+ fill_in "token", with: copied_token
18
+ click_button "Submit Token"
19
+
20
+ assert_equal root_path, current_path
21
+ end
22
+
23
+ test "should not allow OTP token usage beyond the OTP drift window" do
24
+ user = enable_otp_and_sign_in
25
+
26
+ copied_token = ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
27
+
28
+ Timecop.freeze(Time.now + 120)
29
+
30
+ fill_in "token", with: copied_token
31
+ click_button "Submit Token"
32
+
33
+ assert_not_equal root_path, current_path
34
+ end
35
+ end
@@ -10,6 +10,7 @@ class PersistenceTest < ActionDispatch::IntegrationTest
10
10
  def teardown
11
11
  User.otp_trust_persistence = @old_persistence
12
12
  Capybara.reset_sessions!
13
+ Timecop.return
13
14
  end
14
15
 
15
16
  test "a user should be requested the otp challenge every log in" do
@@ -34,7 +35,7 @@ class PersistenceTest < ActionDispatch::IntegrationTest
34
35
  visit user_otp_token_path
35
36
  assert_equal user_otp_token_path, current_path
36
37
 
37
- click_link("Trust this browser")
38
+ click_button("Trust this browser")
38
39
  assert_text "Your browser is trusted."
39
40
  within "#alerts" do
40
41
  assert page.has_content? 'Your device is now trusted.'
@@ -46,6 +47,46 @@ class PersistenceTest < ActionDispatch::IntegrationTest
46
47
  assert_equal root_path, current_path
47
48
  end
48
49
 
50
+ test "a user should be able to remove their browser" do
51
+ # log in 1fa
52
+ user = enable_otp_and_sign_in
53
+ otp_challenge_for user
54
+
55
+ original_persistence_seed = user.otp_persistence_seed
56
+
57
+ visit user_otp_token_path
58
+ click_button("Trust this browser")
59
+ click_button("Remove this browser from the list of trusted browsers")
60
+
61
+ assert_text "Your browser is not trusted."
62
+ # Test that the OTP Persistence Seed is still valid
63
+ assert_equal user.reload.otp_persistence_seed, original_persistence_seed
64
+
65
+ sign_out
66
+ sign_user_in
67
+ assert_equal user_otp_credential_path, current_path
68
+ end
69
+
70
+ test "a user should be able to clear all trusted browsers" do
71
+ # log in 1fa
72
+ user = enable_otp_and_sign_in
73
+ otp_challenge_for user
74
+
75
+ original_persistence_seed = user.otp_persistence_seed
76
+
77
+ visit user_otp_token_path
78
+ click_button("Trust this browser")
79
+ click_button("Clear the list of trusted browsers")
80
+
81
+ assert_text "Your browser is not trusted."
82
+ # Test that the OTP Persistence Seed was reset
83
+ assert_not_equal user.reload.otp_persistence_seed, original_persistence_seed
84
+
85
+ sign_out
86
+ sign_user_in
87
+ assert_equal user_otp_credential_path, current_path
88
+ end
89
+
49
90
  test "a user should be able to download its recovery codes" do
50
91
  # log in 1fa
51
92
  user = enable_otp_and_sign_in
@@ -68,13 +109,27 @@ class PersistenceTest < ActionDispatch::IntegrationTest
68
109
  visit user_otp_token_path
69
110
  assert_equal user_otp_token_path, current_path
70
111
 
71
- click_link("Trust this browser")
112
+ click_button("Trust this browser")
72
113
  assert_text "Your browser is trusted."
73
114
  sign_out
74
115
 
75
- sleep User.otp_trust_persistence.to_i + 1
76
- sign_user_in
116
+ Timecop.travel(Time.now + 30.days)
77
117
 
118
+ sign_user_in
78
119
  assert_equal user_otp_credential_path, current_path
79
120
  end
121
+
122
+ test "a user should be prompted for credentials if the credentials_refresh time is expired" do
123
+ # log in 1fa
124
+ user = enable_otp_and_sign_in
125
+ otp_challenge_for user
126
+
127
+ visit user_otp_token_path
128
+ assert_equal user_otp_token_path, current_path
129
+
130
+ Timecop.travel(Time.now + 15.minutes)
131
+
132
+ click_button("Trust this browser")
133
+ assert_equal refresh_user_otp_credential_path, current_path
134
+ end
80
135
  end
@@ -2,16 +2,9 @@ require "test_helper"
2
2
  require "integration_tests_helper"
3
3
 
4
4
  class RefreshTest < ActionDispatch::IntegrationTest
5
- def setup
6
- @old_refresh = User.otp_credentials_refresh
7
- User.otp_credentials_refresh = 1.second
8
- Admin.otp_credentials_refresh = 1.second
9
- end
10
-
11
5
  def teardown
12
- User.otp_credentials_refresh = @old_refresh
13
- Admin.otp_credentials_refresh = @old_refresh
14
6
  Capybara.reset_sessions!
7
+ Timecop.return
15
8
  end
16
9
 
17
10
  test "a user that just signed in should be able to access their OTP settings without refreshing" do
@@ -26,7 +19,7 @@ class RefreshTest < ActionDispatch::IntegrationTest
26
19
  visit user_otp_token_path
27
20
  assert_equal user_otp_token_path, current_path
28
21
 
29
- sleep(2)
22
+ Timecop.travel(Time.now + 15.minutes)
30
23
 
31
24
  visit user_otp_token_path
32
25
  assert_equal refresh_user_otp_credential_path, current_path
@@ -37,7 +30,7 @@ class RefreshTest < ActionDispatch::IntegrationTest
37
30
  visit user_otp_token_path
38
31
  assert_equal user_otp_token_path, current_path
39
32
 
40
- sleep(2)
33
+ Timecop.travel(Time.now + 15.minutes)
41
34
 
42
35
  visit user_otp_token_path
43
36
  assert_equal refresh_user_otp_credential_path, current_path
@@ -52,7 +45,7 @@ class RefreshTest < ActionDispatch::IntegrationTest
52
45
  visit user_otp_token_path
53
46
  assert_equal user_otp_token_path, current_path
54
47
 
55
- sleep(2)
48
+ Timecop.travel(Time.now + 15.minutes)
56
49
 
57
50
  visit user_otp_token_path
58
51
  assert_equal refresh_user_otp_credential_path, current_path
@@ -67,14 +60,15 @@ class RefreshTest < ActionDispatch::IntegrationTest
67
60
 
68
61
  visit "/"
69
62
  within "#alerts" do
70
- assert !page.has_content?('Sorry, you provided the wrong credentials.')
63
+ assert_not page.has_content?('Sorry, you provided the wrong credentials.')
71
64
  end
72
65
  end
73
66
 
74
67
  test "user should be finally be able to access their settings, and just password is enough" do
75
68
  user = enable_otp_and_sign_in_with_otp
76
69
 
77
- sleep(2)
70
+ Timecop.travel(Time.now + 15.minutes)
71
+
78
72
  visit user_otp_token_path
79
73
  assert_equal refresh_user_otp_credential_path, current_path
80
74
 
@@ -102,7 +96,7 @@ class RefreshTest < ActionDispatch::IntegrationTest
102
96
  click_button "Submit Token"
103
97
  assert_equal "/", current_path
104
98
 
105
- sleep(2)
99
+ Timecop.travel(Time.now + 15.minutes)
106
100
 
107
101
  visit admin_otp_token_path
108
102
  assert_equal refresh_admin_otp_credential_path, current_path
@@ -113,4 +107,19 @@ class RefreshTest < ActionDispatch::IntegrationTest
113
107
  assert_equal admin_otp_token_path, current_path
114
108
  end
115
109
 
110
+ test "failed credentials should return a 422 'unprocessable entity' status" do
111
+ sign_user_in
112
+ visit user_otp_token_path
113
+ assert_equal user_otp_token_path, current_path
114
+
115
+ Timecop.travel(Time.now + 15.minutes)
116
+
117
+ visit user_otp_token_path
118
+ assert_equal refresh_user_otp_credential_path, current_path
119
+
120
+ fill_in "user_refresh_password", with: "12345670"
121
+ click_button "Continue..."
122
+
123
+ assert_equal 422, page.status_code
124
+ end
116
125
  end
@@ -0,0 +1,143 @@
1
+ require "test_helper"
2
+ require "integration_tests_helper"
3
+
4
+ class RememberableTest < ActionDispatch::IntegrationTest
5
+ def setup
6
+ @rememberable_user = create_rememberable_user
7
+ @rememberable_user.populate_otp_secrets!
8
+ @rememberable_user.enable_otp!
9
+ end
10
+
11
+ def teardown
12
+ Capybara.reset_sessions!
13
+ Timecop.return
14
+ end
15
+
16
+ test "checking remember_me at the sign in page persists the selection to the OTP credentials page" do
17
+ visit new_rememberable_user_session_path
18
+
19
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
20
+ fill_in "rememberable_user_password", with: "12345678"
21
+ check "Remember me"
22
+ click_button("Log in")
23
+
24
+ assert_equal rememberable_user_otp_credential_path, current_path
25
+
26
+ assert_equal "true", find("#remember_me", visible: false).value
27
+ end
28
+
29
+ test "checking remember me during the sign in process with OTP enabled remembers the user" do
30
+ visit new_rememberable_user_session_path
31
+
32
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
33
+ fill_in "rememberable_user_password", with: "12345678"
34
+ check "Remember me"
35
+ click_button("Log in")
36
+
37
+ assert_equal rememberable_user_otp_credential_path, current_path
38
+
39
+ fill_in "token", with: ROTP::TOTP.new(@rememberable_user.otp_auth_secret).at(Time.now)
40
+ click_button("Submit Token")
41
+
42
+ assert current_path, "/"
43
+
44
+ assert page.driver.browser.last_request.cookies['remember_rememberable_user_token']
45
+ end
46
+
47
+ test "not checking remember me during the sign in process does not remember the user" do
48
+ visit new_rememberable_user_session_path
49
+
50
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
51
+ fill_in "rememberable_user_password", with: "12345678"
52
+ click_button("Log in")
53
+
54
+ assert_equal rememberable_user_otp_credential_path, current_path
55
+
56
+ fill_in "token", with: ROTP::TOTP.new(@rememberable_user.otp_auth_secret).at(Time.now)
57
+ click_button("Submit Token")
58
+
59
+ assert current_path, "/"
60
+
61
+ assert_nil page.driver.browser.last_request.cookies['remember_rememberable_user_token']
62
+ end
63
+
64
+ test "the OTP credentials page persists the remember_me value through any reloads" do
65
+ visit new_rememberable_user_session_path
66
+
67
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
68
+ fill_in "rememberable_user_password", with: "12345678"
69
+ check "Remember me"
70
+ click_button("Log in")
71
+
72
+ assert_equal rememberable_user_otp_credential_path, current_path
73
+
74
+ fill_in "token", with: "123456"
75
+ click_button("Submit Token")
76
+
77
+ assert_equal rememberable_user_otp_credential_path, current_path
78
+
79
+ assert_equal "true", find("#remember_me", visible: false).value
80
+ end
81
+
82
+ test "checking remember_me at the sign in page does not remember the user until sign in is completed" do
83
+ visit new_rememberable_user_session_path
84
+
85
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
86
+ fill_in "rememberable_user_password", with: "12345678"
87
+ check "Remember me"
88
+ click_button("Log in")
89
+
90
+ assert_equal rememberable_user_otp_credential_path, current_path
91
+
92
+ assert_nil page.driver.browser.last_request.cookies['remember_rememberable_user_token']
93
+ end
94
+
95
+ test "rememberable users without OTP enabled are remembered immediately" do
96
+ @rememberable_user.disable_otp!
97
+
98
+ visit new_rememberable_user_session_path
99
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
100
+ fill_in "rememberable_user_password", with: "12345678"
101
+ check "Remember me"
102
+ click_button("Log in")
103
+
104
+ assert page.driver.browser.last_request.cookies['remember_rememberable_user_token']
105
+ end
106
+
107
+ test "rememberable users with browser persistence enabled are still remembered when signing in" do
108
+ visit new_rememberable_user_session_path
109
+
110
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
111
+ fill_in "rememberable_user_password", with: "12345678"
112
+ check "Remember me"
113
+ click_button("Log in")
114
+
115
+ fill_in "token", with: ROTP::TOTP.new(@rememberable_user.otp_auth_secret).at(Time.now)
116
+ click_button("Submit Token")
117
+
118
+ visit rememberable_user_otp_token_path
119
+ click_button "Trust this browser"
120
+ click_button("Sign Out")
121
+
122
+ visit new_rememberable_user_session_path
123
+ fill_in "rememberable_user_email", with: "rememberable-user@email.invalid"
124
+ fill_in "rememberable_user_password", with: "12345678"
125
+ check "Remember me"
126
+ click_button("Log in")
127
+
128
+ assert page.driver.browser.last_request.cookies['remember_rememberable_user_token']
129
+ end
130
+
131
+ test "normal users without rememberable strategy are not affected" do
132
+ create_full_user
133
+ visit new_user_session_path
134
+
135
+ assert_not page.has_content? "Remember me"
136
+
137
+ fill_in "user_email", with: "user@email.invalid"
138
+ fill_in "user_password", with: "12345678"
139
+ click_button("Log in")
140
+
141
+ assert_nil page.driver.browser.last_request.cookies['remember_rememberable_user_token']
142
+ end
143
+ end
@@ -2,7 +2,6 @@ require "test_helper"
2
2
  require "integration_tests_helper"
3
3
 
4
4
  class ResetTokenTest < ActionDispatch::IntegrationTest
5
-
6
5
  def setup
7
6
  # log in 1fa
8
7
  @user = enable_otp_and_sign_in
@@ -44,5 +43,4 @@ class ResetTokenTest < ActionDispatch::IntegrationTest
44
43
  assert_not_nil @user.otp_recovery_secret
45
44
  assert_not_equal @user.otp_recovery_secret, recovery_secret
46
45
  end
47
-
48
46
  end
@@ -4,6 +4,7 @@ require "integration_tests_helper"
4
4
  class SignInTest < ActionDispatch::IntegrationTest
5
5
  def teardown
6
6
  Capybara.reset_sessions!
7
+ Timecop.return
7
8
  end
8
9
 
9
10
  test "a new user should be able to sign in without using their token" do
@@ -57,6 +58,16 @@ class SignInTest < ActionDispatch::IntegrationTest
57
58
  assert page.has_content? "You need to type in the token you generated with your device."
58
59
  end
59
60
 
61
+ test "failed authentication should return a 422 'unprocessable entity' status" do
62
+ enable_otp_and_sign_in
63
+ assert_equal user_otp_credential_path, current_path
64
+
65
+ fill_in "token", with: "123456"
66
+ click_button "Submit Token"
67
+
68
+ assert_equal 422, page.status_code
69
+ end
70
+
60
71
  test "successful token authentication" do
61
72
  user = enable_otp_and_sign_in
62
73
 
@@ -66,18 +77,14 @@ class SignInTest < ActionDispatch::IntegrationTest
66
77
  assert_equal root_path, current_path
67
78
  end
68
79
 
69
- test "should fail if the the challenge times out" do
70
- old_timeout = User.otp_authentication_timeout
71
- User.otp_authentication_timeout = 1.second
72
-
80
+ test "should fail and redirect if the the challenge is expired" do
73
81
  user = enable_otp_and_sign_in
74
82
 
75
- sleep(2)
83
+ Timecop.travel(Time.now + 3.minutes)
76
84
 
77
85
  fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
78
86
  click_button "Submit Token"
79
87
 
80
- User.otp_authentication_timeout = old_timeout
81
88
  assert_equal new_user_session_path, current_path
82
89
  end
83
90
 
@@ -92,7 +99,7 @@ class SignInTest < ActionDispatch::IntegrationTest
92
99
  fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
93
100
  click_button "Submit Token"
94
101
 
95
- assert !page.has_content?("The token you provided was invalid.")
102
+ assert_not page.has_content?("The token you provided was invalid.")
96
103
  end
97
104
 
98
105
  test "invalid token flash message does not persist to successful authentication redirect." do
@@ -106,6 +113,6 @@ class SignInTest < ActionDispatch::IntegrationTest
106
113
  fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
107
114
  click_button "Submit Token"
108
115
 
109
- assert !page.has_content?("You need to type in the token you generated with your device.")
116
+ assert_not page.has_content?("You need to type in the token you generated with your device.")
110
117
  end
111
118
  end
@@ -2,7 +2,6 @@ require "test_helper"
2
2
  require "integration_tests_helper"
3
3
 
4
4
  class TrackableTest < ActionDispatch::IntegrationTest
5
-
6
5
  def setup
7
6
  @user = sign_user_in
8
7
 
@@ -46,5 +45,4 @@ class TrackableTest < ActionDispatch::IntegrationTest
46
45
  assert_not_equal @sign_in_count, @user.sign_in_count
47
46
  assert_not_equal @current_sign_in_at, @user.current_sign_in_at
48
47
  end
49
-
50
48
  end
@@ -27,6 +27,17 @@ class ActionDispatch::IntegrationTest
27
27
  end
28
28
  end
29
29
 
30
+ def create_lockable_user
31
+ @lockable_user ||= begin
32
+ lockable_user = LockableUser.create!(
33
+ email: "lockable-user@devise-otp.local",
34
+ password: "12345678",
35
+ password_confirmation: "12345678"
36
+ )
37
+ lockable_user
38
+ end
39
+ end
40
+
30
41
  def create_non_otp_user
31
42
  @non_otp_user ||= begin
32
43
  non_otp_user = NonOtpUser.create!(
@@ -38,6 +49,17 @@ class ActionDispatch::IntegrationTest
38
49
  end
39
50
  end
40
51
 
52
+ def create_rememberable_user
53
+ @rememberable_user ||= begin
54
+ rememberable_user = RememberableUser.create!(
55
+ email: "rememberable-user@email.invalid",
56
+ password: "12345678",
57
+ password_confirmation: "12345678"
58
+ )
59
+ rememberable_user
60
+ end
61
+ end
62
+
41
63
  def enable_otp_and_sign_in_with_otp
42
64
  enable_otp_and_sign_in.tap do |user|
43
65
  fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
@@ -89,5 +111,4 @@ class ActionDispatch::IntegrationTest
89
111
  page.has_content?("Log in") ? click_button("Log in") : click_button("Sign in")
90
112
  user
91
113
  end
92
-
93
114
  end