rodauth 1.23.0 → 2.0.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +132 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +207 -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 +74 -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/http_basic_auth.rdoc +10 -1
  19. data/doc/jwt.rdoc +22 -22
  20. data/doc/jwt_cors.rdoc +2 -3
  21. data/doc/jwt_refresh.rdoc +12 -8
  22. data/doc/lockout.rdoc +17 -15
  23. data/doc/login.rdoc +10 -2
  24. data/doc/login_password_requirements_base.rdoc +15 -37
  25. data/doc/logout.rdoc +2 -2
  26. data/doc/otp.rdoc +24 -19
  27. data/doc/password_complexity.rdoc +10 -26
  28. data/doc/password_expiration.rdoc +11 -25
  29. data/doc/password_grace_period.rdoc +16 -2
  30. data/doc/recovery_codes.rdoc +18 -12
  31. data/doc/release_notes/2.0.0.txt +361 -0
  32. data/doc/remember.rdoc +40 -64
  33. data/doc/reset_password.rdoc +12 -9
  34. data/doc/session_expiration.rdoc +1 -0
  35. data/doc/single_session.rdoc +16 -25
  36. data/doc/sms_codes.rdoc +24 -14
  37. data/doc/two_factor_base.rdoc +60 -22
  38. data/doc/verify_account.rdoc +14 -12
  39. data/doc/verify_account_grace_period.rdoc +6 -2
  40. data/doc/verify_login_change.rdoc +9 -8
  41. data/doc/webauthn.rdoc +115 -0
  42. data/doc/webauthn_login.rdoc +15 -0
  43. data/doc/webauthn_verify_account.rdoc +9 -0
  44. data/javascript/webauthn_auth.js +45 -0
  45. data/javascript/webauthn_setup.js +35 -0
  46. data/lib/roda/plugins/rodauth.rb +1 -1
  47. data/lib/rodauth.rb +29 -24
  48. data/lib/rodauth/features/account_expiration.rb +5 -5
  49. data/lib/rodauth/features/active_sessions.rb +160 -0
  50. data/lib/rodauth/features/audit_logging.rb +96 -0
  51. data/lib/rodauth/features/base.rb +131 -47
  52. data/lib/rodauth/features/change_password_notify.rb +1 -1
  53. data/lib/rodauth/features/confirm_password.rb +40 -2
  54. data/lib/rodauth/features/create_account.rb +7 -13
  55. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  56. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  57. data/lib/rodauth/features/email_auth.rb +29 -27
  58. data/lib/rodauth/features/email_base.rb +3 -3
  59. data/lib/rodauth/features/http_basic_auth.rb +44 -37
  60. data/lib/rodauth/features/jwt.rb +51 -8
  61. data/lib/rodauth/features/jwt_refresh.rb +3 -3
  62. data/lib/rodauth/features/lockout.rb +11 -13
  63. data/lib/rodauth/features/login.rb +48 -8
  64. data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
  65. data/lib/rodauth/features/otp.rb +71 -81
  66. data/lib/rodauth/features/password_complexity.rb +4 -11
  67. data/lib/rodauth/features/password_expiration.rb +1 -1
  68. data/lib/rodauth/features/password_grace_period.rb +17 -10
  69. data/lib/rodauth/features/recovery_codes.rb +47 -51
  70. data/lib/rodauth/features/remember.rb +11 -27
  71. data/lib/rodauth/features/reset_password.rb +25 -25
  72. data/lib/rodauth/features/session_expiration.rb +6 -4
  73. data/lib/rodauth/features/single_session.rb +7 -5
  74. data/lib/rodauth/features/sms_codes.rb +58 -67
  75. data/lib/rodauth/features/two_factor_base.rb +132 -28
  76. data/lib/rodauth/features/verify_account.rb +23 -20
  77. data/lib/rodauth/features/verify_account_grace_period.rb +19 -8
  78. data/lib/rodauth/features/verify_login_change.rb +11 -10
  79. data/lib/rodauth/features/webauthn.rb +507 -0
  80. data/lib/rodauth/features/webauthn_login.rb +70 -0
  81. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  82. data/lib/rodauth/version.rb +2 -2
  83. data/templates/button.str +1 -3
  84. data/templates/change-login.str +1 -2
  85. data/templates/change-password.str +3 -5
  86. data/templates/close-account.str +2 -2
  87. data/templates/confirm-password.str +1 -1
  88. data/templates/create-account.str +1 -1
  89. data/templates/email-auth-request-form.str +1 -2
  90. data/templates/email-auth.str +1 -1
  91. data/templates/global-logout-field.str +6 -0
  92. data/templates/login-confirm-field.str +2 -4
  93. data/templates/login-display.str +3 -2
  94. data/templates/login-field.str +2 -4
  95. data/templates/login-form-footer.str +6 -0
  96. data/templates/login-form.str +7 -0
  97. data/templates/login.str +1 -9
  98. data/templates/logout.str +1 -1
  99. data/templates/multi-phase-login.str +3 -0
  100. data/templates/otp-auth-code-field.str +5 -3
  101. data/templates/otp-auth.str +1 -1
  102. data/templates/otp-disable.str +1 -1
  103. data/templates/otp-setup.str +3 -3
  104. data/templates/password-confirm-field.str +2 -4
  105. data/templates/password-field.str +2 -4
  106. data/templates/recovery-auth.str +3 -6
  107. data/templates/recovery-codes.str +1 -1
  108. data/templates/remember.str +15 -20
  109. data/templates/reset-password-request.str +2 -2
  110. data/templates/reset-password.str +1 -2
  111. data/templates/sms-auth.str +1 -1
  112. data/templates/sms-code-field.str +5 -3
  113. data/templates/sms-confirm.str +1 -2
  114. data/templates/sms-disable.str +1 -2
  115. data/templates/sms-request.str +1 -1
  116. data/templates/sms-setup.str +6 -4
  117. data/templates/two-factor-auth.str +5 -0
  118. data/templates/two-factor-disable.str +6 -0
  119. data/templates/two-factor-manage.str +16 -0
  120. data/templates/unlock-account-request.str +2 -2
  121. data/templates/unlock-account.str +1 -1
  122. data/templates/verify-account-resend.str +1 -1
  123. data/templates/verify-account.str +1 -2
  124. data/templates/verify-login-change.str +1 -1
  125. data/templates/webauthn-auth.str +11 -0
  126. data/templates/webauthn-remove.str +14 -0
  127. data/templates/webauthn-setup.str +12 -0
  128. metadata +64 -11
  129. data/doc/verify_change_login.rdoc +0 -11
  130. data/lib/rodauth/features/verify_change_login.rb +0 -20
