rodauth 1.23.0 → 2.4.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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +184 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +221 -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 +76 -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 +5 -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 +2 -3
  42. data/doc/jwt_refresh.rdoc +23 -8
  43. data/doc/lockout.rdoc +17 -15
  44. data/doc/login.rdoc +17 -2
  45. data/doc/login_password_requirements_base.rdoc +18 -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/password_pepper.rdoc +44 -0
  52. data/doc/recovery_codes.rdoc +18 -12
  53. data/doc/release_notes/2.0.0.txt +361 -0
  54. data/doc/release_notes/2.1.0.txt +31 -0
  55. data/doc/release_notes/2.2.0.txt +39 -0
  56. data/doc/release_notes/2.3.0.txt +37 -0
  57. data/doc/release_notes/2.4.0.txt +22 -0
  58. data/doc/remember.rdoc +40 -64
  59. data/doc/reset_password.rdoc +12 -9
  60. data/doc/session_expiration.rdoc +1 -0
  61. data/doc/single_session.rdoc +16 -25
  62. data/doc/sms_codes.rdoc +24 -14
  63. data/doc/two_factor_base.rdoc +60 -22
  64. data/doc/verify_account.rdoc +14 -12
  65. data/doc/verify_account_grace_period.rdoc +6 -2
  66. data/doc/verify_login_change.rdoc +9 -8
  67. data/doc/webauthn.rdoc +115 -0
  68. data/doc/webauthn_login.rdoc +15 -0
  69. data/doc/webauthn_verify_account.rdoc +9 -0
  70. data/javascript/webauthn_auth.js +45 -0
  71. data/javascript/webauthn_setup.js +35 -0
  72. data/lib/roda/plugins/rodauth.rb +1 -1
  73. data/lib/rodauth.rb +33 -28
  74. data/lib/rodauth/features/account_expiration.rb +5 -5
  75. data/lib/rodauth/features/active_sessions.rb +158 -0
  76. data/lib/rodauth/features/audit_logging.rb +98 -0
  77. data/lib/rodauth/features/base.rb +152 -49
  78. data/lib/rodauth/features/change_password_notify.rb +1 -1
  79. data/lib/rodauth/features/close_account.rb +8 -6
  80. data/lib/rodauth/features/confirm_password.rb +40 -2
  81. data/lib/rodauth/features/create_account.rb +8 -13
  82. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  83. data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
  84. data/lib/rodauth/features/email_auth.rb +30 -28
  85. data/lib/rodauth/features/email_base.rb +3 -3
  86. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  87. data/lib/rodauth/features/jwt.rb +63 -16
  88. data/lib/rodauth/features/jwt_cors.rb +15 -15
  89. data/lib/rodauth/features/jwt_refresh.rb +42 -13
  90. data/lib/rodauth/features/lockout.rb +11 -13
  91. data/lib/rodauth/features/login.rb +58 -13
  92. data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
  93. data/lib/rodauth/features/otp.rb +76 -82
  94. data/lib/rodauth/features/password_complexity.rb +8 -13
  95. data/lib/rodauth/features/password_expiration.rb +1 -1
  96. data/lib/rodauth/features/password_grace_period.rb +17 -10
  97. data/lib/rodauth/features/password_pepper.rb +45 -0
  98. data/lib/rodauth/features/recovery_codes.rb +47 -51
  99. data/lib/rodauth/features/remember.rb +13 -27
  100. data/lib/rodauth/features/reset_password.rb +25 -25
  101. data/lib/rodauth/features/session_expiration.rb +7 -10
  102. data/lib/rodauth/features/single_session.rb +8 -6
  103. data/lib/rodauth/features/sms_codes.rb +58 -68
  104. data/lib/rodauth/features/two_factor_base.rb +134 -30
  105. data/lib/rodauth/features/verify_account.rb +28 -20
  106. data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
  107. data/lib/rodauth/features/verify_login_change.rb +11 -10
  108. data/lib/rodauth/features/webauthn.rb +505 -0
  109. data/lib/rodauth/features/webauthn_login.rb +70 -0
  110. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  111. data/lib/rodauth/migrations.rb +16 -5
  112. data/lib/rodauth/version.rb +2 -2
  113. data/templates/button.str +1 -3
  114. data/templates/change-login.str +1 -2
  115. data/templates/change-password.str +3 -5
  116. data/templates/close-account.str +2 -2
  117. data/templates/confirm-password.str +1 -1
  118. data/templates/create-account.str +1 -1
  119. data/templates/email-auth-request-form.str +1 -2
  120. data/templates/email-auth.str +1 -1
  121. data/templates/global-logout-field.str +6 -0
  122. data/templates/login-confirm-field.str +2 -4
  123. data/templates/login-display.str +3 -2
  124. data/templates/login-field.str +2 -4
  125. data/templates/login-form-footer.str +6 -0
  126. data/templates/login-form.str +7 -0
  127. data/templates/login.str +1 -9
  128. data/templates/logout.str +1 -1
  129. data/templates/multi-phase-login.str +3 -0
  130. data/templates/otp-auth-code-field.str +5 -3
  131. data/templates/otp-auth.str +1 -1
  132. data/templates/otp-disable.str +1 -1
  133. data/templates/otp-setup.str +3 -3
  134. data/templates/password-confirm-field.str +2 -4
  135. data/templates/password-field.str +2 -4
  136. data/templates/recovery-auth.str +3 -6
  137. data/templates/recovery-codes.str +1 -1
  138. data/templates/remember.str +15 -20
  139. data/templates/reset-password-request.str +2 -2
  140. data/templates/reset-password.str +1 -2
  141. data/templates/sms-auth.str +1 -1
  142. data/templates/sms-code-field.str +5 -3
  143. data/templates/sms-confirm.str +1 -2
  144. data/templates/sms-disable.str +1 -2
  145. data/templates/sms-request.str +1 -1
  146. data/templates/sms-setup.str +6 -4
  147. data/templates/two-factor-auth.str +5 -0
  148. data/templates/two-factor-disable.str +6 -0
  149. data/templates/two-factor-manage.str +16 -0
  150. data/templates/unlock-account-request.str +2 -2
  151. data/templates/unlock-account.str +1 -1
  152. data/templates/verify-account-resend.str +1 -1
  153. data/templates/verify-account.str +1 -2
  154. data/templates/verify-login-change.str +1 -1
  155. data/templates/webauthn-auth.str +11 -0
  156. data/templates/webauthn-remove.str +14 -0
  157. data/templates/webauthn-setup.str +12 -0
  158. metadata +96 -13
  159. data/doc/verify_change_login.rdoc +0 -11
  160. data/lib/rodauth/features/verify_change_login.rb +0 -20
