rodauth 1.21.0 → 2.2.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/CHANGELOG +182 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +211 -79
- data/doc/account_expiration.rdoc +12 -26
- data/doc/active_sessions.rdoc +49 -0
- data/doc/audit_logging.rdoc +44 -0
- data/doc/base.rdoc +75 -128
- data/doc/change_login.rdoc +7 -14
- data/doc/change_password.rdoc +9 -13
- data/doc/change_password_notify.rdoc +2 -2
- data/doc/close_account.rdoc +9 -16
- data/doc/confirm_password.rdoc +12 -5
- data/doc/create_account.rdoc +11 -22
- data/doc/disallow_password_reuse.rdoc +6 -13
- data/doc/email_auth.rdoc +15 -14
- data/doc/email_base.rdoc +6 -15
- data/doc/guides/admin_activation.rdoc +46 -0
- data/doc/guides/already_authenticated.rdoc +10 -0
- data/doc/guides/alternative_login.rdoc +46 -0
- data/doc/guides/create_account_programmatically.rdoc +38 -0
- data/doc/guides/delay_password.rdoc +25 -0
- data/doc/guides/email_only.rdoc +16 -0
- data/doc/guides/i18n.rdoc +26 -0
- data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
- data/doc/guides/links.rdoc +12 -0
- data/doc/guides/login_return.rdoc +37 -0
- data/doc/guides/password_column.rdoc +25 -0
- data/doc/guides/password_confirmation.rdoc +37 -0
- data/doc/guides/password_requirements.rdoc +30 -0
- data/doc/guides/paths.rdoc +36 -0
- data/doc/guides/query_params.rdoc +9 -0
- data/doc/guides/redirects.rdoc +17 -0
- data/doc/guides/registration_field.rdoc +68 -0
- data/doc/guides/require_mfa.rdoc +30 -0
- data/doc/guides/reset_password_autologin.rdoc +21 -0
- data/doc/guides/status_column.rdoc +28 -0
- data/doc/guides/totp_or_recovery.rdoc +16 -0
- data/doc/http_basic_auth.rdoc +10 -1
- data/doc/jwt.rdoc +22 -22
- data/doc/jwt_cors.rdoc +22 -0
- data/doc/jwt_refresh.rdoc +18 -8
- data/doc/lockout.rdoc +17 -15
- data/doc/login.rdoc +10 -2
- data/doc/login_password_requirements_base.rdoc +15 -37
- data/doc/logout.rdoc +2 -2
- data/doc/otp.rdoc +25 -19
- data/doc/password_complexity.rdoc +10 -26
- data/doc/password_expiration.rdoc +11 -25
- data/doc/password_grace_period.rdoc +16 -2
- data/doc/recovery_codes.rdoc +18 -12
- data/doc/release_notes/1.22.0.txt +11 -0
- data/doc/release_notes/1.23.0.txt +32 -0
- data/doc/release_notes/2.0.0.txt +361 -0
- data/doc/release_notes/2.1.0.txt +31 -0
- data/doc/release_notes/2.2.0.txt +39 -0
- data/doc/remember.rdoc +40 -64
- data/doc/reset_password.rdoc +12 -9
- data/doc/session_expiration.rdoc +1 -0
- data/doc/single_session.rdoc +16 -25
- data/doc/sms_codes.rdoc +24 -14
- data/doc/two_factor_base.rdoc +60 -22
- data/doc/verify_account.rdoc +14 -12
- data/doc/verify_account_grace_period.rdoc +6 -2
- data/doc/verify_login_change.rdoc +9 -8
- data/doc/webauthn.rdoc +115 -0
- data/doc/webauthn_login.rdoc +15 -0
- data/doc/webauthn_verify_account.rdoc +9 -0
- data/javascript/webauthn_auth.js +45 -0
- data/javascript/webauthn_setup.js +35 -0
- data/lib/roda/plugins/rodauth.rb +1 -1
- data/lib/rodauth.rb +36 -28
- data/lib/rodauth/features/account_expiration.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +158 -0
- data/lib/rodauth/features/audit_logging.rb +98 -0
- data/lib/rodauth/features/base.rb +144 -43
- data/lib/rodauth/features/change_password_notify.rb +2 -2
- data/lib/rodauth/features/confirm_password.rb +40 -2
- data/lib/rodauth/features/create_account.rb +8 -13
- data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
- data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
- data/lib/rodauth/features/email_auth.rb +31 -30
- data/lib/rodauth/features/email_base.rb +9 -4
- data/lib/rodauth/features/http_basic_auth.rb +55 -35
- data/lib/rodauth/features/jwt.rb +63 -16
- data/lib/rodauth/features/jwt_cors.rb +53 -0
- data/lib/rodauth/features/jwt_refresh.rb +32 -9
- data/lib/rodauth/features/lockout.rb +12 -14
- data/lib/rodauth/features/login.rb +54 -10
- data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
- data/lib/rodauth/features/otp.rb +77 -80
- data/lib/rodauth/features/password_complexity.rb +8 -13
- data/lib/rodauth/features/password_expiration.rb +2 -2
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/recovery_codes.rb +49 -53
- data/lib/rodauth/features/remember.rb +11 -27
- data/lib/rodauth/features/reset_password.rb +26 -26
- data/lib/rodauth/features/session_expiration.rb +6 -4
- data/lib/rodauth/features/single_session.rb +8 -6
- data/lib/rodauth/features/sms_codes.rb +62 -72
- data/lib/rodauth/features/two_factor_base.rb +134 -30
- data/lib/rodauth/features/verify_account.rb +29 -21
- data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
- data/lib/rodauth/features/verify_login_change.rb +12 -11
- data/lib/rodauth/features/webauthn.rb +505 -0
- data/lib/rodauth/features/webauthn_login.rb +70 -0
- data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
- data/lib/rodauth/version.rb +2 -2
- data/templates/button.str +1 -3
- data/templates/change-login.str +1 -2
- data/templates/change-password.str +3 -5
- data/templates/close-account.str +2 -2
- data/templates/confirm-password.str +1 -1
- data/templates/create-account.str +1 -1
- data/templates/email-auth-email.str +1 -1
- data/templates/email-auth-request-form.str +2 -3
- data/templates/email-auth.str +1 -1
- data/templates/global-logout-field.str +6 -0
- data/templates/login-confirm-field.str +2 -4
- data/templates/login-display.str +3 -2
- data/templates/login-field.str +2 -4
- data/templates/login-form-footer.str +6 -0
- data/templates/login-form.str +7 -0
- data/templates/login.str +1 -9
- data/templates/logout.str +1 -1
- data/templates/multi-phase-login.str +3 -0
- data/templates/otp-auth-code-field.str +5 -3
- data/templates/otp-auth.str +1 -1
- data/templates/otp-disable.str +1 -1
- data/templates/otp-setup.str +3 -3
- data/templates/password-confirm-field.str +2 -4
- data/templates/password-field.str +2 -4
- data/templates/recovery-auth.str +3 -6
- data/templates/recovery-codes.str +1 -1
- data/templates/remember.str +15 -20
- data/templates/reset-password-email.str +1 -1
- data/templates/reset-password-request.str +3 -3
- data/templates/reset-password.str +1 -2
- data/templates/sms-auth.str +1 -1
- data/templates/sms-code-field.str +5 -3
- data/templates/sms-confirm.str +1 -2
- data/templates/sms-disable.str +1 -2
- data/templates/sms-request.str +1 -1
- data/templates/sms-setup.str +6 -4
- data/templates/two-factor-auth.str +5 -0
- data/templates/two-factor-disable.str +6 -0
- data/templates/two-factor-manage.str +16 -0
- data/templates/unlock-account-email.str +1 -1
- data/templates/unlock-account-request.str +4 -4
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-email.str +1 -1
- data/templates/verify-account-resend.str +3 -3
- data/templates/verify-account.str +1 -2
- data/templates/verify-login-change-email.str +2 -1
- data/templates/verify-login-change.str +1 -1
- data/templates/webauthn-auth.str +11 -0
- data/templates/webauthn-remove.str +14 -0
- data/templates/webauthn-setup.str +12 -0
- metadata +110 -52
- data/Rakefile +0 -179
- data/doc/verify_change_login.rdoc +0 -11
- data/lib/rodauth/features/verify_change_login.rb +0 -20
- data/spec/account_expiration_spec.rb +0 -225
- data/spec/all.rb +0 -1
- data/spec/change_login_spec.rb +0 -156
- data/spec/change_password_notify_spec.rb +0 -33
- data/spec/change_password_spec.rb +0 -202
- data/spec/close_account_spec.rb +0 -162
- data/spec/confirm_password_spec.rb +0 -70
- data/spec/create_account_spec.rb +0 -127
- data/spec/disallow_common_passwords_spec.rb +0 -93
- data/spec/disallow_password_reuse_spec.rb +0 -179
- data/spec/email_auth_spec.rb +0 -285
- data/spec/http_basic_auth_spec.rb +0 -143
- data/spec/jwt_refresh_spec.rb +0 -256
- data/spec/jwt_spec.rb +0 -235
- data/spec/lockout_spec.rb +0 -250
- data/spec/login_spec.rb +0 -328
- data/spec/migrate/001_tables.rb +0 -184
- data/spec/migrate/002_account_password_hash_column.rb +0 -11
- data/spec/migrate_password/001_tables.rb +0 -73
- data/spec/migrate_travis/001_tables.rb +0 -141
- data/spec/password_complexity_spec.rb +0 -109
- data/spec/password_expiration_spec.rb +0 -244
- data/spec/password_grace_period_spec.rb +0 -93
- data/spec/remember_spec.rb +0 -451
- data/spec/reset_password_spec.rb +0 -229
- data/spec/rodauth_spec.rb +0 -343
- data/spec/session_expiration_spec.rb +0 -58
- data/spec/single_session_spec.rb +0 -127
- data/spec/spec_helper.rb +0 -327
- data/spec/two_factor_spec.rb +0 -1462
- data/spec/update_password_hash_spec.rb +0 -40
- data/spec/verify_account_grace_period_spec.rb +0 -171
- data/spec/verify_account_spec.rb +0 -240
- data/spec/verify_change_login_spec.rb +0 -46
- data/spec/verify_login_change_spec.rb +0 -232
- data/spec/views/layout-other.str +0 -11
- data/spec/views/layout.str +0 -11
- data/spec/views/login.str +0 -21
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Rodauth
|
|
4
4
|
Feature.define(:login_password_requirements_base, :LoginPasswordRequirementsBase) do
|
|
5
|
-
|
|
5
|
+
translatable_method :already_an_account_with_this_login_message, 'already an account with this login'
|
|
6
6
|
auth_value_method :login_confirm_param, 'login-confirm'
|
|
7
7
|
auth_value_method :login_minimum_length, 3
|
|
8
8
|
auth_value_method :login_maximum_length, 255
|
|
9
|
-
|
|
9
|
+
translatable_method :logins_do_not_match_message, 'logins do not match'
|
|
10
10
|
auth_value_method :password_confirm_param, 'password-confirm'
|
|
11
11
|
auth_value_method :password_minimum_length, 6
|
|
12
|
-
|
|
12
|
+
translatable_method :passwords_do_not_match_message, 'passwords do not match'
|
|
13
13
|
auth_value_method :require_email_address_logins?, true
|
|
14
14
|
auth_value_method :require_login_confirmation?, true
|
|
15
15
|
auth_value_method :require_password_confirmation?, true
|
|
16
|
-
|
|
16
|
+
translatable_method :same_as_existing_password_message, "invalid password, same as current password"
|
|
17
17
|
|
|
18
18
|
auth_value_methods(
|
|
19
19
|
:login_confirm_label,
|
data/lib/rodauth/features/otp.rb
CHANGED
|
@@ -19,62 +19,59 @@ module Rodauth
|
|
|
19
19
|
before 'otp_setup'
|
|
20
20
|
before 'otp_disable'
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
before_otp_auth_route(&block)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
button 'Authenticate via 2nd Factor', 'otp_auth'
|
|
30
|
-
button 'Disable Two Factor Authentication', 'otp_disable'
|
|
31
|
-
button 'Setup Two Factor Authentication', 'otp_setup'
|
|
22
|
+
button 'Authenticate Using TOTP', 'otp_auth'
|
|
23
|
+
button 'Disable TOTP Authentication', 'otp_disable'
|
|
24
|
+
button 'Setup TOTP Authentication', 'otp_setup'
|
|
32
25
|
|
|
33
|
-
error_flash "Error disabling
|
|
34
|
-
error_flash "Error logging in via
|
|
35
|
-
error_flash "Error setting up
|
|
36
|
-
error_flash "You have already setup
|
|
26
|
+
error_flash "Error disabling TOTP authentication", 'otp_disable'
|
|
27
|
+
error_flash "Error logging in via TOTP authentication", 'otp_auth'
|
|
28
|
+
error_flash "Error setting up TOTP authentication", 'otp_setup'
|
|
29
|
+
error_flash "You have already setup TOTP authentication", 'otp_already_setup'
|
|
30
|
+
error_flash "TOTP authentication code use locked out due to numerous failures", 'otp_lockout'
|
|
37
31
|
|
|
38
|
-
notice_flash "
|
|
39
|
-
notice_flash "
|
|
32
|
+
notice_flash "TOTP authentication has been disabled", 'otp_disable'
|
|
33
|
+
notice_flash "TOTP authentication is now setup", 'otp_setup'
|
|
40
34
|
|
|
41
35
|
redirect :otp_disable
|
|
42
36
|
redirect :otp_already_setup
|
|
43
37
|
redirect :otp_setup
|
|
38
|
+
redirect(:otp_lockout){two_factor_auth_required_redirect}
|
|
44
39
|
|
|
45
40
|
loaded_templates %w'otp-disable otp-auth otp-setup otp-auth-code-field password-field'
|
|
46
|
-
view 'otp-disable', 'Disable
|
|
41
|
+
view 'otp-disable', 'Disable TOTP Authentication', 'otp_disable'
|
|
47
42
|
view 'otp-auth', 'Enter Authentication Code', 'otp_auth'
|
|
48
|
-
view 'otp-setup', 'Setup
|
|
43
|
+
view 'otp-setup', 'Setup TOTP Authentication', 'otp_setup'
|
|
44
|
+
|
|
45
|
+
translatable_method :otp_auth_link_text, "Authenticate Using TOTP"
|
|
46
|
+
translatable_method :otp_setup_link_text, "Setup TOTP Authentication"
|
|
47
|
+
translatable_method :otp_disable_link_text, "Disable TOTP Authentication"
|
|
49
48
|
|
|
50
49
|
auth_value_method :otp_auth_failures_limit, 5
|
|
51
|
-
|
|
50
|
+
translatable_method :otp_auth_label, 'Authentication Code'
|
|
52
51
|
auth_value_method :otp_auth_param, 'otp'
|
|
53
52
|
auth_value_method :otp_class, ROTP::TOTP
|
|
54
53
|
auth_value_method :otp_digits, nil
|
|
55
|
-
auth_value_method :otp_drift,
|
|
54
|
+
auth_value_method :otp_drift, 30
|
|
56
55
|
auth_value_method :otp_interval, nil
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
translatable_method :otp_invalid_auth_code_message, "Invalid authentication code"
|
|
57
|
+
translatable_method :otp_invalid_secret_message, "invalid secret"
|
|
59
58
|
auth_value_method :otp_keys_column, :key
|
|
60
59
|
auth_value_method :otp_keys_id_column, :id
|
|
61
60
|
auth_value_method :otp_keys_failures_column, :num_failures
|
|
62
61
|
auth_value_method :otp_keys_table, :account_otp_keys
|
|
63
62
|
auth_value_method :otp_keys_last_use_column, :last_use
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
translatable_method :otp_provisioning_uri_label, 'Provisioning URL'
|
|
64
|
+
translatable_method :otp_secret_label, 'Secret'
|
|
66
65
|
auth_value_method :otp_setup_param, 'otp_secret'
|
|
67
66
|
auth_value_method :otp_setup_raw_param, 'otp_raw_secret'
|
|
67
|
+
translatable_method :otp_auth_form_footer, ''
|
|
68
68
|
|
|
69
69
|
auth_cached_method :otp_key
|
|
70
70
|
auth_cached_method :otp
|
|
71
71
|
private :otp
|
|
72
72
|
|
|
73
73
|
auth_value_methods(
|
|
74
|
-
:otp_auth_form_footer,
|
|
75
74
|
:otp_issuer,
|
|
76
|
-
:otp_lockout_error_flash,
|
|
77
|
-
:otp_lockout_redirect,
|
|
78
75
|
:otp_keys_use_hmac?
|
|
79
76
|
)
|
|
80
77
|
|
|
@@ -82,6 +79,7 @@ module Rodauth
|
|
|
82
79
|
:otp,
|
|
83
80
|
:otp_exists?,
|
|
84
81
|
:otp_key,
|
|
82
|
+
:otp_last_use,
|
|
85
83
|
:otp_locked_out?,
|
|
86
84
|
:otp_new_secret,
|
|
87
85
|
:otp_provisioning_name,
|
|
@@ -103,18 +101,13 @@ module Rodauth
|
|
|
103
101
|
route(:otp_auth) do |r|
|
|
104
102
|
require_login
|
|
105
103
|
require_account_session
|
|
106
|
-
require_two_factor_not_authenticated
|
|
104
|
+
require_two_factor_not_authenticated('totp')
|
|
107
105
|
require_otp_setup
|
|
108
106
|
|
|
109
107
|
if otp_locked_out?
|
|
110
108
|
set_response_error_status(lockout_error_status)
|
|
111
109
|
set_redirect_error_flash otp_lockout_error_flash
|
|
112
|
-
|
|
113
|
-
redirect redir
|
|
114
|
-
else
|
|
115
|
-
clear_session
|
|
116
|
-
redirect require_login_redirect
|
|
117
|
-
end
|
|
110
|
+
redirect otp_lockout_redirect
|
|
118
111
|
end
|
|
119
112
|
|
|
120
113
|
before_otp_auth_route
|
|
@@ -126,7 +119,7 @@ module Rodauth
|
|
|
126
119
|
r.post do
|
|
127
120
|
if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
|
|
128
121
|
before_otp_authentication
|
|
129
|
-
two_factor_authenticate(
|
|
122
|
+
two_factor_authenticate('totp')
|
|
130
123
|
end
|
|
131
124
|
|
|
132
125
|
otp_record_authentication_failure
|
|
@@ -178,7 +171,9 @@ module Rodauth
|
|
|
178
171
|
transaction do
|
|
179
172
|
before_otp_setup
|
|
180
173
|
otp_add_key
|
|
181
|
-
|
|
174
|
+
unless two_factor_authenticated?
|
|
175
|
+
two_factor_update_session('totp')
|
|
176
|
+
end
|
|
182
177
|
after_otp_setup
|
|
183
178
|
end
|
|
184
179
|
set_notice_flash otp_setup_notice_flash
|
|
@@ -204,7 +199,9 @@ module Rodauth
|
|
|
204
199
|
transaction do
|
|
205
200
|
before_otp_disable
|
|
206
201
|
otp_remove
|
|
207
|
-
|
|
202
|
+
if two_factor_login_type_match?('totp')
|
|
203
|
+
two_factor_remove_session('totp')
|
|
204
|
+
end
|
|
208
205
|
after_otp_disable
|
|
209
206
|
end
|
|
210
207
|
set_notice_flash otp_disable_notice_flash
|
|
@@ -218,18 +215,6 @@ module Rodauth
|
|
|
218
215
|
end
|
|
219
216
|
end
|
|
220
217
|
|
|
221
|
-
def two_factor_authentication_setup?
|
|
222
|
-
super || otp_exists?
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def two_factor_need_setup_redirect
|
|
226
|
-
"#{prefix}/#{otp_setup_route}"
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def two_factor_auth_required_redirect
|
|
230
|
-
"#{prefix}/#{otp_auth_route}"
|
|
231
|
-
end
|
|
232
|
-
|
|
233
218
|
def two_factor_remove
|
|
234
219
|
super
|
|
235
220
|
otp_remove
|
|
@@ -240,19 +225,6 @@ module Rodauth
|
|
|
240
225
|
otp_remove_auth_failures
|
|
241
226
|
end
|
|
242
227
|
|
|
243
|
-
def otp_auth_form_footer
|
|
244
|
-
super if defined?(super)
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def otp_lockout_redirect
|
|
248
|
-
return super if defined?(super)
|
|
249
|
-
nil
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def otp_lockout_error_flash
|
|
253
|
-
"Authentication code use locked out due to numerous failures.#{super if defined?(super)}"
|
|
254
|
-
end
|
|
255
|
-
|
|
256
228
|
def require_otp_setup
|
|
257
229
|
unless otp_exists?
|
|
258
230
|
set_redirect_error_status(two_factor_not_setup_error_status)
|
|
@@ -270,11 +242,11 @@ module Rodauth
|
|
|
270
242
|
ot_pass = ot_pass.gsub(/\s+/, '')
|
|
271
243
|
if drift = otp_drift
|
|
272
244
|
if otp.respond_to?(:verify_with_drift)
|
|
245
|
+
# :nocov:
|
|
273
246
|
otp.verify_with_drift(ot_pass, drift)
|
|
274
|
-
else
|
|
275
247
|
# :nocov:
|
|
248
|
+
else
|
|
276
249
|
otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift)
|
|
277
|
-
# :nocov:
|
|
278
250
|
end
|
|
279
251
|
else
|
|
280
252
|
otp.verify(ot_pass)
|
|
@@ -283,7 +255,7 @@ module Rodauth
|
|
|
283
255
|
|
|
284
256
|
def otp_remove
|
|
285
257
|
otp_key_ds.delete
|
|
286
|
-
|
|
258
|
+
@otp_key = nil
|
|
287
259
|
end
|
|
288
260
|
|
|
289
261
|
def otp_add_key
|
|
@@ -297,6 +269,10 @@ module Rodauth
|
|
|
297
269
|
update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
|
|
298
270
|
end
|
|
299
271
|
|
|
272
|
+
def otp_last_use
|
|
273
|
+
convert_timestamp(otp_key_ds.get(otp_keys_last_use_column))
|
|
274
|
+
end
|
|
275
|
+
|
|
300
276
|
def otp_record_authentication_failure
|
|
301
277
|
otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
|
|
302
278
|
end
|
|
@@ -314,7 +290,7 @@ module Rodauth
|
|
|
314
290
|
end
|
|
315
291
|
|
|
316
292
|
def otp_issuer
|
|
317
|
-
|
|
293
|
+
domain
|
|
318
294
|
end
|
|
319
295
|
|
|
320
296
|
def otp_provisioning_name
|
|
@@ -337,8 +313,37 @@ module Rodauth
|
|
|
337
313
|
!!hmac_secret
|
|
338
314
|
end
|
|
339
315
|
|
|
316
|
+
def possible_authentication_methods
|
|
317
|
+
methods = super
|
|
318
|
+
methods << 'totp' if otp_exists? && !@otp_tmp_key
|
|
319
|
+
methods
|
|
320
|
+
end
|
|
321
|
+
|
|
340
322
|
private
|
|
341
323
|
|
|
324
|
+
def _two_factor_auth_links
|
|
325
|
+
links = super
|
|
326
|
+
links << [20, otp_auth_path, otp_auth_link_text] if otp_exists? && !otp_locked_out?
|
|
327
|
+
links
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def _two_factor_setup_links
|
|
331
|
+
links = super
|
|
332
|
+
links << [20, otp_setup_path, otp_setup_link_text] unless otp_exists?
|
|
333
|
+
links
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def _two_factor_remove_links
|
|
337
|
+
links = super
|
|
338
|
+
links << [20, otp_disable_path, otp_disable_link_text] if otp_exists?
|
|
339
|
+
links
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def _two_factor_remove_all_from_session
|
|
343
|
+
two_factor_remove_session('totp')
|
|
344
|
+
super
|
|
345
|
+
end
|
|
346
|
+
|
|
342
347
|
def clear_cached_otp
|
|
343
348
|
remove_instance_variable(:@otp) if defined?(@otp)
|
|
344
349
|
end
|
|
@@ -362,32 +367,24 @@ module Rodauth
|
|
|
362
367
|
end
|
|
363
368
|
|
|
364
369
|
if ROTP::Base32.respond_to?(:random_base32)
|
|
365
|
-
# :nocov:
|
|
366
370
|
def otp_new_secret
|
|
367
371
|
ROTP::Base32.random_base32.downcase
|
|
368
372
|
end
|
|
369
|
-
# :nocov:
|
|
370
373
|
else
|
|
374
|
+
# :nocov:
|
|
371
375
|
def otp_new_secret
|
|
372
376
|
ROTP::Base32.random.downcase
|
|
373
377
|
end
|
|
378
|
+
# :nocov:
|
|
374
379
|
end
|
|
375
380
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
380
|
-
length.times.map{|i|chars[data[i] % 32].chr}.join
|
|
381
|
-
end
|
|
382
|
-
# :nocov:
|
|
383
|
-
else
|
|
384
|
-
def base32_encode(data, length)
|
|
385
|
-
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
386
|
-
length.times.map{|i|chars[data[i].ord % 32]}.join
|
|
387
|
-
end
|
|
381
|
+
def base32_encode(data, length)
|
|
382
|
+
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
383
|
+
length.times.map{|i|chars[data[i].ord % 32]}.join
|
|
388
384
|
end
|
|
389
385
|
|
|
390
386
|
def _otp_tmp_key(secret)
|
|
387
|
+
@otp_tmp_key = true
|
|
391
388
|
@otp_user_key = nil
|
|
392
389
|
@otp_key = secret
|
|
393
390
|
end
|
|
@@ -11,13 +11,10 @@ module Rodauth
|
|
|
11
11
|
auth_value_method :password_max_length_for_groups_check, 11
|
|
12
12
|
auth_value_method :password_max_repeating_characters, 3
|
|
13
13
|
auth_value_method :password_invalid_pattern, Regexp.union([/qwerty/i, /azerty/i, /asdf/i, /zxcv/i] + (1..8).map{|i| /#{i}#{i+1}#{(i+2)%10}/})
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
auth_value_methods(
|
|
19
|
-
:password_too_many_repeating_characters_message
|
|
20
|
-
)
|
|
14
|
+
translatable_method :password_not_enough_character_groups_message, "does not include uppercase letters, lowercase letters, and numbers"
|
|
15
|
+
translatable_method :password_invalid_pattern_message, "includes common character sequence"
|
|
16
|
+
translatable_method :password_in_dictionary_message, "is a word in a dictionary"
|
|
17
|
+
translatable_method :password_too_many_repeating_characters_message, "contains too many of the same character in a row"
|
|
21
18
|
|
|
22
19
|
def password_meets_requirements?(password)
|
|
23
20
|
super && \
|
|
@@ -29,14 +26,16 @@ module Rodauth
|
|
|
29
26
|
|
|
30
27
|
def post_configure
|
|
31
28
|
super
|
|
32
|
-
return if
|
|
29
|
+
return if method(:password_dictionary).owner != Rodauth::PasswordComplexity
|
|
33
30
|
|
|
34
31
|
case password_dictionary_file
|
|
35
32
|
when false
|
|
36
|
-
|
|
33
|
+
# nothing
|
|
37
34
|
when nil
|
|
38
35
|
default_dictionary_file = '/usr/share/dict/words'
|
|
36
|
+
# :nocov:
|
|
39
37
|
if File.file?(default_dictionary_file)
|
|
38
|
+
# :nocov:
|
|
40
39
|
words = File.read(default_dictionary_file)
|
|
41
40
|
end
|
|
42
41
|
else
|
|
@@ -73,10 +72,6 @@ module Rodauth
|
|
|
73
72
|
false
|
|
74
73
|
end
|
|
75
74
|
|
|
76
|
-
def password_too_many_repeating_characters_message
|
|
77
|
-
"contains #{password_max_repeating_characters} or more of the same character in a row"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
75
|
def password_not_in_dictionary?(password)
|
|
81
76
|
return true unless dict = password_dictionary
|
|
82
77
|
return true unless password =~ /\A(?:\d*)([A-Za-z!@$+|][A-Za-z!@$+|0134578]+[A-Za-z!@$+|])(?:\d*)\z/
|
|
@@ -8,7 +8,7 @@ module Rodauth
|
|
|
8
8
|
error_flash "Your password cannot be changed yet", 'password_not_changeable_yet'
|
|
9
9
|
|
|
10
10
|
redirect :password_not_changeable_yet
|
|
11
|
-
redirect(:password_change_needed){
|
|
11
|
+
redirect(:password_change_needed){change_password_path}
|
|
12
12
|
|
|
13
13
|
auth_value_method :allow_password_change_after, -86400
|
|
14
14
|
auth_value_method :require_password_change_after, 90*86400
|
|
@@ -38,7 +38,7 @@ module Rodauth
|
|
|
38
38
|
|
|
39
39
|
def set_password(password)
|
|
40
40
|
update_password_changed_at
|
|
41
|
-
|
|
41
|
+
set_session_value(password_changed_at_session_key, Time.now.to_i)
|
|
42
42
|
super
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -5,6 +5,8 @@ module Rodauth
|
|
|
5
5
|
auth_value_method :password_grace_period, 300
|
|
6
6
|
session_key :last_password_entry_session_key, :last_password_entry
|
|
7
7
|
|
|
8
|
+
auth_methods :password_recently_entered?
|
|
9
|
+
|
|
8
10
|
def modifications_require_password?
|
|
9
11
|
return false unless super
|
|
10
12
|
!password_recently_entered?
|
|
@@ -17,6 +19,16 @@ module Rodauth
|
|
|
17
19
|
v
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
def password_recently_entered?
|
|
23
|
+
return false unless last_password_entry = session[last_password_entry_session_key]
|
|
24
|
+
last_password_entry + password_grace_period > Time.now.to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update_session
|
|
28
|
+
super
|
|
29
|
+
set_session_value(last_password_entry_session_key, @last_password_entry) if defined?(@last_password_entry)
|
|
30
|
+
end
|
|
31
|
+
|
|
20
32
|
private
|
|
21
33
|
|
|
22
34
|
def after_create_account
|
|
@@ -29,18 +41,13 @@ module Rodauth
|
|
|
29
41
|
@last_password_entry = Time.now.to_i
|
|
30
42
|
end
|
|
31
43
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
session[last_password_entry_session_key] = @last_password_entry if defined?(@last_password_entry)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def password_recently_entered?
|
|
38
|
-
return false unless last_password_entry = session[last_password_entry_session_key]
|
|
39
|
-
last_password_entry + password_grace_period > Time.now.to_i
|
|
44
|
+
def set_last_password_entry
|
|
45
|
+
set_session_value(last_password_entry_session_key, Time.now.to_i)
|
|
40
46
|
end
|
|
41
47
|
|
|
42
|
-
def
|
|
43
|
-
|
|
48
|
+
def require_password_authentication?
|
|
49
|
+
return true if defined?(super) && super
|
|
50
|
+
!password_recently_entered?
|
|
44
51
|
end
|
|
45
52
|
end
|
|
46
53
|
end
|
|
@@ -17,14 +17,14 @@ module Rodauth
|
|
|
17
17
|
button 'Authenticate via Recovery Code', 'recovery_auth'
|
|
18
18
|
button 'View Authentication Recovery Codes', 'view_recovery_codes'
|
|
19
19
|
|
|
20
|
-
error_flash "Error authenticating via recovery code
|
|
21
|
-
error_flash "Unable to add recovery codes
|
|
22
|
-
error_flash "Unable to view recovery codes
|
|
20
|
+
error_flash "Error authenticating via recovery code", 'invalid_recovery_code'
|
|
21
|
+
error_flash "Unable to add recovery codes", 'add_recovery_codes'
|
|
22
|
+
error_flash "Unable to view recovery codes", 'view_recovery_codes'
|
|
23
23
|
|
|
24
|
-
notice_flash "Additional authentication recovery codes have been added
|
|
24
|
+
notice_flash "Additional authentication recovery codes have been added", 'recovery_codes_added'
|
|
25
25
|
|
|
26
|
-
redirect(:recovery_auth){
|
|
27
|
-
redirect(:add_recovery_codes){
|
|
26
|
+
redirect(:recovery_auth){recovery_auth_path}
|
|
27
|
+
redirect(:add_recovery_codes){recovery_codes_path}
|
|
28
28
|
|
|
29
29
|
loaded_templates %w'add-recovery-codes recovery-auth recovery-codes password-field'
|
|
30
30
|
view 'add-recovery-codes', 'Authentication Recovery Codes', 'add_recovery_codes'
|
|
@@ -32,15 +32,19 @@ module Rodauth
|
|
|
32
32
|
view 'recovery-codes', 'View Authentication Recovery Codes', 'recovery_codes'
|
|
33
33
|
|
|
34
34
|
auth_value_method :add_recovery_codes_param, 'add'
|
|
35
|
-
|
|
36
|
-
auth_value_method :
|
|
35
|
+
translatable_method :add_recovery_codes_heading, '<h2>Add Additional Recovery Codes</h2>'
|
|
36
|
+
auth_value_method :auto_add_recovery_codes?, false
|
|
37
|
+
translatable_method :invalid_recovery_code_message, "Invalid recovery code"
|
|
37
38
|
auth_value_method :recovery_codes_limit, 16
|
|
38
39
|
auth_value_method :recovery_codes_column, :code
|
|
39
40
|
auth_value_method :recovery_codes_id_column, :id
|
|
40
|
-
|
|
41
|
+
translatable_method :recovery_codes_label, 'Recovery Code'
|
|
41
42
|
auth_value_method :recovery_codes_param, 'recovery-code'
|
|
42
43
|
auth_value_method :recovery_codes_table, :account_recovery_codes
|
|
43
44
|
|
|
45
|
+
translatable_method :recovery_auth_link_text, "Authenticate Using Recovery Code"
|
|
46
|
+
translatable_method :recovery_codes_link_text, "View Authentication Recovery Codes"
|
|
47
|
+
|
|
44
48
|
auth_cached_method :recovery_codes
|
|
45
49
|
|
|
46
50
|
auth_value_methods(
|
|
@@ -59,7 +63,7 @@ module Rodauth
|
|
|
59
63
|
require_login
|
|
60
64
|
require_account_session
|
|
61
65
|
require_two_factor_setup
|
|
62
|
-
require_two_factor_not_authenticated
|
|
66
|
+
require_two_factor_not_authenticated('recovery_code')
|
|
63
67
|
before_recovery_auth_route
|
|
64
68
|
|
|
65
69
|
r.get do
|
|
@@ -69,7 +73,7 @@ module Rodauth
|
|
|
69
73
|
r.post do
|
|
70
74
|
if recovery_code_match?(param(recovery_codes_param))
|
|
71
75
|
before_recovery_auth
|
|
72
|
-
two_factor_authenticate(
|
|
76
|
+
two_factor_authenticate('recovery_code')
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
set_response_error_status(invalid_key_error_status)
|
|
@@ -124,61 +128,24 @@ module Rodauth
|
|
|
124
128
|
|
|
125
129
|
attr_accessor :recovery_codes_button
|
|
126
130
|
|
|
127
|
-
def two_factor_need_setup_redirect
|
|
128
|
-
super || (add_recovery_codes_redirect if recovery_codes_primary?)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def two_factor_auth_required_redirect
|
|
132
|
-
super || (recovery_auth_redirect if recovery_codes_primary?)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def two_factor_auth_fallback_redirect
|
|
136
|
-
recovery_auth_redirect
|
|
137
|
-
end
|
|
138
|
-
|
|
139
131
|
def two_factor_remove
|
|
140
132
|
super
|
|
141
133
|
recovery_codes_remove
|
|
142
134
|
end
|
|
143
135
|
|
|
144
|
-
def two_factor_authentication_setup?
|
|
145
|
-
super || (recovery_codes_primary? && !recovery_codes.empty?)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def otp_auth_form_footer
|
|
149
|
-
"#{super if defined?(super)}<p><a href=\"#{recovery_auth_route}\">Authenticate using recovery code</a></p>"
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def otp_lockout_redirect
|
|
153
|
-
recovery_auth_redirect
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def otp_lockout_error_flash
|
|
157
|
-
"#{super if defined?(super)} Can use recovery code to unlock."
|
|
158
|
-
end
|
|
159
|
-
|
|
160
136
|
def otp_add_key
|
|
161
137
|
super if defined?(super)
|
|
162
|
-
|
|
138
|
+
auto_add_missing_recovery_codes
|
|
163
139
|
end
|
|
164
140
|
|
|
165
141
|
def sms_confirm
|
|
166
142
|
super if defined?(super)
|
|
167
|
-
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def otp_remove
|
|
171
|
-
super if defined?(super)
|
|
172
|
-
unless recovery_codes_primary?
|
|
173
|
-
recovery_codes_remove
|
|
174
|
-
end
|
|
143
|
+
auto_add_missing_recovery_codes
|
|
175
144
|
end
|
|
176
145
|
|
|
177
|
-
def
|
|
146
|
+
def add_webauthn_credential(_)
|
|
178
147
|
super if defined?(super)
|
|
179
|
-
|
|
180
|
-
recovery_codes_remove
|
|
181
|
-
end
|
|
148
|
+
auto_add_missing_recovery_codes
|
|
182
149
|
end
|
|
183
150
|
|
|
184
151
|
def recovery_codes_remove
|
|
@@ -221,14 +188,43 @@ module Rodauth
|
|
|
221
188
|
end
|
|
222
189
|
end
|
|
223
190
|
|
|
191
|
+
def possible_authentication_methods
|
|
192
|
+
methods = super
|
|
193
|
+
methods << 'recovery_code' unless recovery_codes_ds.empty?
|
|
194
|
+
methods
|
|
195
|
+
end
|
|
196
|
+
|
|
224
197
|
private
|
|
225
198
|
|
|
199
|
+
def _two_factor_auth_links
|
|
200
|
+
links = super
|
|
201
|
+
links << [40, recovery_auth_path, recovery_auth_link_text] unless recovery_codes_ds.empty?
|
|
202
|
+
links
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def _two_factor_setup_links
|
|
206
|
+
links = super
|
|
207
|
+
links << [40, recovery_codes_path, recovery_codes_link_text] if (recovery_codes_primary? || uses_two_factor_authentication?)
|
|
208
|
+
links
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def _two_factor_remove_all_from_session
|
|
212
|
+
two_factor_remove_session('recovery_code')
|
|
213
|
+
super
|
|
214
|
+
end
|
|
215
|
+
|
|
226
216
|
def new_recovery_code
|
|
227
217
|
random_key
|
|
228
218
|
end
|
|
229
219
|
|
|
230
220
|
def recovery_codes_primary?
|
|
231
|
-
(features & [:otp, :sms_codes]).empty?
|
|
221
|
+
(features & [:otp, :sms_codes, :webauthn]).empty?
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def auto_add_missing_recovery_codes
|
|
225
|
+
if auto_add_recovery_codes?
|
|
226
|
+
add_recovery_codes(recovery_codes_limit - recovery_codes.length)
|
|
227
|
+
end
|
|
232
228
|
end
|
|
233
229
|
|
|
234
230
|
def _recovery_codes
|