rodauth 2.46.0 → 2.48.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/lib/rodauth/features/active_sessions.rb +2 -2
- data/lib/rodauth/features/argon2.rb +6 -6
- data/lib/rodauth/features/audit_logging.rb +4 -3
- data/lib/rodauth/features/base.rb +84 -30
- data/lib/rodauth/features/change_password.rb +11 -2
- data/lib/rodauth/features/confirm_password.rb +1 -1
- data/lib/rodauth/features/create_account.rb +2 -1
- data/lib/rodauth/features/disallow_password_reuse.rb +2 -2
- data/lib/rodauth/features/email_auth.rb +13 -4
- data/lib/rodauth/features/email_base.rb +36 -1
- data/lib/rodauth/features/http_basic_auth.rb +6 -1
- data/lib/rodauth/features/internal_request.rb +4 -0
- data/lib/rodauth/features/json.rb +5 -1
- data/lib/rodauth/features/jwt.rb +9 -3
- data/lib/rodauth/features/jwt_cors.rb +2 -0
- data/lib/rodauth/features/jwt_refresh.rb +33 -18
- data/lib/rodauth/features/lockout.rb +24 -21
- data/lib/rodauth/features/login.rb +12 -4
- data/lib/rodauth/features/login_password_requirements_base.rb +43 -13
- data/lib/rodauth/features/otp.rb +21 -12
- data/lib/rodauth/features/otp_unlock.rb +2 -2
- data/lib/rodauth/features/password_complexity.rb +2 -2
- data/lib/rodauth/features/password_expiration.rb +0 -1
- data/lib/rodauth/features/password_grace_period.rb +8 -0
- data/lib/rodauth/features/password_pepper.rb +11 -1
- data/lib/rodauth/features/recovery_codes.rb +4 -1
- data/lib/rodauth/features/remember.rb +4 -2
- data/lib/rodauth/features/require_domain.rb +12 -0
- data/lib/rodauth/features/require_hmac_secret.rb +12 -0
- data/lib/rodauth/features/reset_password.rb +25 -20
- data/lib/rodauth/features/single_session.rb +6 -2
- data/lib/rodauth/features/sms_codes.rb +4 -1
- data/lib/rodauth/features/two_factor_base.rb +2 -1
- data/lib/rodauth/features/update_password_hash.rb +5 -1
- data/lib/rodauth/features/verify_account.rb +29 -20
- data/lib/rodauth/features/verify_login_change.rb +19 -11
- data/lib/rodauth/features/webauthn.rb +16 -11
- data/lib/rodauth/features/webauthn_login.rb +1 -10
- data/lib/rodauth/features/webauthn_verify_account.rb +0 -4
- data/lib/rodauth/version.rb +1 -1
- data/lib/rodauth.rb +14 -12
- metadata +4 -2
|
@@ -8,6 +8,7 @@ module Rodauth
|
|
|
8
8
|
auth_value_method :jwt_cors_allow_methods, 'POST'
|
|
9
9
|
auth_value_method :jwt_cors_allow_headers, 'Content-Type, Authorization, Accept'
|
|
10
10
|
auth_value_method :jwt_cors_expose_headers, 'Authorization'
|
|
11
|
+
auth_value_method :jwt_cors_vary_header, 'Origin'
|
|
11
12
|
auth_value_method :jwt_cors_max_age, 86400
|
|
12
13
|
|
|
13
14
|
auth_methods(:jwt_cors_allow?)
|
|
@@ -33,6 +34,7 @@ module Rodauth
|
|
|
33
34
|
|
|
34
35
|
def before_rodauth
|
|
35
36
|
if jwt_cors_allow?
|
|
37
|
+
set_response_header('vary', jwt_cors_vary_header)
|
|
36
38
|
set_response_header('access-control-allow-origin', request.env['HTTP_ORIGIN'])
|
|
37
39
|
|
|
38
40
|
# Handle CORS preflight request
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Rodauth
|
|
4
4
|
Feature.define(:jwt_refresh, :JwtRefresh) do
|
|
5
|
-
depends :jwt
|
|
5
|
+
depends :jwt, :require_hmac_secret
|
|
6
6
|
|
|
7
7
|
after 'refresh_token'
|
|
8
8
|
before 'refresh_token'
|
|
@@ -31,28 +31,34 @@ module Rodauth
|
|
|
31
31
|
:account_from_refresh_token
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
uses_instance_variables(
|
|
34
|
+
uses_instance_variables(
|
|
35
|
+
:@jwt_refresh_route,
|
|
36
|
+
:@jwt_refresh_key_for_update,
|
|
37
|
+
)
|
|
35
38
|
|
|
36
39
|
route do |r|
|
|
37
|
-
@jwt_refresh_route = true
|
|
38
40
|
before_jwt_refresh_route
|
|
39
41
|
|
|
40
42
|
r.post do
|
|
43
|
+
@jwt_refresh_route = true
|
|
41
44
|
if !session_value
|
|
42
45
|
response.status ||= jwt_refresh_without_access_token_status
|
|
43
46
|
json_response[json_response_error_key] = jwt_refresh_without_access_token_message
|
|
44
|
-
|
|
47
|
+
else
|
|
48
|
+
@jwt_refresh_key_for_update = true
|
|
45
49
|
transaction do
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
|
|
51
|
+
before_refresh_token
|
|
52
|
+
formatted_token = generate_refresh_token
|
|
53
|
+
remove_jwt_refresh_token_key(refresh_token)
|
|
54
|
+
set_jwt_refresh_token_hmac_session_key(formatted_token)
|
|
55
|
+
json_response[jwt_refresh_token_key] = formatted_token
|
|
56
|
+
json_response[jwt_access_token_key] = session_jwt
|
|
57
|
+
after_refresh_token
|
|
58
|
+
else
|
|
59
|
+
json_response[json_response_error_key] = jwt_refresh_invalid_token_message
|
|
60
|
+
end
|
|
53
61
|
end
|
|
54
|
-
else
|
|
55
|
-
json_response[json_response_error_key] = jwt_refresh_invalid_token_message
|
|
56
62
|
end
|
|
57
63
|
_return_json_response
|
|
58
64
|
end
|
|
@@ -88,7 +94,7 @@ module Rodauth
|
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
def account_from_refresh_token(token)
|
|
91
|
-
|
|
97
|
+
set_account(_account_from_refresh_token(token), :jwt_refresh)
|
|
92
98
|
end
|
|
93
99
|
|
|
94
100
|
def clear_tokens(reason)
|
|
@@ -142,7 +148,7 @@ module Rodauth
|
|
|
142
148
|
end
|
|
143
149
|
|
|
144
150
|
def _jwt_decode_opts
|
|
145
|
-
if allow_refresh_with_expired_jwt_access_token? && (@jwt_refresh_route || request.path == jwt_refresh_path)
|
|
151
|
+
if allow_refresh_with_expired_jwt_access_token? && (@jwt_refresh_route || (request.path == jwt_refresh_path && request.request_method == "POST"))
|
|
146
152
|
Hash[super].merge!(:verify_expiration=>false)
|
|
147
153
|
else
|
|
148
154
|
super
|
|
@@ -165,8 +171,12 @@ module Rodauth
|
|
|
165
171
|
where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
|
|
166
172
|
delete
|
|
167
173
|
|
|
168
|
-
jwt_refresh_token_account_token_ds(account_id, token_id)
|
|
169
|
-
|
|
174
|
+
ds = jwt_refresh_token_account_token_ds(account_id, token_id)
|
|
175
|
+
if @jwt_refresh_key_for_update
|
|
176
|
+
ds = ds.for_update
|
|
177
|
+
@jwt_refresh_key_for_update = nil
|
|
178
|
+
end
|
|
179
|
+
ds.get(jwt_refresh_token_key_column)
|
|
170
180
|
end
|
|
171
181
|
|
|
172
182
|
def jwt_refresh_token_account_ds(account_id)
|
|
@@ -214,7 +224,12 @@ module Rodauth
|
|
|
214
224
|
else
|
|
215
225
|
id, token_id, key = _account_refresh_token_split(token)
|
|
216
226
|
|
|
217
|
-
if id &&
|
|
227
|
+
if id &&
|
|
228
|
+
token_id &&
|
|
229
|
+
key &&
|
|
230
|
+
(actual = get_active_refresh_token(session_value, token_id)) &&
|
|
231
|
+
(timing_safe_eql?(key, convert_token_key(actual)) ||
|
|
232
|
+
(hmac_secret_rotation? && timing_safe_eql?(key, compute_old_hmac(actual))))
|
|
218
233
|
jwt_refresh_token_account_token_ds(id, token_id).delete
|
|
219
234
|
end
|
|
220
235
|
end
|
|
@@ -116,16 +116,19 @@ module Rodauth
|
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
r.post do
|
|
119
|
-
key =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
key = session_param(unlock_account_session_key, unlock_account_key_param)
|
|
120
|
+
select_key_for_update!
|
|
121
|
+
|
|
122
|
+
transaction do
|
|
123
|
+
unless key && account_from_unlock_key(key)
|
|
124
|
+
remove_session_value(unlock_account_session_key)
|
|
125
|
+
set_redirect_error_status invalid_key_error_status
|
|
126
|
+
set_error_reason :invalid_unlock_account_key
|
|
127
|
+
set_redirect_error_flash no_matching_unlock_account_key_error_flash
|
|
128
|
+
redirect unlock_account_request_redirect
|
|
129
|
+
end
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
transaction do
|
|
131
|
+
if !unlock_account_requires_password? || password_match?(param(password_param))
|
|
129
132
|
before_unlock_account
|
|
130
133
|
unlock_account
|
|
131
134
|
clear_tokens(:unlock_account)
|
|
@@ -133,15 +136,15 @@ module Rodauth
|
|
|
133
136
|
if unlock_account_autologin?
|
|
134
137
|
autologin_session('unlock_account')
|
|
135
138
|
end
|
|
136
|
-
end
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
remove_session_value(unlock_account_session_key)
|
|
141
|
+
unlock_account_response
|
|
142
|
+
else
|
|
143
|
+
set_response_error_reason_status(:invalid_password, invalid_password_error_status)
|
|
144
|
+
set_field_error(password_param, invalid_password_message)
|
|
145
|
+
set_error_flash unlock_account_error_flash
|
|
146
|
+
unlock_account_view
|
|
147
|
+
end
|
|
145
148
|
end
|
|
146
149
|
end
|
|
147
150
|
end
|
|
@@ -184,11 +187,11 @@ module Rodauth
|
|
|
184
187
|
with_sql(:update_sql, account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1).
|
|
185
188
|
single_value
|
|
186
189
|
else
|
|
187
|
-
# :
|
|
190
|
+
# simplecov:disable
|
|
188
191
|
if ds.update(account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1) > 0
|
|
189
192
|
ds.get(account_login_failures_number_column)
|
|
190
193
|
end
|
|
191
|
-
# :
|
|
194
|
+
# simplecov:enable
|
|
192
195
|
end
|
|
193
196
|
|
|
194
197
|
unless number
|
|
@@ -221,7 +224,7 @@ module Rodauth
|
|
|
221
224
|
end
|
|
222
225
|
|
|
223
226
|
def account_from_unlock_key(key)
|
|
224
|
-
|
|
227
|
+
set_account(_account_from_unlock_key(key), :unlock)
|
|
225
228
|
end
|
|
226
229
|
|
|
227
230
|
def unlock_account_email_link
|
|
@@ -310,7 +313,7 @@ module Rodauth
|
|
|
310
313
|
end
|
|
311
314
|
|
|
312
315
|
def _account_from_unlock_key(token)
|
|
313
|
-
account_from_key(token){|id| account_lockouts_ds(id).get(account_lockouts_key_column)}
|
|
316
|
+
account_from_key(token){|id| apply_key_for_update(account_lockouts_ds(id)).get(account_lockouts_key_column)}
|
|
314
317
|
end
|
|
315
318
|
end
|
|
316
319
|
end
|
|
@@ -15,7 +15,7 @@ module Rodauth
|
|
|
15
15
|
auth_value_method :login_error_status, 401
|
|
16
16
|
translatable_method :login_form_footer_links_heading, '<h2 class="rodauth-login-form-footer-links-heading">Other Options</h2>'
|
|
17
17
|
auth_value_method :login_return_to_requested_location?, false
|
|
18
|
-
|
|
18
|
+
def_deprecated_alias :return_to_path_max_size, :login_return_to_requested_location_max_path_size
|
|
19
19
|
auth_value_method :use_multi_phase_login?, false
|
|
20
20
|
|
|
21
21
|
session_key :login_redirect_session_key, :login_redirect
|
|
@@ -106,14 +106,14 @@ module Rodauth
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def login_required
|
|
109
|
-
if login_return_to_requested_location?
|
|
110
|
-
|
|
109
|
+
if login_return_to_requested_location?
|
|
110
|
+
set_session_return_to_path(login_redirect_session_key, login_return_to_requested_location_path)
|
|
111
111
|
end
|
|
112
112
|
super
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def login_return_to_requested_location_path
|
|
116
|
-
request.fullpath
|
|
116
|
+
request.fullpath
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def after_login_entered_during_multi_phase_login
|
|
@@ -153,6 +153,14 @@ module Rodauth
|
|
|
153
153
|
login_path
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
def post_configure
|
|
157
|
+
# RODAUTH3: Remove
|
|
158
|
+
super
|
|
159
|
+
if method(:already_logged_in).owner == Rodauth::Base
|
|
160
|
+
missing_recommended_configuration("The Rodauth 'already_logged_in' configuration method was not used. Use the configuration method to define the behavior when accessing a page while logged in that is expected to be accessed when not logged in (safest to halt or redirect).")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
156
164
|
private
|
|
157
165
|
|
|
158
166
|
def _login_response
|
|
@@ -12,7 +12,7 @@ module Rodauth
|
|
|
12
12
|
translatable_method :logins_do_not_match_message, 'logins do not match'
|
|
13
13
|
auth_value_method :password_confirm_param, 'password-confirm'
|
|
14
14
|
auth_value_method :password_minimum_length, 6
|
|
15
|
-
auth_value_method :password_maximum_bytes, nil
|
|
15
|
+
auth_value_method :password_maximum_bytes, nil # RODAUTH3: 72 if bcrypt is used
|
|
16
16
|
auth_value_method :password_maximum_length, nil
|
|
17
17
|
translatable_method :passwords_do_not_match_message, 'passwords do not match'
|
|
18
18
|
auth_value_method :require_email_address_logins?, true
|
|
@@ -67,13 +67,39 @@ module Rodauth
|
|
|
67
67
|
def set_password(password)
|
|
68
68
|
hash = password_hash(password)
|
|
69
69
|
if account_password_hash_column
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
ds = account_ds
|
|
71
|
+
|
|
72
|
+
if @existing_password_hash_or_salt
|
|
73
|
+
ds = ds.where(account_password_hash_column => @existing_password_hash_or_salt)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
unless update_account({account_password_hash_column=>hash}, ds) == 1
|
|
77
|
+
raise SetPasswordFailure, "set_password did not update password, either due to missing account, or concurrent password change"
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
ph_ds = ds = password_hash_ds
|
|
81
|
+
if @existing_password_hash_or_salt
|
|
82
|
+
existing_expression = if use_database_authentication_functions?
|
|
83
|
+
Sequel.function(function_name(:rodauth_get_salt), :id)
|
|
84
|
+
# simplecov:disable
|
|
85
|
+
else
|
|
86
|
+
password_hash_column
|
|
87
|
+
# simplecov:enable
|
|
88
|
+
end
|
|
89
|
+
ds = ds.where(existing_expression => @existing_password_hash_or_salt)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if ds.update(password_hash_column=>hash) == 0
|
|
93
|
+
if ph_ds.empty?
|
|
94
|
+
# This shouldn't raise a uniqueness error, as the update should only fail for a new user,
|
|
95
|
+
# and an existing user should always have a valid password hash row. If this does
|
|
96
|
+
# fail, retrying it will cause problems, it will override a concurrently running update
|
|
97
|
+
# with potentially a different password.
|
|
98
|
+
db[password_hash_table].insert(password_hash_id_column=>account_id, password_hash_column=>hash)
|
|
99
|
+
else
|
|
100
|
+
raise SetPasswordFailure, "set_password did not update password due to concurrent password change"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
77
103
|
end
|
|
78
104
|
hash
|
|
79
105
|
end
|
|
@@ -133,12 +159,12 @@ module Rodauth
|
|
|
133
159
|
def login_confirmation_matches?(login, login_confirmation)
|
|
134
160
|
login.casecmp?(login_confirmation)
|
|
135
161
|
end
|
|
136
|
-
# :
|
|
162
|
+
# simplecov:disable
|
|
137
163
|
else
|
|
138
164
|
def login_confirmation_matches?(login, login_confirmation)
|
|
139
165
|
login.casecmp(login_confirmation) == 0
|
|
140
166
|
end
|
|
141
|
-
# :
|
|
167
|
+
# simplecov:enable
|
|
142
168
|
end
|
|
143
169
|
|
|
144
170
|
def login_meets_length_requirements?(login)
|
|
@@ -174,7 +200,7 @@ module Rodauth
|
|
|
174
200
|
elsif password_maximum_length && password_maximum_length < password.length
|
|
175
201
|
set_password_requirement_error_message(:password_too_long, password_too_long_message)
|
|
176
202
|
false
|
|
177
|
-
elsif password_maximum_bytes && password_maximum_bytes < password
|
|
203
|
+
elsif password_maximum_bytes && password_maximum_bytes < password_bytesize(password)
|
|
178
204
|
set_password_requirement_error_message(:password_too_many_bytes, password_too_many_bytes_message)
|
|
179
205
|
false
|
|
180
206
|
else
|
|
@@ -182,6 +208,10 @@ module Rodauth
|
|
|
182
208
|
end
|
|
183
209
|
end
|
|
184
210
|
|
|
211
|
+
def password_bytesize(password)
|
|
212
|
+
password.bytesize
|
|
213
|
+
end
|
|
214
|
+
|
|
185
215
|
def password_does_not_contain_null_byte?(password)
|
|
186
216
|
return true unless password.include?("\0")
|
|
187
217
|
set_password_requirement_error_message(:password_contains_null_byte, contains_null_byte_message)
|
|
@@ -193,11 +223,11 @@ module Rodauth
|
|
|
193
223
|
BCrypt::Engine::MIN_COST
|
|
194
224
|
end
|
|
195
225
|
else
|
|
196
|
-
# :
|
|
226
|
+
# simplecov:disable
|
|
197
227
|
def password_hash_cost
|
|
198
228
|
BCrypt::Engine::DEFAULT_COST
|
|
199
229
|
end
|
|
200
|
-
# :
|
|
230
|
+
# simplecov:enable
|
|
201
231
|
end
|
|
202
232
|
|
|
203
233
|
def extract_password_hash_cost(hash)
|
data/lib/rodauth/features/otp.rb
CHANGED
|
@@ -5,7 +5,7 @@ require 'rqrcode'
|
|
|
5
5
|
|
|
6
6
|
module Rodauth
|
|
7
7
|
Feature.define(:otp, :Otp) do
|
|
8
|
-
depends :two_factor_base
|
|
8
|
+
depends :two_factor_base, :require_domain, :require_hmac_secret
|
|
9
9
|
|
|
10
10
|
additional_form_tags 'otp_disable'
|
|
11
11
|
additional_form_tags 'otp_auth'
|
|
@@ -127,13 +127,18 @@ module Rodauth
|
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
r.post do
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
transaction do
|
|
131
|
+
otp_key_ds.for_update.first
|
|
132
|
+
|
|
133
|
+
if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
|
|
134
|
+
before_otp_authentication
|
|
135
|
+
two_factor_authenticate('totp')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
otp_record_authentication_failure
|
|
139
|
+
after_otp_authentication_failure
|
|
133
140
|
end
|
|
134
141
|
|
|
135
|
-
otp_record_authentication_failure
|
|
136
|
-
after_otp_authentication_failure
|
|
137
142
|
set_response_error_reason_status(:invalid_otp_auth_code, invalid_key_error_status)
|
|
138
143
|
set_field_error(otp_auth_param, otp_invalid_auth_code_message)
|
|
139
144
|
set_error_flash otp_auth_error_flash
|
|
@@ -265,15 +270,19 @@ module Rodauth
|
|
|
265
270
|
return false unless otp_exists?
|
|
266
271
|
ot_pass = ot_pass.gsub(/\s+/, '')
|
|
267
272
|
if drift = otp_drift
|
|
273
|
+
# simplecov:disable
|
|
268
274
|
if otp.respond_to?(:verify_with_drift)
|
|
269
|
-
# :nocov:
|
|
270
275
|
otp.verify_with_drift(ot_pass, drift)
|
|
271
|
-
|
|
276
|
+
# simplecov:enable
|
|
272
277
|
else
|
|
273
|
-
otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift)
|
|
278
|
+
otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift, :after=>otp_last_use)
|
|
274
279
|
end
|
|
275
|
-
|
|
280
|
+
# simplecov:disable
|
|
281
|
+
elsif otp.respond_to?(:verify_with_drift)
|
|
276
282
|
otp.verify(ot_pass)
|
|
283
|
+
# simplecov:enable
|
|
284
|
+
else
|
|
285
|
+
otp.verify(ot_pass, :after=>otp_last_use)
|
|
277
286
|
end
|
|
278
287
|
end
|
|
279
288
|
|
|
@@ -404,11 +413,11 @@ module Rodauth
|
|
|
404
413
|
ROTP::Base32.random_base32.downcase
|
|
405
414
|
end
|
|
406
415
|
else
|
|
407
|
-
# :
|
|
416
|
+
# simplecov:disable
|
|
408
417
|
def otp_new_secret
|
|
409
418
|
ROTP::Base32.random.downcase
|
|
410
419
|
end
|
|
411
|
-
# :
|
|
420
|
+
# simplecov:enable
|
|
412
421
|
end
|
|
413
422
|
|
|
414
423
|
def base32_encode(data, length)
|
|
@@ -181,9 +181,9 @@ module Rodauth
|
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
@otp_unlock_data = nil
|
|
184
|
-
# :
|
|
184
|
+
# simplecov:disable
|
|
185
185
|
if otp_unlock_data
|
|
186
|
-
# :
|
|
186
|
+
# simplecov:enable
|
|
187
187
|
if otp_unlock_num_successes >= otp_unlock_auths_required
|
|
188
188
|
# At least the requisite number of consecutive successful unlock
|
|
189
189
|
# authentications. Unlock OTP authentication.
|
|
@@ -33,9 +33,9 @@ module Rodauth
|
|
|
33
33
|
# nothing
|
|
34
34
|
when nil
|
|
35
35
|
default_dictionary_file = '/usr/share/dict/words'
|
|
36
|
-
# :
|
|
36
|
+
# simplecov:disable
|
|
37
37
|
if File.file?(default_dictionary_file)
|
|
38
|
-
# :
|
|
38
|
+
# simplecov:enable
|
|
39
39
|
words = File.read(default_dictionary_file)
|
|
40
40
|
end
|
|
41
41
|
else
|
|
@@ -70,7 +70,6 @@ module Rodauth
|
|
|
70
70
|
return password_changed_at + require_password_change_after < Time.now.to_i
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
account_from_session
|
|
74
73
|
if password_changed_at = get_password_changed_at
|
|
75
74
|
set_session_value(password_changed_at_session_key, password_changed_at.to_i)
|
|
76
75
|
password_changed_at + require_password_change_after < Time.now
|
|
@@ -33,6 +33,14 @@ module Rodauth
|
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
+
def new_password_matches_current_password?(_)
|
|
37
|
+
last_password_entry = session[last_password_entry_session_key]
|
|
38
|
+
if v = super
|
|
39
|
+
set_session_value(last_password_entry_session_key, last_password_entry)
|
|
40
|
+
end
|
|
41
|
+
v
|
|
42
|
+
end
|
|
43
|
+
|
|
36
44
|
def after_create_account
|
|
37
45
|
super if defined?(super)
|
|
38
46
|
@last_password_entry = Time.now.to_i
|
|
@@ -12,7 +12,11 @@ module Rodauth
|
|
|
12
12
|
|
|
13
13
|
def password_match?(password)
|
|
14
14
|
if (result = super) && @previous_pepper_matched && password_pepper_update?
|
|
15
|
-
|
|
15
|
+
begin
|
|
16
|
+
set_password(password)
|
|
17
|
+
rescue Rodauth::SetPasswordFailure
|
|
18
|
+
return false
|
|
19
|
+
end
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
result
|
|
@@ -24,6 +28,12 @@ module Rodauth
|
|
|
24
28
|
|
|
25
29
|
private
|
|
26
30
|
|
|
31
|
+
def password_bytesize(password)
|
|
32
|
+
n = super
|
|
33
|
+
n += password_pepper.bytesize if password_pepper
|
|
34
|
+
n
|
|
35
|
+
end
|
|
36
|
+
|
|
27
37
|
def password_hash_match?(hash, password)
|
|
28
38
|
return super if password_pepper.nil?
|
|
29
39
|
|
|
@@ -162,7 +162,10 @@ module Rodauth
|
|
|
162
162
|
def recovery_code_match?(code)
|
|
163
163
|
recovery_codes.each do |s|
|
|
164
164
|
if timing_safe_eql?(code, s)
|
|
165
|
-
recovery_codes_ds.where(recovery_codes_column=>code).delete
|
|
165
|
+
unless recovery_codes_ds.where(recovery_codes_column=>code).delete == 1
|
|
166
|
+
return false
|
|
167
|
+
end
|
|
168
|
+
|
|
166
169
|
if recovery_codes_primary?
|
|
167
170
|
add_recovery_code
|
|
168
171
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Rodauth
|
|
4
4
|
Feature.define(:remember, :Remember) do
|
|
5
|
+
depends :require_hmac_secret
|
|
6
|
+
|
|
5
7
|
notice_flash "Your remember setting has been updated"
|
|
6
8
|
error_flash "There was an error updating your remember setting"
|
|
7
9
|
loaded_templates %w'remember'
|
|
@@ -68,9 +70,9 @@ module Rodauth
|
|
|
68
70
|
if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
|
|
69
71
|
transaction do
|
|
70
72
|
before_remember
|
|
71
|
-
# :
|
|
73
|
+
# simplecov:disable
|
|
72
74
|
case remember
|
|
73
|
-
# :
|
|
75
|
+
# simplecov:enable
|
|
74
76
|
when remember_remember_param_value
|
|
75
77
|
remember_login
|
|
76
78
|
when remember_forget_param_value
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Feature.define(:require_domain, :RequireDomain) do
|
|
5
|
+
def post_configure
|
|
6
|
+
super
|
|
7
|
+
if method(:domain).owner == Rodauth::Base
|
|
8
|
+
missing_recommended_configuration("The Rodauth 'domain' configuration method was not used. This can potentially leave the application vulnerable to requests that use malicious or invalid domains.")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Feature.define(:require_hmac_secret, :RequireHmacSecret) do
|
|
5
|
+
def post_configure
|
|
6
|
+
super
|
|
7
|
+
if method(:hmac_secret).owner == Rodauth::Base
|
|
8
|
+
missing_recommended_configuration("The Rodauth 'hmac_secret' configuration method was not used. This significantly reduces Rodauth's security. Consult the Rodauth README for what this method does, and set an appropriate value.")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -120,29 +120,26 @@ module Rodauth
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
r.post do
|
|
123
|
-
key =
|
|
124
|
-
|
|
125
|
-
set_redirect_error_status(invalid_key_error_status)
|
|
126
|
-
set_error_reason :invalid_reset_password_key
|
|
127
|
-
set_redirect_error_flash reset_password_error_flash
|
|
128
|
-
redirect reset_password_email_sent_redirect
|
|
129
|
-
end
|
|
130
|
-
|
|
123
|
+
key = session_param(reset_password_session_key, reset_password_key_param)
|
|
124
|
+
select_key_for_update!
|
|
131
125
|
password = param(password_param)
|
|
126
|
+
|
|
132
127
|
catch_error do
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
128
|
+
transaction do
|
|
129
|
+
handle_invalid_reset_password_key unless key && account_from_reset_password_key(key)
|
|
136
130
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
unless password_meets_requirements?(password)
|
|
132
|
+
throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
|
|
133
|
+
end
|
|
140
134
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
if password_match?(password)
|
|
136
|
+
throw_error_reason(:same_as_existing_password, invalid_field_error_status, password_param, same_as_existing_password_message)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if require_password_confirmation? && password != param(password_confirm_param)
|
|
140
|
+
throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
|
|
141
|
+
end
|
|
144
142
|
|
|
145
|
-
transaction do
|
|
146
143
|
before_reset_password
|
|
147
144
|
set_password(password)
|
|
148
145
|
clear_tokens(:reset_password)
|
|
@@ -184,7 +181,7 @@ module Rodauth
|
|
|
184
181
|
end
|
|
185
182
|
|
|
186
183
|
def account_from_reset_password_key(key)
|
|
187
|
-
|
|
184
|
+
set_account(_account_from_reset_password_key(key), :reset_password)
|
|
188
185
|
end
|
|
189
186
|
|
|
190
187
|
def reset_password_email_link
|
|
@@ -199,7 +196,7 @@ module Rodauth
|
|
|
199
196
|
def get_reset_password_key(id)
|
|
200
197
|
ds = password_reset_ds(id)
|
|
201
198
|
ds.where(Sequel::CURRENT_TIMESTAMP > reset_password_deadline_column).delete
|
|
202
|
-
ds.get(reset_password_key_column)
|
|
199
|
+
apply_key_for_update(ds).get(reset_password_key_column)
|
|
203
200
|
end
|
|
204
201
|
|
|
205
202
|
def set_reset_password_email_last_sent
|
|
@@ -267,5 +264,13 @@ module Rodauth
|
|
|
267
264
|
def reset_password_account_status_value
|
|
268
265
|
account_open_status_value
|
|
269
266
|
end
|
|
267
|
+
|
|
268
|
+
def handle_invalid_reset_password_key
|
|
269
|
+
remove_session_value(reset_password_session_key)
|
|
270
|
+
set_redirect_error_status(invalid_key_error_status)
|
|
271
|
+
set_error_reason :invalid_reset_password_key
|
|
272
|
+
set_redirect_error_flash reset_password_error_flash
|
|
273
|
+
redirect reset_password_email_sent_redirect
|
|
274
|
+
end
|
|
270
275
|
end
|
|
271
276
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Rodauth
|
|
4
4
|
Feature.define(:single_session, :SingleSession) do
|
|
5
|
+
depends :require_hmac_secret
|
|
6
|
+
|
|
5
7
|
error_flash 'This session has been logged out as another session has become active'
|
|
6
8
|
redirect
|
|
7
9
|
|
|
@@ -29,13 +31,15 @@ module Rodauth
|
|
|
29
31
|
single_session_key = session[single_session_session_key]
|
|
30
32
|
current_key = single_session_ds.get(single_session_key_column)
|
|
31
33
|
if single_session_key.nil?
|
|
32
|
-
|
|
34
|
+
if current_key
|
|
35
|
+
false
|
|
36
|
+
else
|
|
33
37
|
# No row exists for this user, indicating the feature has never
|
|
34
38
|
# been used, so it is OK to treat the current session as a new
|
|
35
39
|
# session.
|
|
36
40
|
update_single_session_key
|
|
41
|
+
true
|
|
37
42
|
end
|
|
38
|
-
true
|
|
39
43
|
elsif current_key
|
|
40
44
|
if hmac_secret && !(valid = timing_safe_eql?(single_session_key, hmac = compute_hmac(current_key)))
|
|
41
45
|
if hmac_secret_rotation? && (valid = timing_safe_eql?(single_session_key, compute_old_hmac(current_key)))
|