@@ -1,5 +1,5 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require 'rodauth'
3
+ require_relative '../../rodauth'
4
4
 
5
5
  Roda::RodaPlugins.register_plugin(:rodauth, Rodauth)
@@ -14,15 +14,15 @@ module Rodauth
14
14
  require 'tilt/string'
15
15
  app.plugin :render
16
16
 
17
- case opts.fetch(:csrf, app.opts[:rodauth_route_csrf])
17
+ case opts.fetch(:csrf, app.opts[:rodauth_csrf])
18
18
  when false
19
19
  # nothing
20
- when :route_csrf
21
- app.plugin :route_csrf
22
- else
20
+ when :rack_csrf
23
21
  # :nocov:
24
22
  app.plugin :csrf
25
23
  # :nocov:
24
+ else
25
+ app.plugin :route_csrf
26
26
  end
27
27
 
28
28
  app.plugin :flash unless opts[:flash] == false
@@ -31,8 +31,14 @@ module Rodauth
31
31
  end
32
32
 
33
33
  def self.configure(app, opts={}, &block)
34
- app.opts[:rodauth_json] = opts.fetch(:json, app.opts[:rodauth_json])
35
- app.opts[:rodauth_csrf] = opts.fetch(:csrf, app.opts[:rodauth_route_csrf])
34
+ json_opt = app.opts[:rodauth_json] = opts.fetch(:json, app.opts[:rodauth_json])
35
+ csrf = app.opts[:rodauth_csrf] = opts.fetch(:csrf, app.opts[:rodauth_csrf])
36
+ app.opts[:rodauth_route_csrf] = case csrf
37
+ when false, :rack_csrf
38
+ false
39
+ else
40
+ json_opt != :only
41
+ end
36
42
  auth_class = (app.opts[:rodauths] ||= {})[opts[:name]] ||= Class.new(Auth)
37
43
  if !auth_class.roda_class
38
44
  auth_class.roda_class = app
@@ -72,8 +78,7 @@ module Rodauth
72
78
  end
73
79
 
74
80
  def def_auth_value_method(meth, priv)
75
- define_method(meth) do |*v, &block|
76
- v = v.first
81
+ define_method(meth) do |v=nil, &block|
77
82
  block ||= proc{v}
78
83
  @auth.send(:define_method, meth, &block)
79
84
  @auth.send(:private, meth) if priv
@@ -104,23 +109,17 @@ module Rodauth
104
109
  route_meth = :"#{name}_route"
105
110
  auth_value_method route_meth, default
106
111
 
107
- define_method(:"#{name}_path") { route_path(send(route_meth)) }
108
- define_method(:"#{name}_url") { route_url(send(route_meth)) }
112
+ define_method(:"#{name}_path"){|opts={}| route_path(send(route_meth), opts)}
113
+ define_method(:"#{name}_url"){|opts={}| route_url(send(route_meth), opts)}
109
114
 
