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.
- checksums.yaml +4 -4
- data/.erb_lint.yml +30 -0
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +42 -1
- data/Gemfile +7 -1
- data/README.md +5 -1
- data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +30 -7
- data/app/controllers/devise_otp/devise/otp_persistence_controller.rb +53 -0
- data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +2 -37
- data/app/views/devise/otp_credentials/refresh.html.erb +6 -6
- data/app/views/devise/otp_credentials/show.html.erb +10 -13
- data/app/views/devise/otp_tokens/_token_secret.html.erb +9 -9
- data/app/views/devise/otp_tokens/_trusted_devices.html.erb +7 -7
- data/app/views/devise/otp_tokens/edit.html.erb +9 -9
- data/app/views/devise/otp_tokens/recovery.html.erb +6 -6
- data/app/views/devise/otp_tokens/show.html.erb +6 -6
- data/bin/erb_lint +27 -0
- data/bin/rubocop +27 -0
- data/config/locales/en.yml +9 -7
- data/devise-otp.gemspec +3 -1
- data/lib/devise/strategies/database_authenticatable.rb +4 -17
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +0 -1
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +1 -2
- data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +1 -2
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +7 -2
- data/lib/devise_otp_authenticatable/hooks/refreshable.rb +0 -1
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +18 -12
- data/lib/devise_otp_authenticatable/routes.rb +7 -7
- data/test/dummy/app/controllers/admin_posts_controller.rb +0 -1
- data/test/dummy/app/controllers/base_controller.rb +0 -2
- data/test/dummy/app/controllers/non_otp_posts_controller.rb +0 -1
- data/test/dummy/app/models/admin.rb +0 -1
- data/test/dummy/app/models/lockable_user.rb +8 -0
- data/test/dummy/app/models/non_otp_user.rb +0 -1
- data/test/dummy/app/models/rememberable_user.rb +8 -0
- data/test/dummy/app/views/layouts/application.html.erb +4 -2
- data/test/dummy/app/views/posts/show.html.erb +0 -1
- data/test/dummy/app/views/shared/_navbar.html.erb +15 -0
- data/test/dummy/config/application.rb +3 -0
- data/test/dummy/config/initializers/devise.rb +2 -2
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/db/migrate/20250731000001_create_lockable_users.rb +9 -0
- data/test/dummy/db/migrate/20250731000002_add_devise_to_lockable_users.rb +52 -0
- data/test/dummy/db/migrate/20250731000003_devise_otp_add_to_lockable_users.rb +28 -0
- data/test/dummy/db/migrate/20250817221304_create_rememberable_users.rb +50 -0
- data/test/dummy/db/migrate/20250818030305_add_devise_otp_to_rememberable_users.rb +28 -0
- data/test/dummy/db/schema.rb +67 -1
- data/test/integration/disable_token_test.rb +0 -2
- data/test/integration/enable_otp_form_test.rb +12 -1
- data/test/integration/lockable_test.rb +143 -0
- data/test/integration/non_otp_user_models_test.rb +0 -2
- data/test/integration/otp_drift_test.rb +35 -0
- data/test/integration/persistence_test.rb +59 -4
- data/test/integration/refresh_test.rb +23 -14
- data/test/integration/rememberable_test.rb +143 -0
- data/test/integration/reset_token_test.rb +0 -2
- data/test/integration/sign_in_test.rb +15 -8
- data/test/integration/trackable_test.rb +0 -2
- data/test/integration_tests_helper.rb +22 -1
- data/test/models/otp_authenticatable_test.rb +48 -11
- data/test/test_helper.rb +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
+
click_button("Trust this browser")
|
|
72
113
|
assert_text "Your browser is trusted."
|
|
73
114
|
sign_out
|
|
74
115
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|