rodauth 1.22.0 → 2.3.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +190 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +210 -80
  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 +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/recovery_codes.rdoc +18 -12
  52. data/doc/release_notes/1.23.0.txt +32 -0
  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/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/close_account.rb +8 -6
  79. data/lib/rodauth/features/confirm_password.rb +40 -2
  80. data/lib/rodauth/features/create_account.rb +8 -13
  81. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  82. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  83. data/lib/rodauth/features/email_auth.rb +31 -30
  84. data/lib/rodauth/features/email_base.rb +9 -4
  85. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  86. data/lib/rodauth/features/jwt.rb +63 -16
  87. data/lib/rodauth/features/jwt_cors.rb +15 -15
  88. data/lib/rodauth/features/jwt_refresh.rb +42 -13
  89. data/lib/rodauth/features/lockout.rb +12 -14
  90. data/lib/rodauth/features/login.rb +64 -15
  91. data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
  92. data/lib/rodauth/features/otp.rb +77 -80
  93. data/lib/rodauth/features/password_complexity.rb +8 -13
  94. data/lib/rodauth/features/password_expiration.rb +2 -2
  95. data/lib/rodauth/features/password_grace_period.rb +17 -10
  96. data/lib/rodauth/features/recovery_codes.rb +49 -53
  97. data/lib/rodauth/features/remember.rb +11 -27
  98. data/lib/rodauth/features/reset_password.rb +26 -26
  99. data/lib/rodauth/features/session_expiration.rb +7 -10
  100. data/lib/rodauth/features/single_session.rb +8 -6
  101. data/lib/rodauth/features/sms_codes.rb +62 -72
  102. data/lib/rodauth/features/two_factor_base.rb +134 -30
  103. data/lib/rodauth/features/verify_account.rb +29 -21
  104. data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
  105. data/lib/rodauth/features/verify_login_change.rb +12 -11
  106. data/lib/rodauth/features/webauthn.rb +505 -0
  107. data/lib/rodauth/features/webauthn_login.rb +70 -0
  108. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  109. data/lib/rodauth/migrations.rb +16 -5
  110. data/lib/rodauth/version.rb +2 -2
  111. data/templates/button.str +1 -3
  112. data/templates/change-login.str +1 -2
  113. data/templates/change-password.str +3 -5
  114. data/templates/close-account.str +2 -2
  115. data/templates/confirm-password.str +1 -1
  116. data/templates/create-account.str +1 -1
  117. data/templates/email-auth-request-form.str +2 -3
  118. data/templates/email-auth.str +1 -1
  119. data/templates/global-logout-field.str +6 -0
  120. data/templates/login-confirm-field.str +2 -4
  121. data/templates/login-display.str +3 -2
  122. data/templates/login-field.str +2 -4
  123. data/templates/login-form-footer.str +6 -0
  124. data/templates/login-form.str +7 -0
  125. data/templates/login.str +1 -9
  126. data/templates/logout.str +1 -1
  127. data/templates/multi-phase-login.str +3 -0
  128. data/templates/otp-auth-code-field.str +5 -3
  129. data/templates/otp-auth.str +1 -1
  130. data/templates/otp-disable.str +1 -1
  131. data/templates/otp-setup.str +3 -3
  132. data/templates/password-confirm-field.str +2 -4
  133. data/templates/password-field.str +2 -4
  134. data/templates/recovery-auth.str +3 -6
  135. data/templates/recovery-codes.str +1 -1
  136. data/templates/remember.str +15 -20
  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-request.str +4 -4
  149. data/templates/unlock-account.str +1 -1
  150. data/templates/verify-account-resend.str +3 -3
  151. data/templates/verify-account.str +1 -2
  152. data/templates/verify-login-change.str +1 -1
  153. data/templates/webauthn-auth.str +11 -0
  154. data/templates/webauthn-remove.str +14 -0
  155. data/templates/webauthn-setup.str +12 -0
  156. metadata +94 -54
  157. data/Rakefile +0 -179
  158. data/doc/verify_change_login.rdoc +0 -11
  159. data/lib/rodauth/features/verify_change_login.rb +0 -20
  160. data/spec/account_expiration_spec.rb +0 -225
  161. data/spec/all.rb +0 -1
  162. data/spec/change_login_spec.rb +0 -156
  163. data/spec/change_password_notify_spec.rb +0 -33
  164. data/spec/change_password_spec.rb +0 -202
  165. data/spec/close_account_spec.rb +0 -162
  166. data/spec/confirm_password_spec.rb +0 -70
  167. data/spec/create_account_spec.rb +0 -127
  168. data/spec/disallow_common_passwords_spec.rb +0 -93
  169. data/spec/disallow_password_reuse_spec.rb +0 -179
  170. data/spec/email_auth_spec.rb +0 -285
  171. data/spec/http_basic_auth_spec.rb +0 -143
  172. data/spec/jwt_cors_spec.rb +0 -57
  173. data/spec/jwt_refresh_spec.rb +0 -256
  174. data/spec/jwt_spec.rb +0 -235
  175. data/spec/lockout_spec.rb +0 -250
  176. data/spec/login_spec.rb +0 -328
  177. data/spec/migrate/001_tables.rb +0 -184
  178. data/spec/migrate/002_account_password_hash_column.rb +0 -11
  179. data/spec/migrate_password/001_tables.rb +0 -73
  180. data/spec/migrate_travis/001_tables.rb +0 -141
  181. data/spec/password_complexity_spec.rb +0 -109
  182. data/spec/password_expiration_spec.rb +0 -244
  183. data/spec/password_grace_period_spec.rb +0 -93
  184. data/spec/remember_spec.rb +0 -451
  185. data/spec/reset_password_spec.rb +0 -229
  186. data/spec/rodauth_spec.rb +0 -343
  187. data/spec/session_expiration_spec.rb +0 -58
  188. data/spec/single_session_spec.rb +0 -127
  189. data/spec/spec_helper.rb +0 -327
  190. data/spec/two_factor_spec.rb +0 -1462
  191. data/spec/update_password_hash_spec.rb +0 -40
  192. data/spec/verify_account_grace_period_spec.rb +0 -171
  193. data/spec/verify_account_spec.rb +0 -240
  194. data/spec/verify_change_login_spec.rb +0 -46
  195. data/spec/verify_login_change_spec.rb +0 -232
  196. data/spec/views/layout-other.str +0 -11
  197. data/spec/views/layout.str +0 -11
  198. data/spec/views/login.str +0 -21