110
115
  handle_meth = :"handle_#{name}"
111
116
  internal_handle_meth = :"_#{handle_meth}"
112
117
  before route_meth
113
-
114
- unless block.arity == 1
115
- # :nocov:
116
- b = block
117
- block = lambda{|r| instance_exec(r, &b)}
118
- # :nocov:
119
- end
120
118
  define_method(internal_handle_meth, &block)
121
119
 
122
120
  define_method(handle_meth) do
123
121
  request.is send(route_meth) do
122
+ check_csrf if check_csrf?
124
123
  before_rodauth
125
124
  send(internal_handle_meth, request)
126
125
  end
@@ -138,7 +137,9 @@ module Rodauth
138
137
  feature.module_eval(&block)
139
138
  configuration.def_configuration_methods(feature)
140
139
 
140
+ # :nocov:
141
141
  if constant
142
+ # :nocov:
142
143
  Rodauth.const_set(constant, feature)
143
144
  Rodauth::FeatureConfiguration.const_set(constant, configuration)
144
145
  end
@@ -180,8 +181,10 @@ module Rodauth
180
181
 
181
182
  def view(page, title, name=feature_name)
182
183
  meth = :"#{name}_view"
184
+ title_meth = :"#{name}_page_title"
185
+ translatable_method(title_meth, title)
183
186
  define_method(meth) do
184
- view(page, title)
187
+ view(page, send(title_meth))
185
188
  end
186
189
  auth_methods meth
187
190
  end
@@ -190,6 +193,7 @@ module Rodauth
190
193
  define_method(:loaded_templates) do
191
194
  super().concat(v)
192
195
  end
196
+ private :loaded_templates
193
197
  end
194
198
 
195
199
  def depends(*deps)
@@ -197,10 +201,9 @@ module Rodauth
197
201
  end
198
202
 
199
203
  %w'after before'.each do |hook|
200
- define_method(hook) do |*args|
201
- name = args[0] || feature_name
204
+ define_method(hook) do |name=feature_name|
202
205
  meth = "#{hook}_#{name}"
203
- class_eval("def #{meth}; super if defined?(super); _#{meth} end", __FILE__, __LINE__)
206
+ class_eval("def #{meth}; super if defined?(super); _#{meth}; hook_action(:#{hook}, :#{name}); nil end", __FILE__, __LINE__)
204
207
  class_eval("def _#{meth}; nil end", __FILE__, __LINE__)
205
208
  private meth, :"_#{meth}"
206
209
  auth_private_methods(meth)
@@ -221,6 +224,11 @@ module Rodauth
221
224
  auth_value_methods(meth)
222
225
  end
223
226
 
227
+ def translatable_method(meth, value)
228
+ define_method(meth){translate(meth, value)}
229
+ auth_value_methods(meth)
230
+ end
231
+
224
232
  def auth_cached_method(meth, iv=:"@#{meth}")
225
233
  umeth = :"_#{meth}"
226
234
  define_method(meth) do
@@ -234,9 +242,8 @@ module Rodauth
234
242
  end
235
243
 
236
244
  [:notice_flash, :error_flash, :button].each do |meth|
237
- define_method(meth) do |v, *args|
238
- name = args.shift || feature_name
239
- auth_value_method(:"#{name}_#{meth}", v)
245
+ define_method(meth) do |v, name=feature_name|
246
+ translatable_method(:"#{name}_#{meth}", v)
240
247
  end
241
248
  end
242
249
  end
@@ -331,10 +338,8 @@ module Rodauth
331
338
  end
332
339
 
333
340
  def freeze
334
- if opts[:rodauths]
335
- opts[:rodauths].each_value(&:freeze)
336
- opts[:rodauths].freeze
337
- end
341
+ opts[:rodauths].each_value(&:freeze)
342
+ opts[:rodauths].freeze
338
343
  super
339
344
  end
340
345
  end
@@ -69,6 +69,11 @@ module Rodauth
69
69
  update_last_login
70
70
  end
71
71
 
72
+ def update_session
73
+ check_account_expiration
74
+ super
75
+ end
76
+
72
77
  private
73
78
 
74
79
  def before_reset_password
@@ -96,11 +101,6 @@ module Rodauth
96
101
  account_activity_ds(account_id).delete
97
102
  end
98
103
 
99
- def update_session
100
- check_account_expiration
101
- super
102
- end
103
-
104
104
  def account_activity_ds(account_id)
105
105
  db[account_activity_table].
106
106
  where(account_activity_id_column=>account_id)
