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.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +182 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +211 -79
  5. data/doc/account_expiration.rdoc +12 -26
  6. data/doc/active_sessions.rdoc +49 -0
  7. data/doc/audit_logging.rdoc +44 -0
  8. data/doc/base.rdoc +75 -128
  9. data/doc/change_login.rdoc +7 -14
  10. data/doc/change_password.rdoc +9 -13
  11. data/doc/change_password_notify.rdoc +2 -2
  12. data/doc/close_account.rdoc +9 -16
  13. data/doc/confirm_password.rdoc +12 -5
  14. data/doc/create_account.rdoc +11 -22
  15. data/doc/disallow_password_reuse.rdoc +6 -13
  16. data/doc/email_auth.rdoc +15 -14
  17. data/doc/email_base.rdoc +6 -15
  18. data/doc/guides/admin_activation.rdoc +46 -0
  19. data/doc/guides/already_authenticated.rdoc +10 -0
  20. data/doc/guides/alternative_login.rdoc +46 -0
  21. data/doc/guides/create_account_programmatically.rdoc +38 -0
  22. data/doc/guides/delay_password.rdoc +25 -0
  23. data/doc/guides/email_only.rdoc +16 -0
  24. data/doc/guides/i18n.rdoc +26 -0
  25. data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
  26. data/doc/guides/links.rdoc +12 -0
  27. data/doc/guides/login_return.rdoc +37 -0
  28. data/doc/guides/password_column.rdoc +25 -0
  29. data/doc/guides/password_confirmation.rdoc +37 -0
  30. data/doc/guides/password_requirements.rdoc +30 -0
  31. data/doc/guides/paths.rdoc +36 -0
  32. data/doc/guides/query_params.rdoc +9 -0
  33. data/doc/guides/redirects.rdoc +17 -0
  34. data/doc/guides/registration_field.rdoc +68 -0
  35. data/doc/guides/require_mfa.rdoc +30 -0
  36. data/doc/guides/reset_password_autologin.rdoc +21 -0
  37. data/doc/guides/status_column.rdoc +28 -0
  38. data/doc/guides/totp_or_recovery.rdoc +16 -0
  39. data/doc/http_basic_auth.rdoc +10 -1
  40. data/doc/jwt.rdoc +22 -22
  41. data/doc/jwt_cors.rdoc +22 -0
  42. data/doc/jwt_refresh.rdoc +18 -8
  43. data/doc/lockout.rdoc +17 -15
  44. data/doc/login.rdoc +10 -2
  45. data/doc/login_password_requirements_base.rdoc +15 -37
  46. data/doc/logout.rdoc +2 -2
  47. data/doc/otp.rdoc +25 -19
  48. data/doc/password_complexity.rdoc +10 -26
  49. data/doc/password_expiration.rdoc +11 -25
  50. data/doc/password_grace_period.rdoc +16 -2
  51. data/doc/recovery_codes.rdoc +18 -12
  52. data/doc/release_notes/1.22.0.txt +11 -0
  53. data/doc/release_notes/1.23.0.txt +32 -0
  54. data/doc/release_notes/2.0.0.txt +361 -0
  55. data/doc/release_notes/2.1.0.txt +31 -0
  56. data/doc/release_notes/2.2.0.txt +39 -0
  57. data/doc/remember.rdoc +40 -64
  58. data/doc/reset_password.rdoc +12 -9
  59. data/doc/session_expiration.rdoc +1 -0
  60. data/doc/single_session.rdoc +16 -25
  61. data/doc/sms_codes.rdoc +24 -14
  62. data/doc/two_factor_base.rdoc +60 -22
  63. data/doc/verify_account.rdoc +14 -12
  64. data/doc/verify_account_grace_period.rdoc +6 -2
  65. data/doc/verify_login_change.rdoc +9 -8
  66. data/doc/webauthn.rdoc +115 -0
  67. data/doc/webauthn_login.rdoc +15 -0
  68. data/doc/webauthn_verify_account.rdoc +9 -0
  69. data/javascript/webauthn_auth.js +45 -0
  70. data/javascript/webauthn_setup.js +35 -0
  71. data/lib/roda/plugins/rodauth.rb +1 -1
  72. data/lib/rodauth.rb +36 -28
  73. data/lib/rodauth/features/account_expiration.rb +5 -5
  74. data/lib/rodauth/features/active_sessions.rb +158 -0
  75. data/lib/rodauth/features/audit_logging.rb +98 -0
  76. data/lib/rodauth/features/base.rb +144 -43
  77. data/lib/rodauth/features/change_password_notify.rb +2 -2
  78. data/lib/rodauth/features/confirm_password.rb +40 -2
  79. data/lib/rodauth/features/create_account.rb +8 -13
  80. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  81. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  82. data/lib/rodauth/features/email_auth.rb +31 -30
  83. data/lib/rodauth/features/email_base.rb +9 -4
  84. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  85. data/lib/rodauth/features/jwt.rb +63 -16
  86. data/lib/rodauth/features/jwt_cors.rb +53 -0
  87. data/lib/rodauth/features/jwt_refresh.rb +32 -9
  88. data/lib/rodauth/features/lockout.rb +12 -14
  89. data/lib/rodauth/features/login.rb +54 -10
  90. data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
  91. data/lib/rodauth/features/otp.rb +77 -80
  92. data/lib/rodauth/features/password_complexity.rb +8 -13
  93. data/lib/rodauth/features/password_expiration.rb +2 -2
  94. data/lib/rodauth/features/password_grace_period.rb +17 -10
  95. data/lib/rodauth/features/recovery_codes.rb +49 -53
  96. data/lib/rodauth/features/remember.rb +11 -27
  97. data/lib/rodauth/features/reset_password.rb +26 -26
  98. data/lib/rodauth/features/session_expiration.rb +6 -4
  99. data/lib/rodauth/features/single_session.rb +8 -6
  100. data/lib/rodauth/features/sms_codes.rb +62 -72
  101. data/lib/rodauth/features/two_factor_base.rb +134 -30
  102. data/lib/rodauth/features/verify_account.rb +29 -21
  103. data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
  104. data/lib/rodauth/features/verify_login_change.rb +12 -11
  105. data/lib/rodauth/features/webauthn.rb +505 -0
  106. data/lib/rodauth/features/webauthn_login.rb +70 -0
  107. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  108. data/lib/rodauth/version.rb +2 -2
  109. data/templates/button.str +1 -3
  110. data/templates/change-login.str +1 -2
  111. data/templates/change-password.str +3 -5
  112. data/templates/close-account.str +2 -2
  113. data/templates/confirm-password.str +1 -1
  114. data/templates/create-account.str +1 -1
  115. data/templates/email-auth-email.str +1 -1
  116. data/templates/email-auth-request-form.str +2 -3
  117. data/templates/email-auth.str +1 -1
  118. data/templates/global-logout-field.str +6 -0
  119. data/templates/login-confirm-field.str +2 -4
  120. data/templates/login-display.str +3 -2
  121. data/templates/login-field.str +2 -4
  122. data/templates/login-form-footer.str +6 -0
  123. data/templates/login-form.str +7 -0
  124. data/templates/login.str +1 -9
  125. data/templates/logout.str +1 -1
  126. data/templates/multi-phase-login.str +3 -0
  127. data/templates/otp-auth-code-field.str +5 -3
  128. data/templates/otp-auth.str +1 -1
  129. data/templates/otp-disable.str +1 -1
  130. data/templates/otp-setup.str +3 -3
  131. data/templates/password-confirm-field.str +2 -4
  132. data/templates/password-field.str +2 -4
  133. data/templates/recovery-auth.str +3 -6
  134. data/templates/recovery-codes.str +1 -1
  135. data/templates/remember.str +15 -20
  136. data/templates/reset-password-email.str +1 -1
  137. data/templates/reset-password-request.str +3 -3
  138. data/templates/reset-password.str +1 -2
  139. data/templates/sms-auth.str +1 -1
  140. data/templates/sms-code-field.str +5 -3
  141. data/templates/sms-confirm.str +1 -2
  142. data/templates/sms-disable.str +1 -2
  143. data/templates/sms-request.str +1 -1
  144. data/templates/sms-setup.str +6 -4
  145. data/templates/two-factor-auth.str +5 -0
  146. data/templates/two-factor-disable.str +6 -0
  147. data/templates/two-factor-manage.str +16 -0
  148. data/templates/unlock-account-email.str +1 -1
  149. data/templates/unlock-account-request.str +4 -4
  150. data/templates/unlock-account.str +1 -1
  151. data/templates/verify-account-email.str +1 -1
  152. data/templates/verify-account-resend.str +3 -3
  153. data/templates/verify-account.str +1 -2
  154. data/templates/verify-login-change-email.str +2 -1
  155. data/templates/verify-login-change.str +1 -1
  156. data/templates/webauthn-auth.str +11 -0
  157. data/templates/webauthn-remove.str +14 -0
  158. data/templates/webauthn-setup.str +12 -0
  159. metadata +110 -52
  160. data/Rakefile +0 -179
  161. data/doc/verify_change_login.rdoc +0 -11
  162. data/lib/rodauth/features/verify_change_login.rb +0 -20
  163. data/spec/account_expiration_spec.rb +0 -225
  164. data/spec/all.rb +0 -1
  165. data/spec/change_login_spec.rb +0 -156
  166. data/spec/change_password_notify_spec.rb +0 -33
  167. data/spec/change_password_spec.rb +0 -202
  168. data/spec/close_account_spec.rb +0 -162
  169. data/spec/confirm_password_spec.rb +0 -70
  170. data/spec/create_account_spec.rb +0 -127
  171. data/spec/disallow_common_passwords_spec.rb +0 -93
  172. data/spec/disallow_password_reuse_spec.rb +0 -179
  173. data/spec/email_auth_spec.rb +0 -285
  174. data/spec/http_basic_auth_spec.rb +0 -143
  175. data/spec/jwt_refresh_spec.rb +0 -256
  176. data/spec/jwt_spec.rb +0 -235
  177. data/spec/lockout_spec.rb +0 -250
  178. data/spec/login_spec.rb +0 -328
  179. data/spec/migrate/001_tables.rb +0 -184
  180. data/spec/migrate/002_account_password_hash_column.rb +0 -11
  181. data/spec/migrate_password/001_tables.rb +0 -73
  182. data/spec/migrate_travis/001_tables.rb +0 -141
  183. data/spec/password_complexity_spec.rb +0 -109
  184. data/spec/password_expiration_spec.rb +0 -244
  185. data/spec/password_grace_period_spec.rb +0 -93
  186. data/spec/remember_spec.rb +0 -451
  187. data/spec/reset_password_spec.rb +0 -229
  188. data/spec/rodauth_spec.rb +0 -343
  189. data/spec/session_expiration_spec.rb +0 -58
  190. data/spec/single_session_spec.rb +0 -127
  191. data/spec/spec_helper.rb +0 -327
  192. data/spec/two_factor_spec.rb +0 -1462
  193. data/spec/update_password_hash_spec.rb +0 -40
  194. data/spec/verify_account_grace_period_spec.rb +0 -171
  195. data/spec/verify_account_spec.rb +0 -240
  196. data/spec/verify_change_login_spec.rb +0 -46
  197. data/spec/verify_login_change_spec.rb +0 -232
  198. data/spec/views/layout-other.str +0 -11
  199. data/spec/views/layout.str +0 -11
  200. 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