@@ -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,160 @@
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 request.post?
122
+ if param_or_nil(global_logout_param)
123
+ remove_all_active_sessions
124
+ else
125
+ remove_current_session
126
+ end
127
+ end
128
+ super if defined?(super)
129
+ end
130
+
131
+ def session_inactivity_deadline_condition
132
+ if deadline = session_inactivity_deadline
133
+ Sequel[active_sessions_last_use_column] < Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, seconds: deadline)
134
+ end
135
+ end
136
+
137
+ def session_lifetime_deadline_condition
138
+ if deadline = session_lifetime_deadline
139
+ Sequel[active_sessions_created_at_column] < Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, seconds: deadline)
140
+ end
141
+ end
142
+
143
+ def inactive_session_cond
144
+ cond = session_inactivity_deadline_condition
145
+ cond2 = session_lifetime_deadline_condition
146
+ return false unless cond || cond2
147
+ Sequel.|(*[cond, cond2].compact)
148
+ end
149
+
150
+ def active_sessions_ds
151
+ db[active_sessions_table].
152
+ where(active_sessions_account_id_column=>session_value)
153
+ end
154
+
155
+ def use_date_arithmetic?
156
+ true
157
+ end
158
+ end
159
+ end
160
+
@@ -0,0 +1,96 @@
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
+ if db.database_type == :postgres
86
+ # For PostgreSQL, use RETURNING NULL. This allows the feature
87
+ # to be used with INSERT but not SELECT permissions on the
88
+ # table, useful for audit logging where the database user
89
+ # the application is running as should not need to read the
90
+ # logs.
91
+ ds = ds.returning(nil)
92
+ end
93
+ ds
94
+ end
95
+ end
96
+ 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,12 @@ module Rodauth
71
81
  :account_session_value,