@@ -0,0 +1,158 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:active_sessions, :ActiveSessions) do
5
+ depends :logout
6
+
7
+ error_flash 'This session has been logged out'
8
+ redirect
9
+
10
+ session_key :session_id_session_key, :active_session_id
11
+ auth_value_method :active_sessions_account_id_column, :account_id
12
+ auth_value_method :active_sessions_created_at_column, :created_at
13
+ auth_value_method :active_sessions_last_use_column, :last_use
14
+ auth_value_method :active_sessions_session_id_column, :session_id
15
+ auth_value_method :active_sessions_table, :account_active_session_keys
16
+ translatable_method :global_logout_label, 'Logout all Logged In Sessons?'
17
+ auth_value_method :global_logout_param, 'global_logout'
18
+ auth_value_method :inactive_session_error_status, 401
19
+ auth_value_method :session_inactivity_deadline, 86400
20
+ auth_value_method(:session_lifetime_deadline, 86400*30)
21
+
22
+ auth_methods(
23
+ :add_active_session,
24
+ :currently_active_session?,
25
+ :handle_duplicate_active_session_id,
26
+ :no_longer_active_session,
27
+ :remove_all_active_sessions,
28
+ :remove_current_session,
29
+ :remove_inactive_sessions,
30
+ )
31
+
32
+ def currently_active_session?
33
+ return false unless session_id = session[session_id_session_key]
34
+
35
+ remove_inactive_sessions
36
+ ds = active_sessions_ds.
37
+ where(active_sessions_session_id_column => compute_hmac(session_id))
38
+
39
+ if session_inactivity_deadline
40
+ ds.update(active_sessions_last_use_column => Sequel::CURRENT_TIMESTAMP) == 1
41
+ else
42
+ ds.count == 1
43
+ end
44
+ end
45
+
46
+ def check_active_session
47
+ if logged_in? && !currently_active_session?
48
+ no_longer_active_session
49
+ end
50
+ end
51
+
52
+ def no_longer_active_session
53
+ clear_session
54
+ set_redirect_error_status inactive_session_error_status
55
+ set_redirect_error_flash active_sessions_error_flash
56
+ redirect active_sessions_redirect
57
+ end
58
+
59
+ def add_active_session
60
+ key = random_key
61
+ set_session_value(session_id_session_key, key)
62
+ if e = raises_uniqueness_violation? do
63
+ active_sessions_ds.insert(active_sessions_account_id_column => session_value, active_sessions_session_id_column => compute_hmac(key))
64
+ end
65
+ handle_duplicate_active_session_id(e)
66
+ end
67
+ nil
68
+ end
69
+
70
+ def handle_duplicate_active_session_id(_e)
71
+ # Do nothing by default as session is already tracked. This will result in
72
+ # the current session and the existing session with the same id
73
+ # being tracked together, so that a logout of one will logout
74
+ # the other, and updating the last use on one will update the other,
75
+ # but this should be acceptable. However, this can be overridden if different
76
+ # behavior is desired.
77
+ end
78
+
79
+ def remove_current_session
80
+ active_sessions_ds.where(active_sessions_session_id_column=>compute_hmac(session[session_id_session_key])).delete
81
+ end
82
+
83
+ def remove_all_active_sessions
84
+ active_sessions_ds.delete
85
+ end
86
+
87
+ def remove_inactive_sessions
88
+ if cond = inactive_session_cond
89
+ active_sessions_ds.where(cond).delete
90
+ end
91
+ end
92
+
93
+ def logout_additional_form_tags
94
+ super.to_s + render('global-logout-field')
95
+ end
96
+
97
+ def update_session
98
+ super
99
+ add_active_session
100
+ end
101
+
102
+ private
103
+
104
+ def after_refresh_token
105
+ super if defined?(super)
106
+ if prev_key = session[session_id_session_key]
107
+ key = random_key
108
+ set_session_value(session_id_session_key, key)
109
+ active_sessions_ds.
110
+ where(active_sessions_session_id_column => compute_hmac(prev_key)).
111
+ update(active_sessions_session_id_column => compute_hmac(key))
112
+ end
113
+ end
114
+
115
+ def after_close_account
116
+ super if defined?(super)
117
+ remove_all_active_sessions
118
+ end
119
+
120
+ def before_logout
121
+ if param_or_nil(global_logout_param)
122
+ remove_all_active_sessions
123
+ else
124
+ remove_current_session
125
+ end
126
+ super
127
+ end
128
+
129
+ def session_inactivity_deadline_condition
130
+ if deadline = session_inactivity_deadline
131
+ Sequel[active_sessions_last_use_column] < Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, seconds: deadline)
132
+ end
133
+ end
134
+
135
+ def session_lifetime_deadline_condition
136
+ if deadline = session_lifetime_deadline
137
+ Sequel[active_sessions_created_at_column] < Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, seconds: deadline)
138
+ end
139
+ end
140
+
141
+ def inactive_session_cond
142
+ cond = session_inactivity_deadline_condition
143
+ cond2 = session_lifetime_deadline_condition
144
+ return false unless cond || cond2
145
+ Sequel.|(*[cond, cond2].compact)
146
+ end
147
+
148
+ def active_sessions_ds
149
+ db[active_sessions_table].
150
+ where(active_sessions_account_id_column=>session_value)
151
+ end
152
+
153
+ def use_date_arithmetic?
154
+ true
155
+ end
156
+ end
157
+ end
158
+
@@ -0,0 +1,98 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:audit_logging, :AuditLogging) do
5
+ auth_value_method :audit_logging_account_id_column, :account_id
6
+ auth_value_method :audit_logging_message_column, :message
7
+ auth_value_method :audit_logging_metadata_column, :metadata
8
+ auth_value_method :audit_logging_table, :account_authentication_audit_logs
9
+ auth_value_method :audit_log_metadata_default, nil
10
+
11
+ auth_methods(
12
+ :add_audit_log,
13
+ :audit_log_insert_hash,
14
+ :audit_log_message,
15
+ :audit_log_message_default,
16
+ :audit_log_metadata,
17
+ :serialize_audit_log_metadata,
18
+ )
19
+
20
+ configuration_module_eval do
21
+ [:audit_log_message_for, :audit_log_metadata_for].each do |method|
22
+ define_method(method) do |action, value=nil, &block|
23
+ block ||= proc{value}
24
+ meth = :"#{method}_#{action}"
25
+ @auth.send(:define_method, meth, &block)
26
+ @auth.send(:private, meth)
27
+ end
28
+ end
29
+ end
30
+
31
+ def hook_action(hook_type, action)
32
+ super
33
+ # In after_logout, session is already cleared, so use before_logout in that case
34
+ if (hook_type == :after || action == :logout) && (id = account ? account_id : session_value)
35
+ add_audit_log(id, action)
36
+ end
37
+ end
38
+
39
+ def add_audit_log(account_id, action)
40
+ if hash = audit_log_insert_hash(account_id, action)
41
+ audit_log_ds.insert(hash)
42
+ end
43
+ end
44
+
45
+ def audit_log_insert_hash(account_id, action)
46
+ if message = audit_log_message(action)
47
+ {
48
+ audit_logging_account_id_column => account_id,
49
+ audit_logging_message_column => message,
50
+ audit_logging_metadata_column => serialize_audit_log_metadata(audit_log_metadata(action))
51
+ }
52
+ end
53
+ end
54
+
55
+ def serialize_audit_log_metadata(metadata)
56
+ metadata.to_json unless metadata.nil?
57
+ end
58
+
59
+ def audit_log_message_default(action)
60
+ action.to_s
61
+ end
62
+
63
+ def audit_log_message(action)
64
+ meth = :"audit_log_message_for_#{action}"
65
+ if respond_to?(meth, true)
66
+ send(meth)
67
+ else
68
+ audit_log_message_default(action)
69
+ end
70
+ end
71
+
72
+ def audit_log_metadata(action)
73
+ meth = :"audit_log_metadata_for_#{action}"
74
+ if respond_to?(meth, true)
75
+ send(meth)
76
+ else
77
+ audit_log_metadata_default
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def audit_log_ds
84
+ ds = db[audit_logging_table]
85
+ # :nocov:
86
+ if db.database_type == :postgres
87
+ # :nocov:
88
+ # For PostgreSQL, use RETURNING NULL. This allows the feature
89
+ # to be used with INSERT but not SELECT permissions on the
90
+ # table, useful for audit logging where the database user
91
+ # the application is running as should not need to read the
92
+ # logs.
93
+ ds = ds.returning(nil)
94
+ end
95
+ ds
96
+ end
97
+ end
98
+ end
@@ -18,17 +18,19 @@ module Rodauth
18
18
  auth_value_method :account_unverified_status_value, 1