- auth_value_method :already_an_account_with_this_login_message, 'already an account with this login'
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
- auth_value_method :logins_do_not_match_message, 'logins do not match'
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
- auth_value_method :passwords_do_not_match_message, 'passwords do not match'
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
- auth_value_method :same_as_existing_password_message, "invalid password, same as current password"
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,
@@ -19,62 +19,59 @@ module Rodauth
19
19
  before 'otp_setup'
20
20
  before 'otp_disable'
21
21
 
22
- configuration_module_eval do
23
- def before_otp_authentication_route(&block)
24
- warn "before_otp_authentication_route is deprecated, switch to before_otp_auth_route"
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 up two factor authentication", 'otp_disable'
34
- error_flash "Error logging in via two factor authentication", 'otp_auth'
35
- error_flash "Error setting up two factor authentication", 'otp_setup'
36
- error_flash "You have already setup two factor authentication", :otp_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 "Two factor authentication has been disabled", 'otp_disable'
39
- notice_flash "Two factor authentication is now setup", 'otp_setup'
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 Two Factor Authentication', 'otp_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 Two Factor Authentication', 'otp_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
- auth_value_method :otp_auth_label, 'Authentication Code'
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, nil
54
+ auth_value_method :otp_drift, 30
56
55
  auth_value_method :otp_interval, nil
