rodauth 2.11.0 → 2.15.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 +28 -0
- data/README.rdoc +29 -7
- data/doc/active_sessions.rdoc +4 -0
- data/doc/base.rdoc +1 -0
- data/doc/error_reasons.rdoc +73 -0
- data/doc/internal_request.rdoc +463 -0
- data/doc/path_class_methods.rdoc +10 -0
- data/doc/release_notes/2.11.0.txt +1 -1
- data/doc/release_notes/2.12.0.txt +17 -0
- data/doc/release_notes/2.13.0.txt +19 -0
- data/doc/release_notes/2.14.0.txt +17 -0
- data/doc/release_notes/2.15.0.txt +48 -0
- data/doc/remember.rdoc +1 -0
- data/lib/rodauth.rb +9 -2
- data/lib/rodauth/features/active_sessions.rb +29 -8
- data/lib/rodauth/features/base.rb +26 -1
- data/lib/rodauth/features/change_login.rb +6 -4
- data/lib/rodauth/features/change_password.rb +5 -3
- data/lib/rodauth/features/close_account.rb +3 -1
- data/lib/rodauth/features/confirm_password.rb +2 -2
- data/lib/rodauth/features/create_account.rb +6 -4
- 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 +6 -0
- data/lib/rodauth/features/internal_request.rb +367 -0
- data/lib/rodauth/features/jwt_refresh.rb +1 -1
- data/lib/rodauth/features/lockout.rb +15 -4
- data/lib/rodauth/features/login.rb +6 -3
- data/lib/rodauth/features/login_password_requirements_base.rb +15 -6
- data/lib/rodauth/features/otp.rb +13 -6
- data/lib/rodauth/features/password_complexity.rb +4 -4
- data/lib/rodauth/features/path_class_methods.rb +22 -0
- data/lib/rodauth/features/recovery_codes.rb +6 -2
- data/lib/rodauth/features/remember.rb +25 -10
- data/lib/rodauth/features/reset_password.rb +8 -4
- data/lib/rodauth/features/session_expiration.rb +1 -0
- data/lib/rodauth/features/single_session.rb +1 -0
- data/lib/rodauth/features/sms_codes.rb +17 -5
- data/lib/rodauth/features/two_factor_base.rb +6 -1
- data/lib/rodauth/features/verify_account.rb +8 -1
- data/lib/rodauth/features/verify_account_grace_period.rb +1 -1
- data/lib/rodauth/features/verify_login_change.rb +5 -2
- data/lib/rodauth/features/webauthn.rb +15 -14
- data/lib/rodauth/features/webauthn_login.rb +1 -1
- data/lib/rodauth/version.rb +1 -1
- data/templates/button.str +1 -1
- data/templates/change-password.str +2 -2
- data/templates/global-logout-field.str +1 -1
- data/templates/login-confirm-field.str +2 -2
- data/templates/login-display.str +2 -2
- data/templates/login-field.str +2 -2
- data/templates/otp-auth-code-field.str +2 -2
- data/templates/otp-setup.str +2 -2
- data/templates/password-confirm-field.str +2 -2
- data/templates/password-field.str +2 -2
- data/templates/recovery-auth.str +2 -2
- data/templates/remember.str +1 -1
- data/templates/sms-code-field.str +2 -2
- data/templates/sms-setup.str +2 -2
- data/templates/unlock-account-email.str +1 -1
- data/templates/webauthn-remove.str +1 -1
- metadata +19 -3
@@ -27,6 +27,8 @@ module Rodauth
|
|
27
27
|
:new_account
|
28
28
|
)
|
29
29
|
|
30
|
+
internal_request_method
|
31
|
+
|
30
32
|
route do |r|
|
31
33
|
check_already_logged_in
|
32
34
|
before_create_account_route
|
@@ -43,7 +45,7 @@ module Rodauth
|
|
43
45
|
|
44
46
|
catch_error do
|
45
47
|
if require_login_confirmation? && login != param(login_confirm_param)
|
46
|
-
|
48
|
+
throw_error_reason(:logins_do_not_match, unmatched_field_error_status, login_param, logins_do_not_match_message)
|
47
49
|
end
|
48
50
|
|
49
51
|
unless login_meets_requirements?(login)
|
@@ -52,11 +54,11 @@ module Rodauth
|
|
52
54
|
|
53
55
|
if create_account_set_password?
|
54
56
|
if require_password_confirmation? && password != param(password_confirm_param)
|
55
|
-
|
57
|
+
throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
|
56
58
|
end
|
57
59
|
|
58
60
|
unless password_meets_requirements?(password)
|
59
|
-
|
61
|
+
throw_error_reason(:password_does_not_meet_requirements, invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
|
60
62
|
end
|
61
63
|
|
62
64
|
if account_password_hash_column
|
@@ -100,7 +102,7 @@ module Rodauth
|
|
100
102
|
raised = raises_uniqueness_violation?{id = db[accounts_table].insert(account)}
|
101
103
|
|
102
104
|
if raised
|
103
|
-
|
105
|
+
set_login_requirement_error_message(:already_an_account_with_this_login, already_an_account_with_this_login_message)
|
104
106
|
end
|
105
107
|
|
106
108
|
if id
|
@@ -32,7 +32,7 @@ module Rodauth
|
|
32
32
|
|
33
33
|
def password_not_one_of_the_most_common?(password)
|
34
34
|
return true unless password_one_of_most_common?(password)
|
35
|
-
|
35
|
+
set_password_requirement_error_message(:password_is_one_of_the_most_common, password_is_one_of_the_most_common_message)
|
36
36
|
false
|
37
37
|
end
|
38
38
|
end
|
@@ -65,7 +65,7 @@ module Rodauth
|
|
65
65
|
end
|
66
66
|
|
67
67
|
return true unless match
|
68
|
-
|
68
|
+
set_password_requirement_error_message(:password_same_as_previous_password, password_same_as_previous_password_message)
|
69
69
|
false
|
70
70
|
end
|
71
71
|
|
@@ -49,6 +49,10 @@ module Rodauth
|
|
49
49
|
|
50
50
|
auth_private_methods :account_from_email_auth_key
|
51
51
|
|
52
|
+
internal_request_method
|
53
|
+
internal_request_method :email_auth_request
|
54
|
+
internal_request_method :valid_email_auth?
|
55
|
+
|
52
56
|
route(:email_auth_request) do |r|
|
53
57
|
check_already_logged_in
|
54
58
|
before_email_auth_request_route
|
@@ -58,6 +62,7 @@ module Rodauth
|
|
58
62
|
_email_auth_request
|
59
63
|
else
|
60
64
|
set_redirect_error_status(no_matching_login_error_status)
|
65
|
+
set_error_reason :no_matching_login
|
61
66
|
set_redirect_error_flash email_auth_request_error_flash
|
62
67
|
end
|
63
68
|
|
@@ -90,6 +95,7 @@ module Rodauth
|
|
90
95
|
key = session[email_auth_session_key] || param(email_auth_key_param)
|
91
96
|
unless account_from_email_auth_key(key)
|
92
97
|
set_redirect_error_status(invalid_key_error_status)
|
98
|
+
set_error_reason :invalid_email_auth_key
|
93
99
|
set_redirect_error_flash email_auth_error_flash
|
94
100
|
redirect email_auth_email_sent_redirect
|
95
101
|
end
|
@@ -0,0 +1,367 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
INVALID_DOMAIN = "invalidurl @@.com"
|
7
|
+
|
8
|
+
class InternalRequestError < StandardError
|
9
|
+
attr_accessor :flash
|
10
|
+
attr_accessor :reason
|
11
|
+
attr_accessor :field_errors
|
12
|
+
|
13
|
+
def initialize(attrs)
|
14
|
+
return super if attrs.is_a?(String)
|
15
|
+
|
16
|
+
@flash = attrs[:flash]
|
17
|
+
@reason = attrs[:reason]
|
18
|
+
@field_errors = attrs[:field_errors] || {}
|
19
|
+
|
20
|
+
super(build_message)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def build_message
|
26
|
+
extras = []
|
27
|
+
extras << reason if reason
|
28
|
+
extras << field_errors unless field_errors.empty?
|
29
|
+
extras = (" (#{extras.join(", ")})" unless extras.empty?)
|
30
|
+
|
31
|
+
"#{flash}#{extras}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module InternalRequestMethods
|
36
|
+
attr_accessor :session
|
37
|
+
attr_accessor :params
|
38
|
+
attr_reader :flash
|
39
|
+
attr_accessor :internal_request_block
|
40
|
+
|
41
|
+
def domain
|
42
|
+
d = super
|
43
|
+
if d == INVALID_DOMAIN
|
44
|
+
raise InternalRequestError, "must set domain in configuration, as it cannot be determined from internal request"
|
45
|
+
end
|
46
|
+
d
|
47
|
+
end
|
48
|
+
|
49
|
+
def raw_param(k)
|
50
|
+
@params[k]
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_error_flash(message)
|
54
|
+
@flash = message
|
55
|
+
_handle_internal_request_error
|
56
|
+
end
|
57
|
+
alias set_redirect_error_flash set_error_flash
|
58
|
+
|
59
|
+
def set_notice_flash(message)
|
60
|
+
@flash = message
|
61
|
+
end
|
62
|
+
alias set_notice_now_flash set_notice_flash
|
63
|
+
|
64
|
+
def modifications_require_password?
|
65
|
+
false
|
66
|
+
end
|
67
|
+
alias require_login_confirmation? modifications_require_password?
|
68
|
+
alias require_password_confirmation? modifications_require_password?
|
69
|
+
alias change_login_requires_password? modifications_require_password?
|
70
|
+
alias change_password_requires_password? modifications_require_password?
|
71
|
+
alias close_account_requires_password? modifications_require_password?
|
72
|
+
alias two_factor_modifications_require_password? modifications_require_password?
|
73
|
+
|
74
|
+
def otp_setup_view
|
75
|
+
hash = {:otp_setup=>otp_user_key}
|
76
|
+
hash[:otp_setup_raw] = otp_key if hmac_secret
|
77
|
+
_return_from_internal_request(hash)
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_recovery_codes_view
|
81
|
+
_return_from_internal_request(recovery_codes)
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_internal_request(meth)
|
85
|
+
catch(:halt) do
|
86
|
+
_around_rodauth do
|
87
|
+
before_rodauth
|
88
|
+
send(meth, request)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
@internal_request_return_value
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def internal_request?
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_error_reason(reason)
|
102
|
+
@error_reason = reason
|
103
|
+
end
|
104
|
+
|
105
|
+
def after_login
|
106
|
+
super
|
107
|
+
_set_internal_request_return_value(account_id) unless @return_false_on_error
|
108
|
+
end
|
109
|
+
|
110
|
+
def after_remember
|
111
|
+
super
|
112
|
+
if params[remember_param] == remember_remember_param_value
|
113
|
+
_set_internal_request_return_value("#{account_id}_#{convert_token_key(remember_key_value)}")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def after_load_memory
|
118
|
+
super
|
119
|
+
_return_from_internal_request(session_value)
|
120
|
+
end
|
121
|
+
|
122
|
+
def before_change_password_route
|
123
|
+
super
|
124
|
+
params[new_password_param] ||= params[password_param]
|
125
|
+
end
|
126
|
+
|
127
|
+
def before_email_auth_request_route
|
128
|
+
super
|
129
|
+
_set_login_param_from_account
|
130
|
+
end
|
131
|
+
|
132
|
+
def before_login_route
|
133
|
+
super
|
134
|
+
_set_login_param_from_account
|
135
|
+
end
|
136
|
+
|
137
|
+
def before_unlock_account_request_route
|
138
|
+
super
|
139
|
+
_set_login_param_from_account
|
140
|
+
end
|
141
|
+
|
142
|
+
def before_reset_password_request_route
|
143
|
+
super
|
144
|
+
_set_login_param_from_account
|
145
|
+
end
|
146
|
+
|
147
|
+
def before_verify_account_resend_route
|
148
|
+
super
|
149
|
+
_set_login_param_from_account
|
150
|
+
end
|
151
|
+
|
152
|
+
def account_from_key(token, status_id=nil)
|
153
|
+
return super unless session_value
|
154
|
+
return unless yield session_value
|
155
|
+
ds = account_ds(session_value)
|
156
|
+
ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
|
157
|
+
ds.first
|
158
|
+
end
|
159
|
+
|
160
|
+
def _set_internal_request_return_value(value)
|
161
|
+
@internal_request_return_value = value
|
162
|
+
end
|
163
|
+
|
164
|
+
def _return_from_internal_request(value)
|
165
|
+
_set_internal_request_return_value(value)
|
166
|
+
throw(:halt)
|
167
|
+
end
|
168
|
+
|
169
|
+
def _handle_internal_request_error
|
170
|
+
if @return_false_on_error
|
171
|
+
_return_from_internal_request(false)
|
172
|
+
else
|
173
|
+
raise InternalRequestError.new(flash: @flash, reason: @error_reason, field_errors: @field_errors)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def _return_false_on_error!
|
178
|
+
@return_false_on_error = true
|
179
|
+
end
|
180
|
+
|
181
|
+
def _set_login_param_from_account
|
182
|
+
if session_value && !params[login_param] && (account = account_ds(session_value).first)
|
183
|
+
params[login_param] = account[login_column]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def _get_remember_cookie
|
188
|
+
params[remember_param]
|
189
|
+
end
|
190
|
+
|
191
|
+
def _handle_internal_request_eval(_)
|
192
|
+
v = instance_eval(&internal_request_block)
|
193
|
+
_set_internal_request_return_value(v) unless defined?(@internal_request_return_value)
|
194
|
+
end
|
195
|
+
|
196
|
+
def _handle_account_id_for_login(_)
|
197
|
+
raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
|
198
|
+
raise InternalRequestError, "no account for login" unless account = account_from_login(login)
|
199
|
+
_return_from_internal_request(account[account_id_column])
|
200
|
+
end
|
201
|
+
|
202
|
+
def _handle_account_exists?(_)
|
203
|
+
raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
|
204
|
+
_return_from_internal_request(!!account_from_login(login))
|
205
|
+
end
|
206
|
+
|
207
|
+
def _handle_lock_account(_)
|
208
|
+
raised_uniqueness_violation{account_lockouts_ds(session_value).insert(_setup_account_lockouts_hash(session_value, generate_unlock_account_key))}
|
209
|
+
end
|
210
|
+
|
211
|
+
def _handle_remember_setup(request)
|
212
|
+
params[remember_param] = remember_remember_param_value
|
213
|
+
_handle_remember(request)
|
214
|
+
end
|
215
|
+
|
216
|
+
def _handle_remember_disable(request)
|
217
|
+
params[remember_param] = remember_disable_param_value
|
218
|
+
_handle_remember(request)
|
219
|
+
end
|
220
|
+
|
221
|
+
def _handle_account_id_for_remember_key(request)
|
222
|
+
load_memory
|
223
|
+
raise InternalRequestError, "invalid remember key"
|
224
|
+
end
|
225
|
+
|
226
|
+
def _handle_otp_setup_params(request)
|
227
|
+
request.env['REQUEST_METHOD'] = 'GET'
|
228
|
+
_handle_otp_setup(request)
|
229
|
+
end
|
230
|
+
|
231
|
+
def _predicate_internal_request(meth, request)
|
232
|
+
_return_false_on_error!
|
233
|
+
_set_internal_request_return_value(true)
|
234
|
+
send(meth, request)
|
235
|
+
end
|
236
|
+
|
237
|
+
def _handle_valid_login_and_password?(request)
|
238
|
+
_predicate_internal_request(:_handle_login, request)
|
239
|
+
end
|
240
|
+
|
241
|
+
def _handle_valid_email_auth?(request)
|
242
|
+
_predicate_internal_request(:_handle_email_auth, request)
|
243
|
+
end
|
244
|
+
|
245
|
+
def _handle_valid_otp_auth?(request)
|
246
|
+
_predicate_internal_request(:_handle_otp_auth, request)
|
247
|
+
end
|
248
|
+
|
249
|
+
def _handle_valid_recovery_auth?(request)
|
250
|
+
_predicate_internal_request(:_handle_recovery_auth, request)
|
251
|
+
end
|
252
|
+
|
253
|
+
def _handle_valid_sms_auth?(request)
|
254
|
+
_predicate_internal_request(:_handle_sms_auth, request)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
module InternalRequestClassMethods
|
259
|
+
def internal_request(route, opts={}, &block)
|
260
|
+
opts = opts.dup
|
261
|
+
|
262
|
+
env = {
|
263
|
+
'REQUEST_METHOD'=>'POST',
|
264
|
+
'PATH_INFO'=>'/',
|
265
|
+
"SCRIPT_NAME" => "",
|
266
|
+
"HTTP_HOST" => INVALID_DOMAIN,
|
267
|
+
"SERVER_NAME" => INVALID_DOMAIN,
|
268
|
+
"SERVER_PORT" => 443,
|
269
|
+
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
|
270
|
+
"rack.input"=>StringIO.new(''),
|
271
|
+
"rack.url_scheme"=>"https"
|
272
|
+
}
|
273
|
+
env.merge!(opts.delete(:env)) if opts[:env]
|
274
|
+
|
275
|
+
session = {}
|
276
|
+
session.merge!(opts.delete(:session)) if opts[:session]
|
277
|
+
|
278
|
+
params = {}
|
279
|
+
params.merge!(opts.delete(:params)) if opts[:params]
|
280
|
+
|
281
|
+
scope = roda_class.new(env)
|
282
|
+
rodauth = new(scope)
|
283
|
+
rodauth.session = session
|
284
|
+
rodauth.params = params
|
285
|
+
rodauth.internal_request_block = block
|
286
|
+
|
287
|
+
unless account_id = opts.delete(:account_id)
|
288
|
+
if (account_login = opts.delete(:account_login))
|
289
|
+
if (account = rodauth.send(:_account_from_login, account_login))
|
290
|
+
account_id = account[rodauth.account_id_column]
|
291
|
+
else
|
292
|
+
raise InternalRequestError, "no account for login: #{account_login.inspect}"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
if account_id
|
298
|
+
session[rodauth.session_key] = account_id
|
299
|
+
unless authenticated_by = opts.delete(:authenticated_by)
|
300
|
+
authenticated_by = case route
|
301
|
+
when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
|
302
|
+
['internal1']
|
303
|
+
else
|
304
|
+
['internal1', 'internal2']
|
305
|
+
end
|
306
|
+
end
|
307
|
+
session[rodauth.authenticated_by_session_key] = authenticated_by
|
308
|
+
end
|
309
|
+
|
310
|
+
opts.keys.each do |k|
|
311
|
+
meth = :"#{k}_param"
|
312
|
+
params[rodauth.public_send(meth).to_s] = opts.delete(k) if rodauth.respond_to?(meth)
|
313
|
+
end
|
314
|
+
|
315
|
+
unless opts.empty?
|
316
|
+
warn "unhandled options passed to #{route}: #{opts.inspect}"
|
317
|
+
end
|
318
|
+
|
319
|
+
rodauth.handle_internal_request(:"_handle_#{route}")
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
Feature.define(:internal_request, :InternalRequest) do
|
324
|
+
configuration_module_eval do
|
325
|
+
def internal_request_configuration(&block)
|
326
|
+
@auth.instance_exec do
|
327
|
+
(@internal_request_configuration_blocks ||= []) << block
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def post_configure
|
333
|
+
super
|
334
|
+
|
335
|
+
return if is_a?(InternalRequestMethods)
|
336
|
+
|
337
|
+
klass = self.class
|
338
|
+
internal_class = Class.new(klass) do
|
339
|
+
@roda_class = klass.roda_class
|
340
|
+
@features = klass.features.clone
|
341
|
+
@routes = klass.routes.clone
|
342
|
+
@route_hash = klass.route_hash.clone
|
343
|
+
@configuration = klass.configuration.clone
|
344
|
+
@configuration.instance_variable_set(:@auth, self)
|
345
|
+
end
|
346
|
+
|
347
|
+
if blocks = klass.instance_variable_get(:@internal_request_configuration_blocks)
|
348
|
+
configuration = internal_class.configuration
|
349
|
+
blocks.each do |block|
|
350
|
+
configuration.instance_exec(&block)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
internal_class.send(:extend, InternalRequestClassMethods)
|
354
|
+
internal_class.send(:include, InternalRequestMethods)
|
355
|
+
internal_class.allocate.post_configure
|
356
|
+
|
357
|
+
([:base] + internal_class.features).each do |feature_name|
|
358
|
+
feature = FEATURES[feature_name]
|
359
|
+
if meths = feature.internal_request_methods
|
360
|
+
meths.each do |name|
|
361
|
+
klass.define_singleton_method(name){|opts={}, &block| internal_class.internal_request(name, opts, &block)}
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|