19
19
  auth_value_method :accounts_table, :accounts
20
20
  auth_value_method :cache_templates, true
21
+ auth_value_method :check_csrf_block, nil
22
+ auth_value_method :check_csrf_opts, {}.freeze
21
23
  auth_value_method :default_redirect, '/'
22
24
  session_key :flash_error_key, :error
23
25
  session_key :flash_notice_key, :notice
24
26
  auth_value_method :hmac_secret, nil
25
- auth_value_method :input_field_label_suffix, ''
26
- auth_value_method :input_field_error_class, 'error'
27
- auth_value_method :input_field_error_message_class, 'error_message'
27
+ translatable_method :input_field_label_suffix, ''
28
+ auth_value_method :input_field_error_class, 'error is-invalid'
29
+ auth_value_method :input_field_error_message_class, 'error_message invalid-feedback'
28
30
  auth_value_method :invalid_field_error_status, 422
29
31
  auth_value_method :invalid_key_error_status, 401
30
32
  auth_value_method :invalid_password_error_status, 401
31
- auth_value_method :invalid_password_message, "invalid password"
33
+ translatable_method :invalid_password_message, "invalid password"
32
34
  auth_value_method :login_column, :email
33
35
  auth_value_method :login_required_error_status, 401
34
36
  auth_value_method :lockout_error_status, 403