57
- auth_value_method :otp_invalid_auth_code_message, "Invalid authentication code"
58
- auth_value_method :otp_invalid_secret_message, "invalid secret"
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
- auth_value_method :otp_provisioning_uri_label, 'Provisioning URL'
65
- auth_value_method :otp_secret_label, 'Secret'
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
- if redir = otp_lockout_redirect
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(:totp)
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
- two_factor_update_session(:totp)
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
- two_factor_remove_session
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
- super if defined?(super)
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
- request.host
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
- if RUBY_VERSION < '1.9'
377
- # :nocov:
378
- def base32_encode(data, length)
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
- auth_value_method :password_not_enough_character_groups_message, "does not include uppercase letters, lowercase letters, and numbers"
15
- auth_value_method :password_invalid_pattern_message, "includes common character sequence"
16
- auth_value_method :password_in_dictionary_message, "is a word in a dictionary"
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 singleton_methods.map(&:to_sym).include?(:password_dictionary)
29
+ return if method(:password_dictionary).owner != Rodauth::PasswordComplexity
33
30
 
34
31
  case password_dictionary_file
35
32
  when false
36
- return
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){"#{prefix}/#{change_password_route}"}
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
- session[password_changed_at_session_key] = Time.now.to_i
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 update_session
33
- super
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 set_last_password_entry
43
- session[last_password_entry_session_key] = Time.now.to_i
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.", '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'
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.", 'recovery_codes_added'
24
+ notice_flash "Additional authentication recovery codes have been added", 'recovery_codes_added'
25
25
 
26
- redirect(:recovery_auth){"#{prefix}/#{recovery_auth_route}"}
27
- redirect(:add_recovery_codes){"#{prefix}/#{recovery_codes_route}"}
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
- auth_value_method :add_recovery_codes_heading, '<h2>Add Additional Recovery Codes</h2>'
36
- auth_value_method :invalid_recovery_code_message, "Invalid recovery code"
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
- auth_value_method :recovery_codes_label, 'Recovery Code'
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(:recovery_code)
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
- add_recovery_codes(recovery_codes_limit - recovery_codes.length)
138
+ auto_add_missing_recovery_codes
163
139
  end
164
140
 
165
141
  def sms_confirm
166
142
  super if defined?(super)
167
- add_recovery_codes(recovery_codes_limit - recovery_codes.length)
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 sms_disable
146
+ def add_webauthn_credential(_)
178
147
  super if defined?(super)
179
- unless recovery_codes_primary?
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