rodauth 1.22.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +190 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +210 -80
- 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 +75 -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 +6 -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/recovery_codes.rdoc +18 -12
- data/doc/release_notes/1.23.0.txt +32 -0
- 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/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 +36 -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 +144 -43
- data/lib/rodauth/features/change_password_notify.rb +2 -2
- 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 +1 -1
- data/lib/rodauth/features/email_auth.rb +31 -30
- data/lib/rodauth/features/email_base.rb +9 -4
- 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 +12 -14
- data/lib/rodauth/features/login.rb +64 -15
- data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
- data/lib/rodauth/features/otp.rb +77 -80
- data/lib/rodauth/features/password_complexity.rb +8 -13
- data/lib/rodauth/features/password_expiration.rb +2 -2
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/recovery_codes.rb +49 -53
- data/lib/rodauth/features/remember.rb +11 -27
- data/lib/rodauth/features/reset_password.rb +26 -26
- 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 +62 -72
- data/lib/rodauth/features/two_factor_base.rb +134 -30
- data/lib/rodauth/features/verify_account.rb +29 -21
- data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
- data/lib/rodauth/features/verify_login_change.rb +12 -11
- 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 +2 -3
- 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 +3 -3
- 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 +4 -4
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +3 -3
- 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 +94 -54
- data/Rakefile +0 -179
- data/doc/verify_change_login.rdoc +0 -11
- data/lib/rodauth/features/verify_change_login.rb +0 -20
- data/spec/account_expiration_spec.rb +0 -225
- data/spec/all.rb +0 -1
- data/spec/change_login_spec.rb +0 -156
- data/spec/change_password_notify_spec.rb +0 -33
- data/spec/change_password_spec.rb +0 -202
- data/spec/close_account_spec.rb +0 -162
- data/spec/confirm_password_spec.rb +0 -70
- data/spec/create_account_spec.rb +0 -127
- data/spec/disallow_common_passwords_spec.rb +0 -93
- data/spec/disallow_password_reuse_spec.rb +0 -179
- data/spec/email_auth_spec.rb +0 -285
- data/spec/http_basic_auth_spec.rb +0 -143
- data/spec/jwt_cors_spec.rb +0 -57
- data/spec/jwt_refresh_spec.rb +0 -256
- data/spec/jwt_spec.rb +0 -235
- data/spec/lockout_spec.rb +0 -250
- data/spec/login_spec.rb +0 -328
- data/spec/migrate/001_tables.rb +0 -184
- data/spec/migrate/002_account_password_hash_column.rb +0 -11
- data/spec/migrate_password/001_tables.rb +0 -73
- data/spec/migrate_travis/001_tables.rb +0 -141
- data/spec/password_complexity_spec.rb +0 -109
- data/spec/password_expiration_spec.rb +0 -244
- data/spec/password_grace_period_spec.rb +0 -93
- data/spec/remember_spec.rb +0 -451
- data/spec/reset_password_spec.rb +0 -229
- data/spec/rodauth_spec.rb +0 -343
- data/spec/session_expiration_spec.rb +0 -58
- data/spec/single_session_spec.rb +0 -127
- data/spec/spec_helper.rb +0 -327
- data/spec/two_factor_spec.rb +0 -1462
- data/spec/update_password_hash_spec.rb +0 -40
- data/spec/verify_account_grace_period_spec.rb +0 -171
- data/spec/verify_account_spec.rb +0 -240
- data/spec/verify_change_login_spec.rb +0 -46
- data/spec/verify_login_change_spec.rb +0 -232
- data/spec/views/layout-other.str +0 -11
- data/spec/views/layout.str +0 -11
- data/spec/views/login.str +0 -21
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
|
@@ -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
|
-
|
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,
|
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
|
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,
|
235
|
-
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
|
-
|
332
|
-
|
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
|
-
|
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,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
|
-
|
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, ''
|
48
50
|
auth_value_method :require_bcrypt?, true
|
49
|
-
auth_value_method :mark_input_fields_as_required?,
|
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
|
-
|
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
|
-
:
|
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
|
-
|
172
|
-
|
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
|
-
|
175
|
-
|
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
|
-
|
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 =
|
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
|
-
|
395
|
-
|
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[:
|
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
|