@@ -36,30 +38,39 @@ module Rodauth
36
38
  auth_value_method :password_hash_column, :password_hash
37
39
  auth_value_method :password_hash_table, :account_password_hashes
38
40
  auth_value_method :no_matching_login_error_status, 401
39
- auth_value_method :no_matching_login_message, "no matching login"
41
+ translatable_method :no_matching_login_message, "no matching login"
40
42
  auth_value_method :login_param, 'login'
41
- auth_value_method :login_label, 'Login'
42
- auth_value_method :login_input_type, 'text'
43
- auth_value_method :password_label, 'Password'
43
+ translatable_method :login_label, 'Login'
44
+ translatable_method :password_label, 'Password'
44
45
  auth_value_method :password_param, 'password'
45
- auth_value_method :modifications_require_password?, true
46
46
  session_key :session_key, :account_id
47
+ session_key :authenticated_by_session_key, :authenticated_by
48
+ session_key :autologin_type_session_key, :autologin_type
47
49
  auth_value_method :prefix, ''
50
+ auth_value_method :session_key_prefix, nil
48
51
  auth_value_method :require_bcrypt?, true
49
- auth_value_method :mark_input_fields_as_required?, false
52
+ auth_value_method :mark_input_fields_as_required?, true
53
+ auth_value_method :mark_input_fields_with_autocomplete?, true
54
+ auth_value_method :mark_input_fields_with_inputmode?, true
50
55
  auth_value_method :skip_status_checks?, true
51
- auth_value_method :template_opts, {}
56
+ auth_value_method :template_opts, {}.freeze
52
57
  auth_value_method :title_instance_variable, nil
53
58
  auth_value_method :token_separator, "_"
54
59
  auth_value_method :unmatched_field_error_status, 422
55
60
  auth_value_method :unopen_account_error_status, 403
56
- auth_value_method :unverified_account_message, "unverified account, please verify account before logging in"
61
+ translatable_method :unverified_account_message, "unverified account, please verify account before logging in"
62
+ auth_value_method :default_field_attributes, ''
57
63
 
58
64
  redirect(:require_login){"#{prefix}/login"}
59
65
 
60
66
  auth_value_methods(
67
+ :base_url,
68
+ :check_csrf?,
61
69
  :db,
62
- :default_field_attributes,
70
+ :domain,
71
+ :login_input_type,
72
+ :login_uses_email?,
73
+ :modifications_require_password?,
63
74
  :set_deadline_values?,
64
75
  :use_date_arithmetic?,
65
76
  :use_database_authentication_functions?,
@@ -71,9 +82,13 @@ module Rodauth
71
82
  :account_session_value,
72
83
  :already_logged_in,
73
84
  :authenticated?,
85
+ :autocomplete_for_field?,
86
+ :check_csrf,
74
87
  :clear_session,
75
88
  :csrf_tag,
76
89
  :function_name,
90
+ :hook_action,
91
+ :inputmode_for_field?,
77
92
  :logged_in?,
78
93
  :login_required,
79
94
  :open_account?,
@@ -86,6 +101,7 @@ module Rodauth
86
101
  :set_notice_now_flash,
87
102
  :set_redirect_error_flash,
88
103
  :set_title,
104
+ :translate,
89
105
  :unverified_account_message,
90
106
  :update_session
91
107
  )
@@ -102,13 +118,6 @@ module Rodauth
102
118
  def auth_class_eval(&block)
103
119
  auth.class_eval(&block)
104
120
  end
105
-
106
- def account_model(model)
107
- warn "account_model is deprecated, use db and accounts_table settings"
108
- db model.db
109
- accounts_table model.table_name
110
- account_select model.dataset.opts[:select]
111
- end
112
121
  end
113
122
 
114
123
  attr_reader :scope
@@ -168,13 +177,29 @@ module Rodauth
168
177
  value = opts.fetch(:value){scope.h param(param)}
169
178
  end
170
179
 
