devise-otp 0.6.0 → 0.7.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/.github/workflows/ci.yml +0 -2
- data/.gitignore +3 -1
- data/CHANGELOG.md +51 -8
- data/README.md +8 -2
- data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +46 -27
- data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +24 -6
- data/app/views/devise/otp_credentials/show.html.erb +6 -6
- data/app/views/devise/otp_tokens/_token_secret.html.erb +3 -4
- data/app/views/devise/otp_tokens/edit.html.erb +26 -0
- data/app/views/devise/otp_tokens/show.html.erb +7 -14
- data/config/locales/en.yml +23 -14
- data/devise-otp.gemspec +1 -2
- data/lib/devise/strategies/database_authenticatable.rb +64 -0
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +31 -11
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +9 -10
- data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +39 -0
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +10 -0
- data/lib/devise_otp_authenticatable/engine.rb +2 -5
- data/lib/devise_otp_authenticatable/hooks/refreshable.rb +5 -0
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +22 -20
- data/lib/devise_otp_authenticatable/routes.rb +3 -1
- data/test/dummy/app/controllers/admin_posts_controller.rb +85 -0
- data/test/dummy/app/controllers/application_controller.rb +0 -1
- data/test/dummy/app/controllers/base_controller.rb +6 -0
- data/test/dummy/app/models/admin.rb +25 -0
- data/test/dummy/app/views/admin_posts/_form.html.erb +25 -0
- data/test/dummy/app/views/admin_posts/edit.html.erb +6 -0
- data/test/dummy/app/views/admin_posts/index.html.erb +25 -0
- data/test/dummy/app/views/admin_posts/new.html.erb +5 -0
- data/test/dummy/app/views/admin_posts/show.html.erb +15 -0
- data/test/dummy/app/views/base/home.html.erb +1 -0
- data/test/dummy/config/application.rb +0 -2
- data/test/dummy/config/routes.rb +4 -1
- data/test/dummy/db/migrate/20240604000001_create_admins.rb +9 -0
- data/test/dummy/db/migrate/20240604000002_add_devise_to_admins.rb +52 -0
- data/test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb +28 -0
- data/test/integration/disable_token_test.rb +53 -0
- data/test/integration/enable_otp_form_test.rb +57 -0
- data/test/integration/persistence_test.rb +3 -6
- data/test/integration/refresh_test.rb +32 -0
- data/test/integration/reset_token_test.rb +45 -0
- data/test/integration/sign_in_test.rb +10 -14
- data/test/integration/trackable_test.rb +50 -0
- data/test/integration_tests_helper.rb +24 -6
- data/test/models/otp_authenticatable_test.rb +62 -27
- data/test/test_helper.rb +1 -71
- metadata +26 -23
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +0 -58
- data/lib/devise_otp_authenticatable/hooks.rb +0 -11
- data/test/integration/token_test.rb +0 -30
@@ -51,13 +51,10 @@ class PersistenceTest < ActionDispatch::IntegrationTest
|
|
51
51
|
visit user_otp_token_path
|
52
52
|
assert_equal user_otp_token_path, current_path
|
53
53
|
|
54
|
-
|
54
|
+
click_link("Download recovery codes")
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
assert_equal 1, DownloadHelper.downloads.size
|
56
|
+
assert current_path.match?(/recovery\.text/)
|
57
|
+
assert page.body.match?(user.next_otp_recovery_tokens.values.join("\n"))
|
61
58
|
end
|
62
59
|
|
63
60
|
test "trusted status should expire" do
|
@@ -5,10 +5,12 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
5
5
|
def setup
|
6
6
|
@old_refresh = User.otp_credentials_refresh
|
7
7
|
User.otp_credentials_refresh = 1.second
|
8
|
+
Admin.otp_credentials_refresh = 1.second
|
8
9
|
end
|
9
10
|
|
10
11
|
def teardown
|
11
12
|
User.otp_credentials_refresh = @old_refresh
|
13
|
+
Admin.otp_credentials_refresh = @old_refresh
|
12
14
|
Capybara.reset_sessions!
|
13
15
|
end
|
14
16
|
|
@@ -72,4 +74,34 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
72
74
|
|
73
75
|
assert_equal user_otp_token_path, current_path
|
74
76
|
end
|
77
|
+
|
78
|
+
test "works for non-default warden scopes" do
|
79
|
+
admin = create_full_admin
|
80
|
+
|
81
|
+
admin.populate_otp_secrets!
|
82
|
+
admin.enable_otp!
|
83
|
+
|
84
|
+
visit new_admin_session_path
|
85
|
+
fill_in "admin_email", with: admin.email
|
86
|
+
fill_in "admin_password", with: admin.password
|
87
|
+
|
88
|
+
page.has_content?("Log in") ? click_button("Log in") : click_button("Sign in")
|
89
|
+
|
90
|
+
assert_equal admin_otp_credential_path, current_path
|
91
|
+
|
92
|
+
fill_in "token", with: ROTP::TOTP.new(admin.otp_auth_secret).at(Time.now)
|
93
|
+
click_button "Submit Token"
|
94
|
+
assert_equal "/", current_path
|
95
|
+
|
96
|
+
sleep(2)
|
97
|
+
|
98
|
+
visit admin_otp_token_path
|
99
|
+
assert_equal refresh_admin_otp_credential_path, current_path
|
100
|
+
|
101
|
+
fill_in "admin_refresh_password", with: "12345678"
|
102
|
+
click_button "Continue..."
|
103
|
+
|
104
|
+
assert_equal admin_otp_token_path, current_path
|
105
|
+
end
|
106
|
+
|
75
107
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "integration_tests_helper"
|
3
|
+
|
4
|
+
class ResetTokenTest < ActionDispatch::IntegrationTest
|
5
|
+
|
6
|
+
def setup
|
7
|
+
# log in 1fa
|
8
|
+
@user = enable_otp_and_sign_in
|
9
|
+
assert_equal user_otp_credential_path, current_path
|
10
|
+
|
11
|
+
# otp 2fa
|
12
|
+
fill_in "token", with: ROTP::TOTP.new(@user.otp_auth_secret).at(Time.now)
|
13
|
+
click_button "Submit Token"
|
14
|
+
assert_equal root_path, current_path
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
Capybara.reset_sessions!
|
20
|
+
end
|
21
|
+
|
22
|
+
test "redirects to otp_tokens#edit page" do
|
23
|
+
reset_otp
|
24
|
+
|
25
|
+
assert_equal "/users/otp/token/edit", current_path
|
26
|
+
end
|
27
|
+
|
28
|
+
test "generates new token secrets" do
|
29
|
+
# get auth secrets
|
30
|
+
auth_secret = @user.otp_auth_secret
|
31
|
+
recovery_secret = @user.otp_recovery_secret
|
32
|
+
|
33
|
+
# reset otp
|
34
|
+
reset_otp
|
35
|
+
|
36
|
+
# compare auth secrets
|
37
|
+
@user.reload
|
38
|
+
assert_not_nil @user.otp_auth_secret
|
39
|
+
assert_not_equal @user.otp_auth_secret, auth_secret
|
40
|
+
|
41
|
+
assert_not_nil @user.otp_recovery_secret
|
42
|
+
assert_not_equal @user.otp_recovery_secret, recovery_secret
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -17,20 +17,16 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
17
17
|
assert_equal posts_path, current_path
|
18
18
|
end
|
19
19
|
|
20
|
-
test "a new user, just signed in, should be able to
|
20
|
+
test "a new user, just signed in, should be able to see and click the 'Enable Two-Factor Authentication' link" do
|
21
21
|
user = sign_user_in
|
22
22
|
|
23
23
|
visit user_otp_token_path
|
24
|
-
assert
|
24
|
+
assert page.has_content?("Disabled")
|
25
25
|
|
26
|
-
|
27
|
-
click_button "Continue..."
|
26
|
+
click_link "Enable Two-Factor Authentication"
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
assert page.has_content?("Your token secret")
|
32
|
-
assert !user.otp_auth_secret.nil?
|
33
|
-
assert !user.otp_persistence_seed.nil?
|
28
|
+
assert page.has_content?("Enable Two-Factor Authentication")
|
29
|
+
assert_equal edit_user_otp_token_path, current_path
|
34
30
|
end
|
35
31
|
|
36
32
|
test "a new user should be able to sign in enable OTP and be prompted for their token" do
|
@@ -43,17 +39,17 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
43
39
|
enable_otp_and_sign_in
|
44
40
|
assert_equal user_otp_credential_path, current_path
|
45
41
|
|
46
|
-
fill_in "
|
42
|
+
fill_in "token", with: "123456"
|
47
43
|
click_button "Submit Token"
|
48
44
|
|
49
|
-
assert_equal
|
45
|
+
assert_equal user_otp_credential_path, current_path
|
50
46
|
end
|
51
47
|
|
52
48
|
test "fail blank token authentication" do
|
53
49
|
enable_otp_and_sign_in
|
54
50
|
assert_equal user_otp_credential_path, current_path
|
55
51
|
|
56
|
-
fill_in "
|
52
|
+
fill_in "token", with: ""
|
57
53
|
click_button "Submit Token"
|
58
54
|
|
59
55
|
assert_equal user_otp_credential_path, current_path
|
@@ -62,7 +58,7 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
62
58
|
test "successful token authentication" do
|
63
59
|
user = enable_otp_and_sign_in
|
64
60
|
|
65
|
-
fill_in "
|
61
|
+
fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
66
62
|
click_button "Submit Token"
|
67
63
|
|
68
64
|
assert_equal root_path, current_path
|
@@ -76,7 +72,7 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
76
72
|
|
77
73
|
sleep(2)
|
78
74
|
|
79
|
-
fill_in "
|
75
|
+
fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
80
76
|
click_button "Submit Token"
|
81
77
|
|
82
78
|
User.otp_authentication_timeout = old_timeout
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "integration_tests_helper"
|
3
|
+
|
4
|
+
class TrackableTest < ActionDispatch::IntegrationTest
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@user = sign_user_in
|
8
|
+
|
9
|
+
@user.reload
|
10
|
+
|
11
|
+
@sign_in_count = @user.sign_in_count
|
12
|
+
@current_sign_in_at = @user.current_sign_in_at
|
13
|
+
|
14
|
+
sign_out
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
Capybara.reset_sessions!
|
19
|
+
end
|
20
|
+
|
21
|
+
test "if otp is disabled, it should update devise trackable fields as usual when the user signs in" do
|
22
|
+
sign_user_in(@user)
|
23
|
+
|
24
|
+
@user.reload
|
25
|
+
|
26
|
+
assert_not_equal @sign_in_count, @user.sign_in_count
|
27
|
+
assert_not_equal @current_sign_in_at, @user.current_sign_in_at
|
28
|
+
end
|
29
|
+
|
30
|
+
test "if otp is enabled, it should not update devise trackable fields until user enters their user token to complete their sign in" do
|
31
|
+
@user.populate_otp_secrets!
|
32
|
+
@user.enable_otp!
|
33
|
+
|
34
|
+
sign_user_in(@user)
|
35
|
+
|
36
|
+
@user.reload
|
37
|
+
|
38
|
+
assert_equal @sign_in_count, @user.sign_in_count
|
39
|
+
assert_equal @current_sign_in_at, @user.current_sign_in_at
|
40
|
+
|
41
|
+
fill_in "token", with: ROTP::TOTP.new(@user.otp_auth_secret).at(Time.now)
|
42
|
+
click_button "Submit Token"
|
43
|
+
|
44
|
+
@user.reload
|
45
|
+
|
46
|
+
assert_not_equal @sign_in_count, @user.sign_in_count
|
47
|
+
assert_not_equal @current_sign_in_at, @user.current_sign_in_at
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -16,18 +16,31 @@ class ActionDispatch::IntegrationTest
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
def create_full_admin
|
20
|
+
@admin ||= begin
|
21
|
+
admin = Admin.create!(
|
22
|
+
email: "admin@email.invalid",
|
23
|
+
password: "12345678",
|
24
|
+
password_confirmation: "12345678"
|
25
|
+
)
|
26
|
+
admin
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
19
30
|
def enable_otp_and_sign_in_with_otp
|
20
31
|
enable_otp_and_sign_in.tap do |user|
|
21
|
-
fill_in "
|
32
|
+
fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
22
33
|
click_button "Submit Token"
|
23
34
|
end
|
24
35
|
end
|
25
36
|
|
26
37
|
def enable_otp_and_sign_in
|
27
38
|
user = create_full_user
|
39
|
+
user.populate_otp_secrets!
|
40
|
+
|
28
41
|
sign_user_in(user)
|
29
|
-
visit
|
30
|
-
|
42
|
+
visit edit_user_otp_token_path
|
43
|
+
fill_in "confirmation_code", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
31
44
|
click_button "Continue..."
|
32
45
|
|
33
46
|
Capybara.reset_sessions!
|
@@ -37,14 +50,18 @@ class ActionDispatch::IntegrationTest
|
|
37
50
|
end
|
38
51
|
|
39
52
|
def otp_challenge_for(user)
|
40
|
-
fill_in "
|
53
|
+
fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
41
54
|
click_button "Submit Token"
|
42
55
|
end
|
43
56
|
|
44
57
|
def disable_otp
|
45
58
|
visit user_otp_token_path
|
46
|
-
|
47
|
-
|
59
|
+
click_button "Disable Two-Factor Authentication"
|
60
|
+
end
|
61
|
+
|
62
|
+
def reset_otp
|
63
|
+
visit user_otp_token_path
|
64
|
+
click_button "Reset Token Secret"
|
48
65
|
end
|
49
66
|
|
50
67
|
def sign_out
|
@@ -61,4 +78,5 @@ class ActionDispatch::IntegrationTest
|
|
61
78
|
page.has_content?("Log in") ? click_button("Log in") : click_button("Sign in")
|
62
79
|
user
|
63
80
|
end
|
81
|
+
|
64
82
|
end
|
@@ -6,42 +6,70 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
6
6
|
new_user
|
7
7
|
end
|
8
8
|
|
9
|
-
test "new users have a
|
10
|
-
|
9
|
+
test "new users do not have a secret set" do
|
10
|
+
user = User.first
|
11
|
+
|
12
|
+
[:otp_auth_secret, :otp_recovery_secret, :otp_persistence_seed].each do |field|
|
13
|
+
assert_nil user.send(field)
|
14
|
+
end
|
11
15
|
end
|
12
16
|
|
13
17
|
test "new users have OTP disabled by default" do
|
14
18
|
assert !User.first.otp_enabled
|
15
19
|
end
|
16
20
|
|
17
|
-
test "
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
test "populating otp secrets should populate all required fields" do
|
22
|
+
user = User.first
|
23
|
+
user.populate_otp_secrets!
|
24
|
+
|
25
|
+
[:otp_auth_secret, :otp_recovery_secret, :otp_persistence_seed].each do |field|
|
26
|
+
assert_not_nil user.send(field)
|
27
|
+
end
|
21
28
|
end
|
22
29
|
|
23
|
-
test "
|
24
|
-
|
25
|
-
|
30
|
+
test "time_based_otp and recover_otp fields should be an instance of TOTP/ROTP objects" do
|
31
|
+
user = User.first
|
32
|
+
user.populate_otp_secrets!
|
33
|
+
|
34
|
+
assert user.time_based_otp.is_a? ROTP::TOTP
|
35
|
+
assert user.recovery_otp.is_a? ROTP::HOTP
|
26
36
|
end
|
27
37
|
|
28
|
-
test "
|
29
|
-
|
30
|
-
|
31
|
-
assert u.otp_enabled
|
32
|
-
otp_auth_secret = u.otp_auth_secret
|
33
|
-
otp_persistence_seed = u.otp_persistence_seed
|
38
|
+
test "clear_otp_fields should clear all otp fields" do
|
39
|
+
user = User.first
|
40
|
+
user.populate_otp_secrets!
|
34
41
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
42
|
+
user.enable_otp!
|
43
|
+
user.generate_otp_challenge!
|
44
|
+
user.update(
|
45
|
+
:otp_failed_attempts => 1,
|
46
|
+
:otp_recovery_counter => 1
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
assert user.otp_enabled
|
51
|
+
[:otp_auth_secret, :otp_recovery_secret, :otp_persistence_seed].each do |field|
|
52
|
+
assert_not_nil user.send(field)
|
53
|
+
end
|
54
|
+
[:otp_failed_attempts, :otp_recovery_counter].each do |field|
|
55
|
+
assert_not user.send(field) == 0
|
56
|
+
end
|
57
|
+
|
58
|
+
user.clear_otp_fields!
|
59
|
+
[:otp_auth_secret, :otp_recovery_secret, :otp_persistence_seed].each do |field|
|
60
|
+
assert_nil user.send(field)
|
61
|
+
end
|
62
|
+
[:otp_failed_attempts, :otp_recovery_counter].each do |field|
|
63
|
+
assert user.send(field) == 0
|
64
|
+
end
|
39
65
|
end
|
40
66
|
|
41
67
|
test "reset_otp_persistence should generate new persistence_seed but NOT change the otp_auth_secret" do
|
42
68
|
u = User.first
|
43
|
-
u.
|
69
|
+
u.populate_otp_secrets!
|
70
|
+
u.enable_otp!
|
44
71
|
assert u.otp_enabled
|
72
|
+
|
45
73
|
otp_auth_secret = u.otp_auth_secret
|
46
74
|
otp_persistence_seed = u.otp_persistence_seed
|
47
75
|
|
@@ -53,7 +81,8 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
53
81
|
|
54
82
|
test "generating a challenge, should retrieve the user later" do
|
55
83
|
u = User.first
|
56
|
-
u.
|
84
|
+
u.populate_otp_secrets!
|
85
|
+
u.enable_otp!
|
57
86
|
challenge = u.generate_otp_challenge!
|
58
87
|
|
59
88
|
w = User.find_valid_otp_challenge(challenge)
|
@@ -63,7 +92,8 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
63
92
|
|
64
93
|
test "expiring the challenge, should retrieve nothing" do
|
65
94
|
u = User.first
|
66
|
-
u.
|
95
|
+
u.populate_otp_secrets!
|
96
|
+
u.enable_otp!
|
67
97
|
challenge = u.generate_otp_challenge!(1.second)
|
68
98
|
sleep(2)
|
69
99
|
|
@@ -73,7 +103,8 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
73
103
|
|
74
104
|
test "expired challenges should not be valid" do
|
75
105
|
u = User.first
|
76
|
-
u.
|
106
|
+
u.populate_otp_secrets!
|
107
|
+
u.enable_otp!
|
77
108
|
challenge = u.generate_otp_challenge!(1.second)
|
78
109
|
sleep(2)
|
79
110
|
assert_equal false, u.otp_challenge_valid?
|
@@ -81,14 +112,16 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
81
112
|
|
82
113
|
test "null otp challenge" do
|
83
114
|
u = User.first
|
84
|
-
u.
|
115
|
+
u.populate_otp_secrets!
|
116
|
+
u.enable_otp!
|
85
117
|
assert_equal false, u.validate_otp_token("")
|
86
118
|
assert_equal false, u.validate_otp_token(nil)
|
87
119
|
end
|
88
120
|
|
89
121
|
test "generated otp token should be valid for the user" do
|
90
122
|
u = User.first
|
91
|
-
u.
|
123
|
+
u.populate_otp_secrets!
|
124
|
+
u.enable_otp!
|
92
125
|
|
93
126
|
secret = u.otp_auth_secret
|
94
127
|
token = ROTP::TOTP.new(secret).now
|
@@ -98,7 +131,8 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
98
131
|
|
99
132
|
test "generated otp token, out of drift window, should be NOT valid for the user" do
|
100
133
|
u = User.first
|
101
|
-
u.
|
134
|
+
u.populate_otp_secrets!
|
135
|
+
u.enable_otp!
|
102
136
|
|
103
137
|
secret = u.otp_auth_secret
|
104
138
|
|
@@ -110,7 +144,8 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
110
144
|
|
111
145
|
test "recovery secrets should be valid, and valid only once" do
|
112
146
|
u = User.first
|
113
|
-
u.
|
147
|
+
u.populate_otp_secrets!
|
148
|
+
u.enable_otp!
|
114
149
|
recovery = u.next_otp_recovery_tokens
|
115
150
|
|
116
151
|
assert u.valid_otp_recovery_token? recovery.fetch(0)
|
data/test/test_helper.rb
CHANGED
@@ -6,86 +6,16 @@ require "dummy/config/environment"
|
|
6
6
|
require "orm/#{DEVISE_ORM}"
|
7
7
|
require "rails/test_help"
|
8
8
|
require "capybara/rails"
|
9
|
-
require "capybara/cuprite"
|
10
9
|
require "minitest/reporters"
|
11
10
|
|
12
|
-
|
11
|
+
Minitest::Reporters.use!
|
13
12
|
|
14
13
|
# I18n.load_path << File.expand_path("../support/locale/en.yml", __FILE__) if DEVISE_ORM == :mongoid
|
15
14
|
|
16
15
|
# ActiveSupport::Deprecation.silenced = true
|
17
16
|
|
18
|
-
# Use a module to not pollute the global namespace
|
19
|
-
module CapybaraHelper
|
20
|
-
def self.register_driver(driver_name, args = [])
|
21
|
-
opts = {headless: true, js_errors: true, window_size: [1920, 1200], browser_options: {}}
|
22
|
-
args.each do |arg|
|
23
|
-
opts[:browser_options][arg] = nil
|
24
|
-
end
|
25
|
-
|
26
|
-
Capybara.register_driver(driver_name) do |app|
|
27
|
-
Capybara::Cuprite::Driver.new(app, opts)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# Register our own custom drivers
|
33
|
-
CapybaraHelper.register_driver(:headless_chrome, %w[disable-gpu no-sandbox disable-dev-shm-usage])
|
34
|
-
|
35
|
-
# Configure Capybara JS driver
|
36
|
-
Capybara.current_driver = :headless_chrome
|
37
|
-
Capybara.javascript_driver = :headless_chrome
|
38
|
-
|
39
|
-
# Configure Capybara server
|
40
|
-
Capybara.run_server = true
|
41
|
-
Capybara.server = :puma, {Silent: true}
|
42
|
-
|
43
17
|
class ActionDispatch::IntegrationTest
|
44
18
|
include Capybara::DSL
|
45
|
-
|
46
|
-
# What capybara calls a "page" in its DSL is actually a Capybara::Session
|
47
|
-
# and doesn't know about the *command* method that allows us to play with
|
48
|
-
# the Chrome API.
|
49
|
-
# See: https://rubydoc.info/github/jnicklas/capybara/master/Capybara/Session
|
50
|
-
#
|
51
|
-
# To enable downloads we need to do it on the browser's page object, so fetch it
|
52
|
-
# from this long method chain.
|
53
|
-
# See: https://github.com/rubycdp/ferrum/blob/master/lib/ferrum/page.rb
|
54
|
-
def enable_chrome_headless_downloads(session, directory)
|
55
|
-
page = session.driver.browser.page
|
56
|
-
page.command("Page.setDownloadBehavior", behavior: "allow", downloadPath: directory)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
# From https://collectiveidea.com/blog/archives/2012/01/27/testing-file-downloads-with-capybara-and-chromedriver
|
61
|
-
module DownloadHelper
|
62
|
-
extend self
|
63
|
-
|
64
|
-
TIMEOUT = 10
|
65
|
-
|
66
|
-
def downloads
|
67
|
-
Dir["/tmp/devise-otp/*"]
|
68
|
-
end
|
69
|
-
|
70
|
-
def wait_for_download(count: 1)
|
71
|
-
yield if block_given?
|
72
|
-
|
73
|
-
Timeout.timeout(TIMEOUT) do
|
74
|
-
sleep 0.2 until downloaded?(count)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def downloaded?(count)
|
79
|
-
!downloading? && downloads.size == count
|
80
|
-
end
|
81
|
-
|
82
|
-
def downloading?
|
83
|
-
downloads.grep(/\.crdownload$/).any?
|
84
|
-
end
|
85
|
-
|
86
|
-
def clear_downloads
|
87
|
-
FileUtils.rm_f(downloads)
|
88
|
-
end
|
89
19
|
end
|
90
20
|
|
91
21
|
require "devise-otp"
|
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: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lele Forzani
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2024-06-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -79,20 +79,6 @@ dependencies:
|
|
79
79
|
- - ">="
|
80
80
|
- !ruby/object:Gem::Version
|
81
81
|
version: '0'
|
82
|
-
- !ruby/object:Gem::Dependency
|
83
|
-
name: cuprite
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
85
|
-
requirements:
|
86
|
-
- - ">="
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: '0'
|
89
|
-
type: :development
|
90
|
-
prerelease: false
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
92
|
-
requirements:
|
93
|
-
- - ">="
|
94
|
-
- !ruby/object:Gem::Version
|
95
|
-
version: '0'
|
96
82
|
- !ruby/object:Gem::Dependency
|
97
83
|
name: minitest-reporters
|
98
84
|
requirement: !ruby/object:Gem::Requirement
|
@@ -167,16 +153,16 @@ dependencies:
|
|
167
153
|
name: sqlite3
|
168
154
|
requirement: !ruby/object:Gem::Requirement
|
169
155
|
requirements:
|
170
|
-
- - "
|
156
|
+
- - "~>"
|
171
157
|
- !ruby/object:Gem::Version
|
172
|
-
version: '
|
158
|
+
version: '1.4'
|
173
159
|
type: :development
|
174
160
|
prerelease: false
|
175
161
|
version_requirements: !ruby/object:Gem::Requirement
|
176
162
|
requirements:
|
177
|
-
- - "
|
163
|
+
- - "~>"
|
178
164
|
- !ruby/object:Gem::Version
|
179
|
-
version: '
|
165
|
+
version: '1.4'
|
180
166
|
- !ruby/object:Gem::Dependency
|
181
167
|
name: standardrb
|
182
168
|
requirement: !ruby/object:Gem::Requirement
|
@@ -214,6 +200,7 @@ files:
|
|
214
200
|
- app/views/devise/otp_credentials/show.html.erb
|
215
201
|
- app/views/devise/otp_tokens/_token_secret.html.erb
|
216
202
|
- app/views/devise/otp_tokens/_trusted_devices.html.erb
|
203
|
+
- app/views/devise/otp_tokens/edit.html.erb
|
217
204
|
- app/views/devise/otp_tokens/recovery.html.erb
|
218
205
|
- app/views/devise/otp_tokens/recovery_codes.text.erb
|
219
206
|
- app/views/devise/otp_tokens/show.html.erb
|
@@ -222,11 +209,12 @@ files:
|
|
222
209
|
- docs/QR_CODES.md
|
223
210
|
- lib/devise-otp.rb
|
224
211
|
- lib/devise-otp/version.rb
|
212
|
+
- lib/devise/strategies/database_authenticatable.rb
|
225
213
|
- lib/devise_otp_authenticatable/controllers/helpers.rb
|
214
|
+
- lib/devise_otp_authenticatable/controllers/public_helpers.rb
|
226
215
|
- lib/devise_otp_authenticatable/controllers/url_helpers.rb
|
227
216
|
- lib/devise_otp_authenticatable/engine.rb
|
228
|
-
- lib/devise_otp_authenticatable/hooks.rb
|
229
|
-
- lib/devise_otp_authenticatable/hooks/sessions.rb
|
217
|
+
- lib/devise_otp_authenticatable/hooks/refreshable.rb
|
230
218
|
- lib/devise_otp_authenticatable/models/otp_authenticatable.rb
|
231
219
|
- lib/devise_otp_authenticatable/routes.rb
|
232
220
|
- lib/generators/active_record/devise_otp_generator.rb
|
@@ -239,13 +227,22 @@ files:
|
|
239
227
|
- test/dummy/app/assets/config/manifest.js
|
240
228
|
- test/dummy/app/assets/javascripts/application.js
|
241
229
|
- test/dummy/app/assets/stylesheets/application.css
|
230
|
+
- test/dummy/app/controllers/admin_posts_controller.rb
|
242
231
|
- test/dummy/app/controllers/application_controller.rb
|
232
|
+
- test/dummy/app/controllers/base_controller.rb
|
243
233
|
- test/dummy/app/controllers/posts_controller.rb
|
244
234
|
- test/dummy/app/helpers/application_helper.rb
|
245
235
|
- test/dummy/app/helpers/posts_helper.rb
|
246
236
|
- test/dummy/app/mailers/.gitkeep
|
237
|
+
- test/dummy/app/models/admin.rb
|
247
238
|
- test/dummy/app/models/post.rb
|
248
239
|
- test/dummy/app/models/user.rb
|
240
|
+
- test/dummy/app/views/admin_posts/_form.html.erb
|
241
|
+
- test/dummy/app/views/admin_posts/edit.html.erb
|
242
|
+
- test/dummy/app/views/admin_posts/index.html.erb
|
243
|
+
- test/dummy/app/views/admin_posts/new.html.erb
|
244
|
+
- test/dummy/app/views/admin_posts/show.html.erb
|
245
|
+
- test/dummy/app/views/base/home.html.erb
|
249
246
|
- test/dummy/app/views/layouts/application.html.erb
|
250
247
|
- test/dummy/app/views/posts/_form.html.erb
|
251
248
|
- test/dummy/app/views/posts/edit.html.erb
|
@@ -273,16 +270,22 @@ files:
|
|
273
270
|
- test/dummy/db/migrate/20130131092406_add_devise_to_users.rb
|
274
271
|
- test/dummy/db/migrate/20130131142320_create_posts.rb
|
275
272
|
- test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb
|
273
|
+
- test/dummy/db/migrate/20240604000001_create_admins.rb
|
274
|
+
- test/dummy/db/migrate/20240604000002_add_devise_to_admins.rb
|
275
|
+
- test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb
|
276
276
|
- test/dummy/lib/assets/.gitkeep
|
277
277
|
- test/dummy/public/404.html
|
278
278
|
- test/dummy/public/422.html
|
279
279
|
- test/dummy/public/500.html
|
280
280
|
- test/dummy/public/favicon.ico
|
281
281
|
- test/dummy/script/rails
|
282
|
+
- test/integration/disable_token_test.rb
|
283
|
+
- test/integration/enable_otp_form_test.rb
|
282
284
|
- test/integration/persistence_test.rb
|
283
285
|
- test/integration/refresh_test.rb
|
286
|
+
- test/integration/reset_token_test.rb
|
284
287
|
- test/integration/sign_in_test.rb
|
285
|
-
- test/integration/
|
288
|
+
- test/integration/trackable_test.rb
|
286
289
|
- test/integration_tests_helper.rb
|
287
290
|
- test/model_tests_helper.rb
|
288
291
|
- test/models/otp_authenticatable_test.rb
|