rodauth 1.22.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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