@@ -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
@@ -101,23 +106,20 @@ module Rodauth
101
106
  attr_accessor :configuration
102
107
 
103
108
  def route(name=feature_name, default=name.to_s.tr('_', '-'), &block)
104
- auth_value_method "#{name}_route", default
109
+ route_meth = :"#{name}_route"
110
+ auth_value_method route_meth, default
111
+
112
+ define_method(:"#{name}_path"){|opts={}| route_path(send(route_meth), opts)}
113
+ define_method(:"#{name}_url"){|opts={}| route_url(send(route_meth), opts)}
105
114
 
106
115
  handle_meth = :"handle_#{name}"
107
116
  internal_handle_meth = :"_#{handle_meth}"
108
- route_meth = :"#{name}_route"
109
117
  before route_meth
110
-
111
- unless block.arity == 1
112
- # :nocov:
113
- b = block
114
- block = lambda{|r| instance_exec(r, &b)}
115
- # :nocov:
116
- end
117
118
  define_method(internal_handle_meth, &block)
118
119
 
119
120
  define_method(handle_meth) do
120
121
  request.is send(route_meth) do
122
+ check_csrf if check_csrf?
121
123
  before_rodauth
122
124
  send(internal_handle_meth, request)
123
125
  end
@@ -135,7 +137,9 @@ module Rodauth
135
137
  feature.module_eval(&block)
136
138
  configuration.def_configuration_methods(feature)
137
139
 
140
+ # :nocov:
138
141
  if constant
142
+ # :nocov:
139
143
  Rodauth.const_set(constant, feature)
140
144
  Rodauth::FeatureConfiguration.const_set(constant, configuration)
141
145
  end
@@ -177,8 +181,10 @@ module Rodauth
177
181
 
178
182
  def view(page, title, name=feature_name)
179
183
  meth = :"#{name}_view"
184
+ title_meth = :"#{name}_page_title"
185
+ translatable_method(title_meth, title)
180
186
  define_method(meth) do
