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