171
- "<input #{opts[:attr]} #{field_attributes(param)} #{field_error_attributes(param)} type=\"#{type}\" class=\"form-control#{add_field_error_class(param)}\" name=\"#{param}\" id=\"#{id}\" value=\"#{value}\"/> #{formatted_field_error(param)}"
172
- end
180
+ field_class = opts.fetch(:class, "form-control")
181
+
182
+ if autocomplete_for_field?(param) && opts[:autocomplete]
183
+ autocomplete = "autocomplete=\"#{opts[:autocomplete]}\""
184
+ end
185
+
186
+ if inputmode_for_field?(param) && opts[:inputmode]
187
+ inputmode = "inputmode=\"#{opts[:inputmode]}\""
188
+ end
173
189
 
174
- def default_field_attributes
175
- if mark_input_fields_as_required?
176
- "required=\"required\""
190
+ if mark_input_fields_as_required? && opts[:required] != false
191
+ required = "required=\"required\""
177
192
  end
193
+
194
+ "<input #{opts[:attr]} #{autocomplete} #{inputmode} #{required} #{field_attributes(param)} #{field_error_attributes(param)} type=\"#{type}\" class=\"#{field_class}#{add_field_error_class(param)}\" name=\"#{param}\" id=\"#{id}\" value=\"#{value}\"/> #{formatted_field_error(param) unless opts[:skip_error_message]}"
195
+ end
196
+
197
+ def autocomplete_for_field?(_param)
198
+ mark_input_fields_with_autocomplete?
199
+ end
200
+
201
+ def inputmode_for_field?(_param)
202
+ mark_input_fields_with_inputmode?
178
203
  end
179
204
 
180
205
  def field_attributes(field)
@@ -193,6 +218,15 @@ module Rodauth
193
218
  end
194
219
  end
195
220
 
221
+ def hook_action(_hook_type, _action)
222
+ # nothing by default
223
+ end
224
+
225
+ def translate(_key, default)
226
+ # do not attempt to translate by default
227
+ default
228
+ end
229
+
196
230
  # Return urlsafe base64 HMAC for data, assumes hmac_secret is set.
197
231
  def compute_hmac(data)
198
232
  s = [compute_raw_hmac(data)].pack('m').chomp!("=\n")
@@ -222,6 +256,10 @@ module Rodauth
222
256
  Sequel::DATABASES.first
223
257
  end
224
258
 
259
+ def password_field_autocomplete_value
260
+ @password_field_autocomplete_value || 'current-password'
261
+ end
262
+
225
263
  # If the account_password_hash_column is set, the password hash is verified in
226
264
  # ruby, it will not use a database function to do so, it will check the password
227
265
  # hash using bcrypt.
@@ -237,6 +275,14 @@ module Rodauth
237
275
  nil
238
276
  end
239
277
 
278
+ def login_input_type
279
+ login_uses_email? ? 'email' : 'text'
280
+ end
281
+
282
+ def login_uses_email?
283
+ login_column == :email
284
+ end
285
+
240
286
  def clear_session
241
287
  if scope.respond_to?(:clear_session)
242
288
  scope.clear_session
@@ -293,6 +339,10 @@ module Rodauth
293
339
  @account = _account_from_session
294
340
  end
295
341
 
342
+ def check_csrf
343
+ scope.check_csrf!(check_csrf_opts, &check_csrf_block)
344
+ end
345
+
296
346
  def csrf_tag(path=request.path)
297
347
  return unless scope.respond_to?(:csrf_tag)
298
348
 
@@ -344,16 +394,34 @@ module Rodauth
344
394
  def password_match?(password)
345
395
  if hash = get_password_hash
346
396
  if account_password_hash_column || !use_database_authentication_functions?
347
- BCrypt::Password.new(hash) == password
397
+ password_hash_match?(hash, password)
348
398
  else
349
- db.get(Sequel.function(function_name(:rodauth_valid_password_hash), account_id, BCrypt::Engine.hash_secret(password, hash)))
399
+ database_function_password_match?(:rodauth_valid_password_hash, account_id, password, hash)
350
400
  end
351
401
  end
352
402
  end
353
403
 
354
404
  def update_session
355
405
  clear_session
356
- session[session_key] = account_session_value
406
+ set_session_value(session_key, account_session_value)
407
+ end
408
+
409
+ def authenticated_by
410
+ session[authenticated_by_session_key]
411
+ end
412
+
413
+ def login_session(auth_type)
414
+ update_session
415
+ set_session_value(authenticated_by_session_key, [auth_type])
416
+ end
417
+
418
+ def autologin_type
419
+ session[autologin_type_session_key]
420
+ end
421
+
422
+ def autologin_session(autologin_type)
423
+ login_session('autologin')
424
+ set_session_value(autologin_type_session_key, autologin_type)
357
425
  end
358
426
 
359
427
  # Return a string for the parameter name. This will be an empty
@@ -365,12 +433,40 @@ module Rodauth
365
433
  # Return a string for the parameter name, or nil if there is no
366
434
  # parameter with that name.
367
435
  def param_or_nil(key)