181
- view(page, title)
187
+ view(page, send(title_meth))
182
188
  end
183
189
  auth_methods meth
184
190
  end
@@ -187,6 +193,7 @@ module Rodauth
187
193
  define_method(:loaded_templates) do
188
194
  super().concat(v)
189
195
  end
196
+ private :loaded_templates
190
197
  end
191
198
 
192
199
  def depends(*deps)
@@ -194,10 +201,9 @@ module Rodauth
194
201
  end
195
202
 
196
203
  %w'after before'.each do |hook|
197
- define_method(hook) do |*args|
198
- name = args[0] || feature_name
204
+ define_method(hook) do |name=feature_name|
199
205
  meth = "#{hook}_#{name}"
200
- 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__)
201
207
  class_eval("def _#{meth}; nil end", __FILE__, __LINE__)
202
208
  private meth, :"_#{meth}"
203
209
  auth_private_methods(meth)
@@ -218,6 +224,11 @@ module Rodauth
218
224
  auth_value_methods(meth)
219
225
  end
220
226
 
227
+ def translatable_method(meth, value)
228
+ define_method(meth){translate(meth, value)}
229
+ auth_value_methods(meth)
230
+ end
231
+
221
232
  def auth_cached_method(meth, iv=:"@#{meth}")
222
233
  umeth = :"_#{meth}"
223
234
  define_method(meth) do
@@ -231,9 +242,8 @@ module Rodauth
231
242
  end
232
243
 
233
244
  [:notice_flash, :error_flash, :button].each do |meth|
234
- define_method(meth) do |v, *args|
235
- name = args.shift || feature_name
236
- auth_value_method(:"#{name}_#{meth}", v)
245
+ define_method(meth) do |v, name=feature_name|
246
+ translatable_method(:"#{name}_#{meth}", v)
237
247
  end
238
248
  end
239
249
  end
@@ -328,10 +338,8 @@ module Rodauth
328
338
  end
329
339
 
330
340
  def freeze
331
- if opts[:rodauths]
332
- opts[:rodauths].each_value(&:freeze)
333
- opts[:rodauths].freeze
334
- end
341
+ opts[:rodauths].each_value(&:freeze)
342
+ opts[:rodauths].freeze
335
343
  super
336
344
  end
337
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,38 @@ 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, ''
48
50
  auth_value_method :require_bcrypt?, true
49
- auth_value_method :mark_input_fields_as_required?, false
51
+ auth_value_method :mark_input_fields_as_required?, true
52
+ auth_value_method :mark_input_fields_with_autocomplete?, true
53
+ auth_value_method :mark_input_fields_with_inputmode?, true
50
54
  auth_value_method :skip_status_checks?, true
51
- auth_value_method :template_opts, {}
55
+ auth_value_method :template_opts, {}.freeze
52
56
  auth_value_method :title_instance_variable, nil
53
57
  auth_value_method :token_separator, "_"
54
58
  auth_value_method :unmatched_field_error_status, 422
55
59
  auth_value_method :unopen_account_error_status, 403
56
- auth_value_method :unverified_account_message, "unverified account, please verify account before logging in"
60
+ translatable_method :unverified_account_message, "unverified account, please verify account before logging in"
61
+ auth_value_method :default_field_attributes, ''
57
62
 
58
63
  redirect(:require_login){"#{prefix}/login"}
59
64
 
60
65
  auth_value_methods(
66
+ :base_url,
67
+ :check_csrf?,
61
68
  :db,
62
- :default_field_attributes,
69
+ :domain,
70
+ :login_input_type,
71
+ :login_uses_email?,
72
+ :modifications_require_password?,
63
73
  :set_deadline_values?,
64
74
  :use_date_arithmetic?,
65
75
  :use_database_authentication_functions?,
@@ -71,9 +81,13 @@ module Rodauth
71
81
  :account_session_value,
72
82
  :already_logged_in,
73
83
  :authenticated?,
84
+ :autocomplete_for_field?,
85
+ :check_csrf,
74
86
  :clear_session,
75
87
  :csrf_tag,
76
88
  :function_name,
89
+ :hook_action,
90
+ :inputmode_for_field?,
77
91
  :logged_in?,
78
92
  :login_required,
79
93
  :open_account?,
@@ -86,6 +100,7 @@ module Rodauth
86
100
  :set_notice_now_flash,
87
101
  :set_redirect_error_flash,
88
102
  :set_title,
103
+ :translate,
89
104
  :unverified_account_message,
90
105
  :update_session
91
106
  )