72
82
  :already_logged_in,
73
83
  :authenticated?,
84
+ :autocomplete_for_field?,
74
85
  :clear_session,
75
86
  :csrf_tag,
76
87
  :function_name,
88
+ :hook_action,
89
+ :inputmode_for_field?,
77
90
  :logged_in?,
78
91
  :login_required,
79
92
  :open_account?,
@@ -86,6 +99,7 @@ module Rodauth
86
99
  :set_notice_now_flash,
87
100
  :set_redirect_error_flash,
88
101
  :set_title,
102
+ :translate,
89
103
  :unverified_account_message,
90
104
  :update_session
91
105
  )
@@ -102,13 +116,6 @@ module Rodauth
102
116
  def auth_class_eval(&block)
103
117
  auth.class_eval(&block)
104
118
  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
119
  end
113
120
 
114
121
  attr_reader :scope
@@ -168,13 +175,29 @@ module Rodauth
168
175
  value = opts.fetch(:value){scope.h param(param)}
169
176
  end
170
177
 
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
178
+ field_class = opts.fetch(:class, "form-control")
179
+
180
+ if autocomplete_for_field?(param) && opts[:autocomplete]
181
+ autocomplete = "autocomplete=\"#{opts[:autocomplete]}\""
182
+ end
183
+
184
+ if inputmode_for_field?(param) && opts[:inputmode]
185
+ inputmode = "inputmode=\"#{opts[:inputmode]}\""
186
+ end
173
187
 
174
- def default_field_attributes
175
- if mark_input_fields_as_required?
176
- "required=\"required\""
188
+ if mark_input_fields_as_required? && opts[:required] != false
189
+ required = "required=\"required\""
177
190
  end
191
+
192
+ "<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]}"
193
+ end
194
+
195
+ def autocomplete_for_field?(_param)
196
+ mark_input_fields_with_autocomplete?
197
+ end
198
+
199
+ def inputmode_for_field?(_param)
200
+ mark_input_fields_with_inputmode?
178
201
  end
179
202
 
180
203
  def field_attributes(field)
@@ -193,6 +216,15 @@ module Rodauth
193
216
  end
194
217
  end
195
218
 
219
+ def hook_action(_hook_type, _action)
220
+ # nothing by default
221
+ end
222
+
223
+ def translate(_key, default)
224
+ # do not attempt to translate by default
225
+ default
226
+ end
227
+
196
228
  # Return urlsafe base64 HMAC for data, assumes hmac_secret is set.
197
229
  def compute_hmac(data)
198
230
  s = [compute_raw_hmac(data)].pack('m').chomp!("=\n")
@@ -237,6 +269,14 @@ module Rodauth
237
269
  nil
238
270
  end
239
271
 
272
+ def login_input_type
273
+ login_uses_email? ? 'email' : 'text'
274
+ end
275
+
276
+ def login_uses_email?
277
+ login_column == :email
278
+ end
279
+
240
280
  def clear_session
241
281
  if scope.respond_to?(:clear_session)
242
282
  scope.clear_session
@@ -353,7 +393,25 @@ module Rodauth
353
393
 
354
394
  def update_session
355
395
  clear_session
356
- session[session_key] = account_session_value
396
+ set_session_value(session_key, account_session_value)
397
+ end
398
+
399
+ def authenticated_by
400
+ session[authenticated_by_session_key]
401
+ end
402
+
403
+ def login_session(auth_type)
404
+ update_session
405
+ set_session_value(authenticated_by_session_key, [auth_type])
406
+ end
407
+
408
+ def autologin_type
409
+ session[autologin_type_session_key]
410
+ end
411
+
412
+ def autologin_session(autologin_type)
413
+ login_session('autologin')
414
+ set_session_value(autologin_type_session_key, autologin_type)
357
415
  end