368
- value = request.params[key]
436
+ value = raw_param(key)
369
437
  value.to_s unless value.nil?
370
438
  end
371
439
 
440
+ def raw_param(key)
441
+ request.params[key]
442
+ end
443
+
444
+ def base_url
445
+ request.base_url
446
+ end
447
+
448
+ def domain
449
+ request.host
450
+ end
451
+
452
+ def modifications_require_password?
453
+ has_password?
454
+ end
455
+
456
+ def possible_authentication_methods
457
+ has_password? ? ['password'] : []
458
+ end
459
+
372
460
  private
373
461
 
462
+ def database_function_password_match?(name, hash_id, password, salt)
463
+ db.get(Sequel.function(function_name(name), hash_id, BCrypt::Engine.hash_secret(password, salt)))
464
+ end
465
+
466
+ def password_hash_match?(hash, password)
467
+ BCrypt::Password.new(hash) == password
468
+ end
469
+
374
470
  def convert_token_key(key)
375
471
  if key && hmac_secret
376
472
  compute_hmac(key)
@@ -387,33 +483,26 @@ module Rodauth
387
483
  request.redirect(path)
388
484
  end
389
485
 
390
- def route_path(route)
391
- "#{prefix}/#{route}"
486
+ def route_path(route, opts={})
487
+ path = "#{prefix}/#{route}"
488
+ path += "?#{Rack::Utils.build_nested_query(opts)}" unless opts.empty?
489
+ path
392
490
  end
393
491
 
394
- def route_url(route)
395
- "#{request.base_url}#{route_path(route)}"
492
+ def route_url(route, opts={})
493
+ "#{base_url}#{route_path(route, opts)}"
396
494
  end
397
495
 
398
496
  def transaction(opts={}, &block)
399
497
  db.transaction(opts, &block)
400
498
  end
401
499
 
402
- if RUBY_VERSION >= '1.9'
403
- def random_key
404
- SecureRandom.urlsafe_base64(32)
405
- end
406
- else
407
- # :nocov:
408
- def random_key
409
- s = [SecureRandom.random_bytes(32)].pack('m').chomp!("=\n")
410
- s.tr!('+/', '-_')
411
- s
412
- end
413
- # :nocov:
500
+ def random_key
501
+ SecureRandom.urlsafe_base64(32)
414
502
  end
415
503
 
416
504
  def convert_session_key(key)
505
+ key = "#{session_key_prefix}#{key}".to_sym if session_key_prefix
417
506
  scope.opts[:sessions_convert_symbols] ? key.to_s : key
418
507
  end
419
508
 
@@ -476,7 +565,11 @@ module Rodauth
476
565
  end
477
566
 
478
567
  def use_request_specific_csrf_tokens?
479
- scope.opts[:rodauth_csrf] == :route_csrf && scope.use_request_specific_csrf_tokens?
568
+ scope.opts[:rodauth_route_csrf] && scope.use_request_specific_csrf_tokens?
569
+ end
570
+
571
+ def check_csrf?
572
+ scope.opts[:rodauth_route_csrf]
480
573
  end
481
574
 
482
575
  def function_name(name)
@@ -489,13 +582,19 @@ module Rodauth
489
582
  end
490
583
  end
491
584
 
585
+ def has_password?
586
+ return @has_password if defined?(@has_password)
587
+ return false unless account || session_value
588
+ @has_password = !!get_password_hash
589
+ end
590
+
492
591
  # Get the password hash for the user. When using database authentication functions,
493
592
  # note that only the salt is returned.
494
593
  def get_password_hash
495
594
  if account_password_hash_column
496
- account[account_password_hash_column]
595
+ (account || account_from_session)[account_password_hash_column]
497
596
  elsif use_database_authentication_functions?
498
- db.get(Sequel.function(function_name(:rodauth_get_salt), account_id))
597
+ db.get(Sequel.function(function_name(:rodauth_get_salt), account ? account_id : session_value))
499
598
  else
500
599
  # :nocov:
501
600
  password_hash_ds.get(password_hash_column)
@@ -548,7 +647,7 @@ module Rodauth
548
647
  end
549
648
 
550
649
  def password_hash_ds
551
- db[password_hash_table].where(password_hash_id_column=>account_id)
650
+ db[password_hash_table].where(password_hash_id_column=>account ? account_id : session_value)
552
651
  end
553
652
 
554
653
  # This is needed for jdbc/sqlite, which returns timestamp columns as strings
@@ -615,6 +714,10 @@ module Rodauth
615
714
  session[key] = value
616
715
  end
617
716
 
717
+ def remove_session_value(key)
718
+ session.delete(key)
719
+ end
720
+
618
721
  def update_hash_ds(hash, ds, values)
619
722
  num = ds.update(values)
620
723
  if num == 1