@@ -102,13 +117,6 @@ module Rodauth
102
117
  def auth_class_eval(&block)
103
118
  auth.class_eval(&block)
104
119
  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
120
  end
113
121
 
114
122
  attr_reader :scope
@@ -168,13 +176,29 @@ module Rodauth
168
176
  value = opts.fetch(:value){scope.h param(param)}
169
177
  end
170
178
 
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
179
+ field_class = opts.fetch(:class, "form-control")
180
+
181
+ if autocomplete_for_field?(param) && opts[:autocomplete]
182
+ autocomplete = "autocomplete=\"#{opts[:autocomplete]}\""
183
+ end
184
+
185
+ if inputmode_for_field?(param) && opts[:inputmode]
186
+ inputmode = "inputmode=\"#{opts[:inputmode]}\""
187
+ end
173
188
 
174
- def default_field_attributes
175
- if mark_input_fields_as_required?
176
- "required=\"required\""
189
+ if mark_input_fields_as_required? && opts[:required] != false
190
+ required = "required=\"required\""
177
191
  end
192
+
193
+ "<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]}"
194
+ end
195
+
196
+ def autocomplete_for_field?(_param)
197
+ mark_input_fields_with_autocomplete?
198
+ end
199
+
200
+ def inputmode_for_field?(_param)
201
+ mark_input_fields_with_inputmode?
178
202
  end
179
203
 
180
204
  def field_attributes(field)
@@ -193,6 +217,15 @@ module Rodauth
193
217
  end
194
218
  end
195
219
 
220
+ def hook_action(_hook_type, _action)
221
+ # nothing by default
222
+ end
223
+
224
+ def translate(_key, default)
225
+ # do not attempt to translate by default
226
+ default
227
+ end
228
+
196
229
  # Return urlsafe base64 HMAC for data, assumes hmac_secret is set.
197
230
  def compute_hmac(data)
198
231
  s = [compute_raw_hmac(data)].pack('m').chomp!("=\n")
@@ -222,6 +255,10 @@ module Rodauth
222
255
  Sequel::DATABASES.first
223
256
  end
224
257
 
258
+ def password_field_autocomplete_value
259
+ @password_field_autocomplete_value || 'current-password'
260
+ end
261
+
225
262
  # If the account_password_hash_column is set, the password hash is verified in
226
263
  # ruby, it will not use a database function to do so, it will check the password
227
264
  # hash using bcrypt.
@@ -237,6 +274,14 @@ module Rodauth
237
274
  nil
238
275
  end
239
276
 
277
+ def login_input_type
278
+ login_uses_email? ? 'email' : 'text'
279
+ end
280
+
281
+ def login_uses_email?
282
+ login_column == :email
283
+ end
284
+
240
285
  def clear_session
241
286
  if scope.respond_to?(:clear_session)
242
287
  scope.clear_session
@@ -293,6 +338,10 @@ module Rodauth
293
338
  @account = _account_from_session
294
339
  end
295
340
 
341
+ def check_csrf
342
+ scope.check_csrf!(check_csrf_opts, &check_csrf_block)
343
+ end
344
+
296
345
  def csrf_tag(path=request.path)
297
346
  return unless scope.respond_to?(:csrf_tag)
298
347
 
@@ -353,7 +402,25 @@ module Rodauth
353
402
 
354
403
  def update_session
355
404
  clear_session
356
- session[session_key] = account_session_value
405
+ set_session_value(session_key, account_session_value)
406
+ end
407
+
408
+ def authenticated_by
409
+ session[authenticated_by_session_key]
410
+ end
411
+
412
+ def login_session(auth_type)
413
+ update_session
414
+ set_session_value(authenticated_by_session_key, [auth_type])
415
+ end
416
+
417
+ def autologin_type
418
+ session[autologin_type_session_key]
419
+ end
420
+
421
+ def autologin_session(autologin_type)
422
+ login_session('autologin')
423
+ set_session_value(autologin_type_session_key, autologin_type)
357
424
  end