358
416
 
359
417
  # Return a string for the parameter name. This will be an empty
@@ -365,10 +423,30 @@ module Rodauth
365
423
  # Return a string for the parameter name, or nil if there is no
366
424
  # parameter with that name.
367
425
  def param_or_nil(key)
368
- value = request.params[key]
426
+ value = raw_param(key)
369
427
  value.to_s unless value.nil?
370
428
  end
371
429
 
430
+ def raw_param(key)
431
+ request.params[key]
432
+ end
433
+
434
+ def base_url
435
+ request.base_url
436
+ end
437
+
438
+ def domain
439
+ request.host
440
+ end
441
+
442
+ def modifications_require_password?
443
+ has_password?
444
+ end
445
+
446
+ def possible_authentication_methods
447
+ has_password? ? ['password'] : []
448
+ end
449
+
372
450
  private
373
451
 
374
452
  def convert_token_key(key)
@@ -387,30 +465,22 @@ module Rodauth
387
465
  request.redirect(path)
388
466
  end
389
467
 
390
- def route_path(route)
391
- "#{prefix}/#{route}"
468
+ def route_path(route, opts={})
469
+ path = "#{prefix}/#{route}"
470
+ path += "?#{Rack::Utils.build_nested_query(opts)}" unless opts.empty?
471
+ path
392
472
  end
393
473
 
394
- def route_url(route)
395
- "#{request.base_url}#{route_path(route)}"
474
+ def route_url(route, opts={})
475
+ "#{base_url}#{route_path(route, opts)}"
396
476
  end
397
477
 
398
478
  def transaction(opts={}, &block)
399
479
  db.transaction(opts, &block)
400
480
  end
401
481
 
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:
482
+ def random_key
483
+ SecureRandom.urlsafe_base64(32)
414
484
  end
415
485
 
416
486
  def convert_session_key(key)
@@ -476,7 +546,11 @@ module Rodauth
476
546
  end
477
547
 
478
548
  def use_request_specific_csrf_tokens?
479
- scope.opts[:rodauth_csrf] == :route_csrf && scope.use_request_specific_csrf_tokens?
549
+ scope.opts[:rodauth_route_csrf] && scope.use_request_specific_csrf_tokens?
550
+ end
551
+
552
+ def check_csrf?
553
+ scope.opts[:rodauth_route_csrf]
480
554
  end
481
555
 
482
556
  def function_name(name)
@@ -489,13 +563,19 @@ module Rodauth
489
563
  end
490
564
  end
491
565
 
566
+ def has_password?
567
+ return @has_password if defined?(@has_password)
568
+ return false unless account || session_value
569
+ @has_password = !!get_password_hash
570
+ end
571
+
492
572
  # Get the password hash for the user. When using database authentication functions,
493
573
  # note that only the salt is returned.
494
574
  def get_password_hash
495
575
  if account_password_hash_column
496
- account[account_password_hash_column]
576
+ (account || account_from_session)[account_password_hash_column]
497
577
  elsif use_database_authentication_functions?
498
- db.get(Sequel.function(function_name(:rodauth_get_salt), account_id))
578
+ db.get(Sequel.function(function_name(:rodauth_get_salt), account ? account_id : session_value))
499
579
  else
500
580
  # :nocov:
501
581
  password_hash_ds.get(password_hash_column)
@@ -548,7 +628,7 @@ module Rodauth
548
628
  end
549
629
 
550
630
  def password_hash_ds
551
- db[password_hash_table].where(password_hash_id_column=>account_id)
631
+ db[password_hash_table].where(password_hash_id_column=>account ? account_id : session_value)
552
632
  end
553
633
 
554
634
  # This is needed for jdbc/sqlite, which returns timestamp columns as strings
@@ -615,6 +695,10 @@ module Rodauth
615
695
  session[key] = value
616
696
  end
617
697
 
698
+ def remove_session_value(key)
699
+ session.delete(key)
700
+ end
701
+
618
702
  def update_hash_ds(hash, ds, values)
619
703
  num = ds.update(values)
620
704
  if num == 1