devise-otp 1.1.0 → 2.1.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/.github/workflows/ci.yml +4 -2
- data/.rubocop.yml +18 -0
- data/Appraisals +4 -17
- data/CHANGELOG.md +59 -1
- data/Gemfile +7 -1
- data/README.md +37 -14
- data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +30 -7
- data/app/controllers/devise_otp/devise/otp_persistence_controller.rb +60 -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 +8 -6
- data/app/views/devise/otp_tokens/show.html.erb +6 -6
- data/bin/appraisal +16 -0
- data/bin/erb_lint +16 -0
- data/bin/rake +16 -0
- data/bin/rubocop +16 -0
- data/config/locales/{en.yml → devise-otp.en.yml} +10 -8
- data/devise-otp.gemspec +2 -2
- data/gemfiles/rails_8.0.gemfile +5 -1
- data/gemfiles/{rails_7.1.gemfile → rails_8.1.gemfile} +7 -7
- data/lib/devise/strategies/database_authenticatable.rb +4 -17
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +1 -2
- 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/assets/stylesheets/application.css +15 -1
- 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 +23 -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 +110 -44
- 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 +96 -5
- 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 +26 -9
- data/gemfiles/rails_7.2.gemfile +0 -17
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class AddDeviseToLockableUsers < ActiveRecord::Migration[5.0]
|
|
2
|
+
def self.up
|
|
3
|
+
change_table(:lockable_users) do |t|
|
|
4
|
+
## Database authenticatable
|
|
5
|
+
t.string :email, null: false, default: ""
|
|
6
|
+
t.string :encrypted_password, null: false, default: ""
|
|
7
|
+
|
|
8
|
+
## Recoverable
|
|
9
|
+
t.string :reset_password_token
|
|
10
|
+
t.datetime :reset_password_sent_at
|
|
11
|
+
|
|
12
|
+
## Rememberable
|
|
13
|
+
t.datetime :remember_created_at
|
|
14
|
+
|
|
15
|
+
## Trackable
|
|
16
|
+
t.integer :sign_in_count, default: 0
|
|
17
|
+
t.datetime :current_sign_in_at
|
|
18
|
+
t.datetime :last_sign_in_at
|
|
19
|
+
t.string :current_sign_in_ip
|
|
20
|
+
t.string :last_sign_in_ip
|
|
21
|
+
|
|
22
|
+
## Confirmable
|
|
23
|
+
# t.string :confirmation_token
|
|
24
|
+
# t.datetime :confirmed_at
|
|
25
|
+
# t.datetime :confirmation_sent_at
|
|
26
|
+
# t.string :unconfirmed_email # Only if using reconfirmable
|
|
27
|
+
|
|
28
|
+
## Lockable
|
|
29
|
+
t.integer :failed_attempts, default: 0 # Only if lock strategy is :failed_attempts
|
|
30
|
+
t.string :unlock_token # Only if unlock strategy is :email or :both
|
|
31
|
+
t.datetime :locked_at
|
|
32
|
+
|
|
33
|
+
## Token authenticatable
|
|
34
|
+
t.string :authentication_token
|
|
35
|
+
|
|
36
|
+
# Uncomment below if timestamps were not included in your original model.
|
|
37
|
+
# t.timestamps
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
add_index :lockable_users, :email, unique: true
|
|
41
|
+
add_index :lockable_users, :reset_password_token, unique: true
|
|
42
|
+
# add_index :lockable_users, :confirmation_token, :unique => true
|
|
43
|
+
add_index :lockable_users, :unlock_token, unique: true
|
|
44
|
+
add_index :lockable_users, :authentication_token, unique: true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.down
|
|
48
|
+
# By default, we don't want to make any assumption about how to roll back a migration when your
|
|
49
|
+
# model already existed. Please edit below which fields you would like to remove in this migration.
|
|
50
|
+
raise ActiveRecord::IrreversibleMigration
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class DeviseOtpAddToLockableUsers < ActiveRecord::Migration[5.0]
|
|
2
|
+
def self.up
|
|
3
|
+
change_table :lockable_users do |t|
|
|
4
|
+
t.string :otp_auth_secret
|
|
5
|
+
t.string :otp_recovery_secret
|
|
6
|
+
t.boolean :otp_enabled, default: false, null: false
|
|
7
|
+
t.boolean :otp_mandatory, default: false, null: false
|
|
8
|
+
t.datetime :otp_enabled_on
|
|
9
|
+
t.integer :otp_time_drift, default: 0, null: false
|
|
10
|
+
t.integer :otp_failed_attempts, default: 0, null: false
|
|
11
|
+
t.integer :otp_recovery_counter, default: 0, null: false
|
|
12
|
+
t.string :otp_persistence_seed
|
|
13
|
+
|
|
14
|
+
t.string :otp_session_challenge
|
|
15
|
+
t.datetime :otp_challenge_expires
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :lockable_users, :otp_session_challenge, unique: true
|
|
19
|
+
add_index :lockable_users, :otp_challenge_expires
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.down
|
|
23
|
+
change_table :lockable_users do |t|
|
|
24
|
+
t.remove :otp_auth_secret, :otp_recovery_secret, :otp_enabled, :otp_mandatory, :otp_enabled_on, :otp_session_challenge,
|
|
25
|
+
:otp_challenge_expires, :otp_time_drift, :otp_failed_attempts, :otp_recovery_counter, :otp_persistence_seed
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class CreateRememberableUsers < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :rememberable_users do |t|
|
|
4
|
+
t.string :name
|
|
5
|
+
|
|
6
|
+
## Database authenticatable
|
|
7
|
+
t.string :email, null: false, default: ""
|
|
8
|
+
t.string :encrypted_password, null: false, default: ""
|
|
9
|
+
|
|
10
|
+
## Recoverable
|
|
11
|
+
t.string :reset_password_token
|
|
12
|
+
t.datetime :reset_password_sent_at
|
|
13
|
+
|
|
14
|
+
## Rememberable
|
|
15
|
+
t.datetime :remember_created_at
|
|
16
|
+
|
|
17
|
+
## Trackable
|
|
18
|
+
t.integer :sign_in_count, default: 0
|
|
19
|
+
t.datetime :current_sign_in_at
|
|
20
|
+
t.datetime :last_sign_in_at
|
|
21
|
+
t.string :current_sign_in_ip
|
|
22
|
+
t.string :last_sign_in_ip
|
|
23
|
+
|
|
24
|
+
## Confirmable
|
|
25
|
+
# t.string :confirmation_token
|
|
26
|
+
# t.datetime :confirmed_at
|
|
27
|
+
# t.datetime :confirmation_sent_at
|
|
28
|
+
# t.string :unconfirmed_email # Only if using reconfirmable
|
|
29
|
+
|
|
30
|
+
## Lockable
|
|
31
|
+
# t.integer :failed_attempts, default: 0 # Only if lock strategy is :failed_attempts
|
|
32
|
+
# t.string :unlock_token # Only if unlock strategy is :email or :both
|
|
33
|
+
# t.datetime :locked_at
|
|
34
|
+
|
|
35
|
+
## Token authenticatable
|
|
36
|
+
# t.string :authentication_token
|
|
37
|
+
|
|
38
|
+
# Uncomment below if timestamps were not included in your original model.
|
|
39
|
+
# t.timestamps
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
add_index :rememberable_users, :email, unique: true
|
|
43
|
+
add_index :rememberable_users, :reset_password_token, unique: true
|
|
44
|
+
# add_index :rememberable_users, :confirmation_token, :unique => true
|
|
45
|
+
# add_index :rememberable_users, :unlock_token, unique: true
|
|
46
|
+
# add_index :rememberable_users, :authentication_token, unique: true
|
|
47
|
+
# t.timestamps
|
|
48
|
+
# end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class AddDeviseOtpToRememberableUsers < ActiveRecord::Migration[7.0]
|
|
2
|
+
def self.up
|
|
3
|
+
change_table :rememberable_users do |t|
|
|
4
|
+
t.string :otp_auth_secret
|
|
5
|
+
t.string :otp_recovery_secret
|
|
6
|
+
t.boolean :otp_enabled, default: false, null: false
|
|
7
|
+
t.boolean :otp_mandatory, default: false, null: false
|
|
8
|
+
t.datetime :otp_enabled_on
|
|
9
|
+
t.integer :otp_time_drift, default: 0, null: false
|
|
10
|
+
t.integer :otp_failed_attempts, default: 0, null: false
|
|
11
|
+
t.integer :otp_recovery_counter, default: 0, null: false
|
|
12
|
+
t.string :otp_persistence_seed
|
|
13
|
+
|
|
14
|
+
t.string :otp_session_challenge
|
|
15
|
+
t.datetime :otp_challenge_expires
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :rememberable_users, :otp_session_challenge, unique: true
|
|
19
|
+
add_index :rememberable_users, :otp_challenge_expires
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.down
|
|
23
|
+
change_table :rememberable_users do |t|
|
|
24
|
+
t.remove :otp_auth_secret, :otp_recovery_secret, :otp_enabled, :otp_mandatory, :otp_enabled_on, :otp_session_challenge,
|
|
25
|
+
:otp_challenge_expires, :otp_time_drift, :otp_failed_attempts, :otp_recovery_counter, :otp_persistence_seed
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/test/dummy/db/schema.rb
CHANGED
|
@@ -10,36 +10,36 @@
|
|
|
10
10
|
#
|
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
|
12
12
|
|
|
13
|
-
ActiveRecord::Schema[8.0].define(version:
|
|
13
|
+
ActiveRecord::Schema[8.0].define(version: 2025_08_18_030305) do
|
|
14
14
|
create_table "admins", force: :cascade do |t|
|
|
15
|
-
t.string "
|
|
15
|
+
t.string "authentication_token"
|
|
16
16
|
t.datetime "created_at", precision: nil, null: false
|
|
17
|
-
t.datetime "
|
|
17
|
+
t.datetime "current_sign_in_at", precision: nil
|
|
18
|
+
t.string "current_sign_in_ip"
|
|
18
19
|
t.string "email", default: "", null: false
|
|
19
20
|
t.string "encrypted_password", default: "", null: false
|
|
20
|
-
t.
|
|
21
|
-
t.datetime "reset_password_sent_at", precision: nil
|
|
22
|
-
t.datetime "remember_created_at", precision: nil
|
|
23
|
-
t.integer "sign_in_count", default: 0
|
|
24
|
-
t.datetime "current_sign_in_at", precision: nil
|
|
21
|
+
t.integer "failed_attempts", default: 0
|
|
25
22
|
t.datetime "last_sign_in_at", precision: nil
|
|
26
|
-
t.string "current_sign_in_ip"
|
|
27
23
|
t.string "last_sign_in_ip"
|
|
28
|
-
t.integer "failed_attempts", default: 0
|
|
29
|
-
t.string "unlock_token"
|
|
30
24
|
t.datetime "locked_at", precision: nil
|
|
31
|
-
t.string "
|
|
25
|
+
t.string "name"
|
|
32
26
|
t.string "otp_auth_secret"
|
|
33
|
-
t.
|
|
27
|
+
t.datetime "otp_challenge_expires", precision: nil
|
|
34
28
|
t.boolean "otp_enabled", default: false, null: false
|
|
35
|
-
t.boolean "otp_mandatory", default: false, null: false
|
|
36
29
|
t.datetime "otp_enabled_on", precision: nil
|
|
37
|
-
t.integer "otp_time_drift", default: 0, null: false
|
|
38
30
|
t.integer "otp_failed_attempts", default: 0, null: false
|
|
39
|
-
t.
|
|
31
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
40
32
|
t.string "otp_persistence_seed"
|
|
33
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
34
|
+
t.string "otp_recovery_secret"
|
|
41
35
|
t.string "otp_session_challenge"
|
|
42
|
-
t.
|
|
36
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
37
|
+
t.datetime "remember_created_at", precision: nil
|
|
38
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
39
|
+
t.string "reset_password_token"
|
|
40
|
+
t.integer "sign_in_count", default: 0
|
|
41
|
+
t.string "unlock_token"
|
|
42
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
43
43
|
t.index ["authentication_token"], name: "index_admins_on_authentication_token", unique: true
|
|
44
44
|
t.index ["email"], name: "index_admins_on_email", unique: true
|
|
45
45
|
t.index ["otp_challenge_expires"], name: "index_admins_on_otp_challenge_expires"
|
|
@@ -48,24 +48,61 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_18_092536) do
|
|
|
48
48
|
t.index ["unlock_token"], name: "index_admins_on_unlock_token", unique: true
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
create_table "
|
|
52
|
-
t.string "
|
|
53
|
-
t.datetime "created_at", null: false
|
|
54
|
-
t.datetime "
|
|
51
|
+
create_table "lockable_users", force: :cascade do |t|
|
|
52
|
+
t.string "authentication_token"
|
|
53
|
+
t.datetime "created_at", precision: nil, null: false
|
|
54
|
+
t.datetime "current_sign_in_at", precision: nil
|
|
55
|
+
t.string "current_sign_in_ip"
|
|
55
56
|
t.string "email", default: "", null: false
|
|
56
57
|
t.string "encrypted_password", default: "", null: false
|
|
58
|
+
t.integer "failed_attempts", default: 0
|
|
59
|
+
t.datetime "last_sign_in_at", precision: nil
|
|
60
|
+
t.string "last_sign_in_ip"
|
|
61
|
+
t.datetime "locked_at", precision: nil
|
|
62
|
+
t.string "name"
|
|
63
|
+
t.string "otp_auth_secret"
|
|
64
|
+
t.datetime "otp_challenge_expires", precision: nil
|
|
65
|
+
t.boolean "otp_enabled", default: false, null: false
|
|
66
|
+
t.datetime "otp_enabled_on", precision: nil
|
|
67
|
+
t.integer "otp_failed_attempts", default: 0, null: false
|
|
68
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
69
|
+
t.string "otp_persistence_seed"
|
|
70
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
71
|
+
t.string "otp_recovery_secret"
|
|
72
|
+
t.string "otp_session_challenge"
|
|
73
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
74
|
+
t.datetime "remember_created_at", precision: nil
|
|
75
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
57
76
|
t.string "reset_password_token"
|
|
58
|
-
t.datetime "reset_password_sent_at"
|
|
59
|
-
t.datetime "remember_created_at"
|
|
60
77
|
t.integer "sign_in_count", default: 0
|
|
61
|
-
t.
|
|
62
|
-
t.datetime "
|
|
78
|
+
t.string "unlock_token"
|
|
79
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
80
|
+
t.index ["authentication_token"], name: "index_lockable_users_on_authentication_token", unique: true
|
|
81
|
+
t.index ["email"], name: "index_lockable_users_on_email", unique: true
|
|
82
|
+
t.index ["otp_challenge_expires"], name: "index_lockable_users_on_otp_challenge_expires"
|
|
83
|
+
t.index ["otp_session_challenge"], name: "index_lockable_users_on_otp_session_challenge", unique: true
|
|
84
|
+
t.index ["reset_password_token"], name: "index_lockable_users_on_reset_password_token", unique: true
|
|
85
|
+
t.index ["unlock_token"], name: "index_lockable_users_on_unlock_token", unique: true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
create_table "non_otp_users", force: :cascade do |t|
|
|
89
|
+
t.string "authentication_token"
|
|
90
|
+
t.datetime "created_at", precision: nil, null: false
|
|
91
|
+
t.datetime "current_sign_in_at", precision: nil
|
|
63
92
|
t.string "current_sign_in_ip"
|
|
64
|
-
t.string "
|
|
93
|
+
t.string "email", default: "", null: false
|
|
94
|
+
t.string "encrypted_password", default: "", null: false
|
|
65
95
|
t.integer "failed_attempts", default: 0
|
|
96
|
+
t.datetime "last_sign_in_at", precision: nil
|
|
97
|
+
t.string "last_sign_in_ip"
|
|
98
|
+
t.datetime "locked_at", precision: nil
|
|
99
|
+
t.string "name"
|
|
100
|
+
t.datetime "remember_created_at", precision: nil
|
|
101
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
102
|
+
t.string "reset_password_token"
|
|
103
|
+
t.integer "sign_in_count", default: 0
|
|
66
104
|
t.string "unlock_token"
|
|
67
|
-
t.datetime "
|
|
68
|
-
t.string "authentication_token"
|
|
105
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
69
106
|
t.index ["authentication_token"], name: "index_non_otp_users_on_authentication_token", unique: true
|
|
70
107
|
t.index ["email"], name: "index_non_otp_users_on_email", unique: true
|
|
71
108
|
t.index ["reset_password_token"], name: "index_non_otp_users_on_reset_password_token", unique: true
|
|
@@ -73,41 +110,70 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_18_092536) do
|
|
|
73
110
|
end
|
|
74
111
|
|
|
75
112
|
create_table "posts", force: :cascade do |t|
|
|
76
|
-
t.string "title"
|
|
77
113
|
t.text "body"
|
|
78
114
|
t.datetime "created_at", precision: nil, null: false
|
|
115
|
+
t.string "title"
|
|
79
116
|
t.datetime "updated_at", precision: nil, null: false
|
|
80
117
|
end
|
|
81
118
|
|
|
82
|
-
create_table "
|
|
83
|
-
t.
|
|
84
|
-
t.
|
|
85
|
-
t.datetime "updated_at", precision: nil, null: false
|
|
119
|
+
create_table "rememberable_users", force: :cascade do |t|
|
|
120
|
+
t.datetime "current_sign_in_at"
|
|
121
|
+
t.string "current_sign_in_ip"
|
|
86
122
|
t.string "email", default: "", null: false
|
|
87
123
|
t.string "encrypted_password", default: "", null: false
|
|
124
|
+
t.datetime "last_sign_in_at"
|
|
125
|
+
t.string "last_sign_in_ip"
|
|
126
|
+
t.string "name"
|
|
127
|
+
t.string "otp_auth_secret"
|
|
128
|
+
t.datetime "otp_challenge_expires"
|
|
129
|
+
t.boolean "otp_enabled", default: false, null: false
|
|
130
|
+
t.datetime "otp_enabled_on"
|
|
131
|
+
t.integer "otp_failed_attempts", default: 0, null: false
|
|
132
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
133
|
+
t.string "otp_persistence_seed"
|
|
134
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
135
|
+
t.string "otp_recovery_secret"
|
|
136
|
+
t.string "otp_session_challenge"
|
|
137
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
138
|
+
t.datetime "remember_created_at"
|
|
139
|
+
t.datetime "reset_password_sent_at"
|
|
88
140
|
t.string "reset_password_token"
|
|
89
|
-
t.datetime "reset_password_sent_at", precision: nil
|
|
90
|
-
t.datetime "remember_created_at", precision: nil
|
|
91
141
|
t.integer "sign_in_count", default: 0
|
|
142
|
+
t.index ["email"], name: "index_rememberable_users_on_email", unique: true
|
|
143
|
+
t.index ["otp_challenge_expires"], name: "index_rememberable_users_on_otp_challenge_expires"
|
|
144
|
+
t.index ["otp_session_challenge"], name: "index_rememberable_users_on_otp_session_challenge", unique: true
|
|
145
|
+
t.index ["reset_password_token"], name: "index_rememberable_users_on_reset_password_token", unique: true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
create_table "users", force: :cascade do |t|
|
|
149
|
+
t.string "authentication_token"
|
|
150
|
+
t.datetime "created_at", precision: nil, null: false
|
|
92
151
|
t.datetime "current_sign_in_at", precision: nil
|
|
93
|
-
t.datetime "last_sign_in_at", precision: nil
|
|
94
152
|
t.string "current_sign_in_ip"
|
|
95
|
-
t.string "
|
|
153
|
+
t.string "email", default: "", null: false
|
|
154
|
+
t.string "encrypted_password", default: "", null: false
|
|
96
155
|
t.integer "failed_attempts", default: 0
|
|
97
|
-
t.
|
|
156
|
+
t.datetime "last_sign_in_at", precision: nil
|
|
157
|
+
t.string "last_sign_in_ip"
|
|
98
158
|
t.datetime "locked_at", precision: nil
|
|
99
|
-
t.string "
|
|
159
|
+
t.string "name"
|
|
100
160
|
t.string "otp_auth_secret"
|
|
101
|
-
t.
|
|
161
|
+
t.datetime "otp_challenge_expires", precision: nil
|
|
102
162
|
t.boolean "otp_enabled", default: false, null: false
|
|
103
|
-
t.boolean "otp_mandatory", default: false, null: false
|
|
104
163
|
t.datetime "otp_enabled_on", precision: nil
|
|
105
|
-
t.integer "otp_time_drift", default: 0, null: false
|
|
106
164
|
t.integer "otp_failed_attempts", default: 0, null: false
|
|
107
|
-
t.
|
|
165
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
108
166
|
t.string "otp_persistence_seed"
|
|
167
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
168
|
+
t.string "otp_recovery_secret"
|
|
109
169
|
t.string "otp_session_challenge"
|
|
110
|
-
t.
|
|
170
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
171
|
+
t.datetime "remember_created_at", precision: nil
|
|
172
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
173
|
+
t.string "reset_password_token"
|
|
174
|
+
t.integer "sign_in_count", default: 0
|
|
175
|
+
t.string "unlock_token"
|
|
176
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
111
177
|
t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
|
|
112
178
|
t.index ["email"], name: "index_users_on_email", unique: true
|
|
113
179
|
t.index ["otp_challenge_expires"], name: "index_users_on_otp_challenge_expires"
|
|
@@ -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
|