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.
- checksums.yaml +4 -4
- data/CHANGELOG +132 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +207 -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 +74 -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/http_basic_auth.rdoc +10 -1
- data/doc/jwt.rdoc +22 -22
- data/doc/jwt_cors.rdoc +2 -3
- data/doc/jwt_refresh.rdoc +12 -8
- data/doc/lockout.rdoc +17 -15
- data/doc/login.rdoc +10 -2
- data/doc/login_password_requirements_base.rdoc +15 -37
- data/doc/logout.rdoc +2 -2
- data/doc/otp.rdoc +24 -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/2.0.0.txt +361 -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 +29 -24
- data/lib/rodauth/features/account_expiration.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +160 -0
- data/lib/rodauth/features/audit_logging.rb +96 -0
- data/lib/rodauth/features/base.rb +131 -47
- data/lib/rodauth/features/change_password_notify.rb +1 -1
- data/lib/rodauth/features/confirm_password.rb +40 -2
- data/lib/rodauth/features/create_account.rb +7 -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 +29 -27
- data/lib/rodauth/features/email_base.rb +3 -3
- data/lib/rodauth/features/http_basic_auth.rb +44 -37
- data/lib/rodauth/features/jwt.rb +51 -8
- data/lib/rodauth/features/jwt_refresh.rb +3 -3
- data/lib/rodauth/features/lockout.rb +11 -13
- data/lib/rodauth/features/login.rb +48 -8
- data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
- data/lib/rodauth/features/otp.rb +71 -81
- data/lib/rodauth/features/password_complexity.rb +4 -11
- data/lib/rodauth/features/password_expiration.rb +1 -1
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/recovery_codes.rb +47 -51
- data/lib/rodauth/features/remember.rb +11 -27
- data/lib/rodauth/features/reset_password.rb +25 -25
- data/lib/rodauth/features/session_expiration.rb +6 -4
- data/lib/rodauth/features/single_session.rb +7 -5
- data/lib/rodauth/features/sms_codes.rb +58 -67
- data/lib/rodauth/features/two_factor_base.rb +132 -28
- data/lib/rodauth/features/verify_account.rb +23 -20
- data/lib/rodauth/features/verify_account_grace_period.rb +19 -8
- data/lib/rodauth/features/verify_login_change.rb +11 -10
- data/lib/rodauth/features/webauthn.rb +507 -0
- data/lib/rodauth/features/webauthn_login.rb +70 -0
- data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
- 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 +64 -11
- data/doc/verify_change_login.rdoc +0 -11
- 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
|
-
|
|
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,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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
"#{
|
|
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
|
-
|
|
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:
|
|
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[:
|
|
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
|