358
425
 
359
426
  # Return a string for the parameter name. This will be an empty
@@ -365,10 +432,30 @@ module Rodauth
365
432
  # Return a string for the parameter name, or nil if there is no
366
433
  # parameter with that name.
367
434
  def param_or_nil(key)
368
- value = request.params[key]
435
+ value = raw_param(key)
369
436
  value.to_s unless value.nil?
370
437
  end
371
438
 
439
+ def raw_param(key)
440
+ request.params[key]
441
+ end
442
+
443
+ def base_url
444
+ request.base_url
445
+ end
446
+
447
+ def domain
448
+ request.host
449
+ end
450
+
451
+ def modifications_require_password?
452
+ has_password?
453
+ end
454
+
455
+ def possible_authentication_methods
456
+ has_password? ? ['password'] : []
457
+ end
458
+
372
459
  private
373
460
 
374
461
  def convert_token_key(key)
@@ -387,22 +474,22 @@ module Rodauth
387
474
  request.redirect(path)
388
475
  end
389
476
 
477
+ def route_path(route, opts={})
478
+ path = "#{prefix}/#{route}"
479
+ path += "?#{Rack::Utils.build_nested_query(opts)}" unless opts.empty?
480
+ path
481
+ end
482
+
483
+ def route_url(route, opts={})
484
+ "#{base_url}#{route_path(route, opts)}"
485
+ end
486
+
390
487
  def transaction(opts={}, &block)
391
488
  db.transaction(opts, &block)
392
489
  end
393
490
 
394
- if RUBY_VERSION >= '1.9'
395
- def random_key
396
- SecureRandom.urlsafe_base64(32)
397
- end
398
- else
399
- # :nocov:
400
- def random_key
401
- s = [SecureRandom.random_bytes(32)].pack('m').chomp!("=\n")
402
- s.tr!('+/', '-_')
403
- s
404
- end
405
- # :nocov:
491
+ def random_key
492
+ SecureRandom.urlsafe_base64(32)
406
493
  end
407
494
 
408
495
  def convert_session_key(key)
@@ -468,7 +555,11 @@ module Rodauth
468
555
  end
469
556
 
470
557
  def use_request_specific_csrf_tokens?
471
- scope.opts[:rodauth_csrf] == :route_csrf && scope.use_request_specific_csrf_tokens?
558
+ scope.opts[:rodauth_route_csrf] && scope.use_request_specific_csrf_tokens?
559
+ end
560
+
561
+ def check_csrf?
562
+ scope.opts[:rodauth_route_csrf]
472
563
  end
473
564
 
474
565
  def function_name(name)
@@ -481,13 +572,19 @@ module Rodauth
481
572
  end
482
573
  end
483
574
 
575
+ def has_password?
576
+ return @has_password if defined?(@has_password)
577
+ return false unless account || session_value
578
+ @has_password = !!get_password_hash
579
+ end
580
+
484
581
  # Get the password hash for the user. When using database authentication functions,
485
582
  # note that only the salt is returned.
486
583
  def get_password_hash
487
584
  if account_password_hash_column
488
- account[account_password_hash_column]
585
+ (account || account_from_session)[account_password_hash_column]
489
586
  elsif use_database_authentication_functions?
490
- db.get(Sequel.function(function_name(:rodauth_get_salt), account_id))
587
+ db.get(Sequel.function(function_name(:rodauth_get_salt), account ? account_id : session_value))
491
588
  else
492
589
  # :nocov:
493
590
  password_hash_ds.get(password_hash_column)
@@ -540,7 +637,7 @@ module Rodauth
540
637
  end
541
638
 
542
639
  def password_hash_ds
543
- db[password_hash_table].where(password_hash_id_column=>account_id)
640
+ db[password_hash_table].where(password_hash_id_column=>account ? account_id : session_value)
544
641
  end
545
642
 
546
643
  # This is needed for jdbc/sqlite, which returns timestamp columns as strings
@@ -607,6 +704,10 @@ module Rodauth
607
704
  session[key] = value
608
705
  end
609
706
 
707
+ def remove_session_value(key)
708
+ session.delete(key)
709
+ end
710
+
610
711
  def update_hash_ds(hash, ds, values)
611
712
  num = ds.update(values)
612
713
  if num == 1