rodauth 2.13.0 → 2.17.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ = New Features
2
+
3
+ * An internal_request feature has been added. This feature allows
4
+ for interacting with Rodauth by calling methods, instead of having
5
+ to use a website or JSON API. This feature is designed primarily
6
+ for administrative use, so that administrators can create accounts,
7
+ change passwords or logins for accounts, and handle similar actions
8
+ without the user of the account being involved.
9
+
10
+ For example, assuming you've loaded the change_password and
11
+ internal_request features, and that your Roda class that
12
+ is loading Rodauth is named App, you can change the password
13
+ for the account with id 1 using:
14
+
15
+ App.rodauth.change_password(account_id: 1, password: 'foobar')
16
+
17
+ The internal request methods are implemented as class methods
18
+ on the Rodauth::Auth subclass (the object returned by App.rodauth).
19
+ These methods call methods on a subclass of that class specific
20
+ to internal requests.
21
+
22
+ The reason the feature is named internal_request is that these
23
+ methods are implemented by submitting a request internally, that is
24
+ processed almost exactly the same way as Rodauth would process a
25
+ web request.
26
+
27
+ See the internal_request feature documentation for details on which
28
+ internal request methods are available and the options they take.
29
+
30
+ * A path_class_methods feature has been added, that allows for calling
31
+ *_path and *_url as class methods. If you would like to call the
32
+ *_url methods as class methods, make sure to use the base_url
33
+ configuration method to set the base URL so that it does not require
34
+ request-specific information.
35
+
36
+ * Rodauth::Auth classes now have a configuration_name method that
37
+ returns the configuration name associated with the class. They also
38
+ have a configuration method that returns the configuration
39
+ associated with the class.
40
+
41
+ * Rodauth::Feature now supports an internal_request_method method for
42
+ specifying which methods are supported as internal request methods.
43
+
44
+ = Other Improvements
45
+
46
+ * The default base_url configuration method will now use the domain
47
+ method to get the domain to use, instead of getting the domain
48
+ information directly from the request environment.
@@ -0,0 +1,20 @@
1
+ = New Features
2
+
3
+ * Rodauth.lib has been added for using Rodauth purely as a library,
4
+ useful in non-web applications:
5
+
6
+ require 'rodauth'
7
+ rodauth = Rodauth.lib do
8
+ enable :create_account, :change_password
9
+ end
10
+ rodauth.create_account(login: 'foo@example.com', password: '...')
11
+ rodauth.change_password(account_id: 24601, password: '...')
12
+
13
+ This is built on top of the internal_request feature, and works by
14
+ creating a Roda application with the rodauth plugin, and returning
15
+ the related Rodauth::Auth class.
16
+
17
+ = Other Improvements
18
+
19
+ * The internal_request feature now works correctly for configurations
20
+ where only_json? is set to true.
@@ -0,0 +1,10 @@
1
+ = Improvements
2
+
3
+ * The jwt_refresh feature now works for unverified accounts when using
4
+ the verify_account_grace_period feature.
5
+
6
+ * When trying to create an account that already exists but is
7
+ unverified, Rodauth now returns a 4xx response.
8
+
9
+ * When trying to login to an unverified account, Rodauth now returns a
10
+ 4xx response.
data/doc/remember.rdoc CHANGED
@@ -69,6 +69,7 @@ generate_remember_key_value :: A random string to use as the remember key.
69
69
  get_remember_key :: Retrieve the remember key from the database.
70
70
  load_memory :: If the remember key cookie is included in the request, and the user is not currently logged in, check the remember keys table and autologin the user if the remember key cookie matches the current remember key for the account. This method needs to be called manually inside the Roda route block to autologin users.
71
71
  logged_in_via_remember_key? :: Whether the current session was logged in via a remember key.
72
+ remembered_session_id :: The session_id which is validly remembered, if any.
72
73
  remember_key_value :: The current value of the remember key/token.
73
74
  remember_login :: Set the cookie containing the remember token, so that future sessions will be autologged in.
74
75
  remember_view :: The HTML to use for the change remember settings form.
@@ -115,6 +115,10 @@ module Rodauth
115
115
  :around_rodauth
116
116
  )
117
117
 
118
+ internal_request_method :account_exists?
119
+ internal_request_method :account_id_for_login
120
+ internal_request_method :internal_request_eval
121
+
118
122
  configuration_module_eval do
119
123
  def auth_class_eval(&block)
120
124
  auth.class_eval(&block)
@@ -445,7 +449,9 @@ module Rodauth
445
449
  end
446
450
 
447
451
  def base_url
448
- request.base_url
452
+ url = String.new("#{request.scheme}://#{domain}")
453
+ url << ":#{request.port}" if request.port != Rack::Request::DEFAULT_PORTS[request.scheme]
454
+ url
449
455
  end
450
456
 
451
457
  def domain
@@ -734,6 +740,10 @@ module Rodauth
734
740
  end
735
741
  end
736
742
 
743
+ def internal_request?
744
+ false
745
+ end
746
+
737
747
  def set_session_value(key, value)
738
748
  session[key] = value
739
749
  end
@@ -19,6 +19,8 @@ module Rodauth
19
19
 
20
20
  auth_methods :change_login
21
21
 
22
+ internal_request_method
23
+
22
24
  route do |r|
23
25
  require_account
24
26
  before_change_login_route
@@ -22,6 +22,8 @@ module Rodauth
22
22
  :invalid_previous_password_message
23
23
  )
24
24
 
25
+ internal_request_method
26
+
25
27
  route do |r|
26
28
  require_account
27
29
  before_change_password_route
@@ -24,6 +24,8 @@ module Rodauth
24
24
  :delete_account
25
25
  )
26
26
 
27
+ internal_request_method
28
+
27
29
  route do |r|
28
30
  require_account
29
31
  before_close_account_route
@@ -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
@@ -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
@@ -0,0 +1,371 @@
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
+ def only_json?
96
+ false
97
+ end
98
+
99
+ private
100
+
101
+ def internal_request?
102
+ true
103
+ end
104
+
105
+ def set_error_reason(reason)
106
+ @error_reason = reason
107
+ end
108
+
109
+ def after_login
110
+ super
111
+ _set_internal_request_return_value(account_id) unless @return_false_on_error
112
+ end
113
+
114
+ def after_remember
115
+ super
116
+ if params[remember_param] == remember_remember_param_value
117
+ _set_internal_request_return_value("#{account_id}_#{convert_token_key(remember_key_value)}")
118
+ end
119
+ end
120
+
121
+ def after_load_memory
122
+ super
123
+ _return_from_internal_request(session_value)
124
+ end
125
+
126
+ def before_change_password_route
127
+ super
128
+ params[new_password_param] ||= params[password_param]
129
+ end
130
+
131
+ def before_email_auth_request_route
132
+ super
133
+ _set_login_param_from_account
134
+ end
135
+
136
+ def before_login_route
137
+ super
138
+ _set_login_param_from_account
139
+ end
140
+
141
+ def before_unlock_account_request_route
142
+ super
143
+ _set_login_param_from_account
144
+ end
145
+
146
+ def before_reset_password_request_route
147
+ super
148
+ _set_login_param_from_account
149
+ end
150
+
151
+ def before_verify_account_resend_route
152
+ super
153
+ _set_login_param_from_account
154
+ end
155
+
156
+ def account_from_key(token, status_id=nil)
157
+ return super unless session_value
158
+ return unless yield session_value
159
+ ds = account_ds(session_value)
160
+ ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
161
+ ds.first
162
+ end
163
+
164
+ def _set_internal_request_return_value(value)
165
+ @internal_request_return_value = value
166
+ end
167
+
168
+ def _return_from_internal_request(value)
169
+ _set_internal_request_return_value(value)
170
+ throw(:halt)
171
+ end
172
+
173
+ def _handle_internal_request_error
174
+ if @return_false_on_error
175
+ _return_from_internal_request(false)
176
+ else
177
+ raise InternalRequestError.new(flash: @flash, reason: @error_reason, field_errors: @field_errors)
178
+ end
179
+ end
180
+
181
+ def _return_false_on_error!
182
+ @return_false_on_error = true
183
+ end
184
+
185
+ def _set_login_param_from_account
186
+ if session_value && !params[login_param] && (account = account_ds(session_value).first)
187
+ params[login_param] = account[login_column]
188
+ end
189
+ end
190
+
191
+ def _get_remember_cookie
192
+ params[remember_param]
193
+ end
194
+
195
+ def _handle_internal_request_eval(_)
196
+ v = instance_eval(&internal_request_block)
197
+ _set_internal_request_return_value(v) unless defined?(@internal_request_return_value)
198
+ end
199
+
200
+ def _handle_account_id_for_login(_)
201
+ raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
202
+ raise InternalRequestError, "no account for login" unless account = account_from_login(login)
203
+ _return_from_internal_request(account[account_id_column])
204
+ end
205
+
206
+ def _handle_account_exists?(_)
207
+ raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
208
+ _return_from_internal_request(!!account_from_login(login))
209
+ end
210
+
211
+ def _handle_lock_account(_)
212
+ raised_uniqueness_violation{account_lockouts_ds(session_value).insert(_setup_account_lockouts_hash(session_value, generate_unlock_account_key))}
213
+ end
214
+
215
+ def _handle_remember_setup(request)
216
+ params[remember_param] = remember_remember_param_value
217
+ _handle_remember(request)
218
+ end
219
+
220
+ def _handle_remember_disable(request)
221
+ params[remember_param] = remember_disable_param_value
222
+ _handle_remember(request)
223
+ end
224
+
225
+ def _handle_account_id_for_remember_key(request)
226
+ load_memory
227
+ raise InternalRequestError, "invalid remember key"
228
+ end
229
+
230
+ def _handle_otp_setup_params(request)
231
+ request.env['REQUEST_METHOD'] = 'GET'
232
+ _handle_otp_setup(request)
233
+ end
234
+
235
+ def _predicate_internal_request(meth, request)
236
+ _return_false_on_error!
237
+ _set_internal_request_return_value(true)
238
+ send(meth, request)
239
+ end
240
+
241
+ def _handle_valid_login_and_password?(request)
242
+ _predicate_internal_request(:_handle_login, request)
243
+ end
244
+
245
+ def _handle_valid_email_auth?(request)
246
+ _predicate_internal_request(:_handle_email_auth, request)
247
+ end
248
+
249
+ def _handle_valid_otp_auth?(request)
250
+ _predicate_internal_request(:_handle_otp_auth, request)
251
+ end
252
+
253
+ def _handle_valid_recovery_auth?(request)
254
+ _predicate_internal_request(:_handle_recovery_auth, request)
255
+ end
256
+
257
+ def _handle_valid_sms_auth?(request)
258
+ _predicate_internal_request(:_handle_sms_auth, request)
259
+ end
260
+ end
261
+
262
+ module InternalRequestClassMethods
263
+ def internal_request(route, opts={}, &block)
264
+ opts = opts.dup
265
+
266
+ env = {
267
+ 'REQUEST_METHOD'=>'POST',
268
+ 'PATH_INFO'=>'/',
269
+ "SCRIPT_NAME" => "",
270
+ "HTTP_HOST" => INVALID_DOMAIN,
271
+ "SERVER_NAME" => INVALID_DOMAIN,
272
+ "SERVER_PORT" => 443,
273
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
274
+ "rack.input"=>StringIO.new(''),
275
+ "rack.url_scheme"=>"https"
276
+ }
277
+ env.merge!(opts.delete(:env)) if opts[:env]
278
+
279
+ session = {}
280
+ session.merge!(opts.delete(:session)) if opts[:session]
281
+
282
+ params = {}
283
+ params.merge!(opts.delete(:params)) if opts[:params]
284
+
285
+ scope = roda_class.new(env)
286
+ rodauth = new(scope)
287
+ rodauth.session = session
288
+ rodauth.params = params
289
+ rodauth.internal_request_block = block
290
+
291
+ unless account_id = opts.delete(:account_id)
292
+ if (account_login = opts.delete(:account_login))
293
+ if (account = rodauth.send(:_account_from_login, account_login))
294
+ account_id = account[rodauth.account_id_column]
295
+ else
296
+ raise InternalRequestError, "no account for login: #{account_login.inspect}"
297
+ end
298
+ end
299
+ end
300
+
301
+ if account_id
302
+ session[rodauth.session_key] = account_id
303
+ unless authenticated_by = opts.delete(:authenticated_by)
304
+ authenticated_by = case route
305
+ when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
306
+ ['internal1']
307
+ else
308
+ ['internal1', 'internal2']
309
+ end
310
+ end
311
+ session[rodauth.authenticated_by_session_key] = authenticated_by
312
+ end
313
+
314
+ opts.keys.each do |k|
315
+ meth = :"#{k}_param"
316
+ params[rodauth.public_send(meth).to_s] = opts.delete(k) if rodauth.respond_to?(meth)
317
+ end
318
+
319
+ unless opts.empty?
320
+ warn "unhandled options passed to #{route}: #{opts.inspect}"
321
+ end
322
+
323
+ rodauth.handle_internal_request(:"_handle_#{route}")
324
+ end
325
+ end
326
+
327
+ Feature.define(:internal_request, :InternalRequest) do
328
+ configuration_module_eval do
329
+ def internal_request_configuration(&block)
330
+ @auth.instance_exec do
331
+ (@internal_request_configuration_blocks ||= []) << block
332
+ end
333
+ end
334
+ end
335
+
336
+ def post_configure
337
+ super
338
+
339
+ return if is_a?(InternalRequestMethods)
340
+
341
+ klass = self.class
342
+ internal_class = Class.new(klass) do
343
+ @roda_class = klass.roda_class
344
+ @features = klass.features.clone
345
+ @routes = klass.routes.clone
346
+ @route_hash = klass.route_hash.clone
347
+ @configuration = klass.configuration.clone
348
+ @configuration.instance_variable_set(:@auth, self)
349
+ end
350
+
351
+ if blocks = klass.instance_variable_get(:@internal_request_configuration_blocks)
352
+ configuration = internal_class.configuration
353
+ blocks.each do |block|
354
+ configuration.instance_exec(&block)
355
+ end
356
+ end
357
+ internal_class.send(:extend, InternalRequestClassMethods)
358
+ internal_class.send(:include, InternalRequestMethods)
359
+ internal_class.allocate.post_configure
360
+
361
+ ([:base] + internal_class.features).each do |feature_name|
362
+ feature = FEATURES[feature_name]
363
+ if meths = feature.internal_request_methods
364
+ meths.each do |name|
365
+ klass.define_singleton_method(name){|opts={}, &block| internal_class.internal_request(name, opts, &block)}
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
371
+ end
@@ -32,8 +32,6 @@ module Rodauth
32
32
  )
33
33
 
34
34
  route do |r|
35
- # For backward compatibility, unused in core Rodauth
36
- # RODAUTH3: Remove
37
35
  @jwt_refresh_route = true
38
36
  before_jwt_refresh_route
39
37
 
@@ -100,7 +98,7 @@ module Rodauth
100
98
  # JWT is invalid for other reasons. Make sure the expiration is the
101
99
  # only reason the JWT isn't valid before treating this as an expired token.
102
100
  JWT.decode(jwt_token, jwt_secret, true, Hash[jwt_decode_opts].merge!(:verify_expiration=>false, :algorithm=>jwt_algorithm))[0]
103
- rescue => e
101
+ rescue
104
102
  else
105
103
  json_response[json_response_error_key] = expired_jwt_access_token_message
106
104
  response.status ||= expired_jwt_access_token_status
@@ -122,7 +120,7 @@ module Rodauth
122
120
  end
123
121
 
124
122
  ds = account_ds(id)
125
- ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks?
123
+ ds = ds.where(account_session_status_filter) unless skip_status_checks?
126
124
  ds.first
127
125
  end
128
126
 
@@ -137,7 +135,7 @@ module Rodauth
137
135
  end
138
136
 
139
137
  def _jwt_decode_opts
140
- if allow_refresh_with_expired_jwt_access_token? && request.path == jwt_refresh_path
138
+ if allow_refresh_with_expired_jwt_access_token? && (@jwt_refresh_route || request.path == jwt_refresh_path)
141
139
  Hash[super].merge!(:verify_expiration=>false)
142
140
  else
143
141
  super
@@ -62,6 +62,10 @@ module Rodauth
62
62
  )
63
63
  auth_private_methods :account_from_unlock_key
64
64
 
65
+ internal_request_method(:lock_account)
66
+ internal_request_method(:unlock_account_request)
67
+ internal_request_method(:unlock_account)
68
+
65
69
  route(:unlock_account_request) do |r|
66
70
  check_already_logged_in
67
71
  before_unlock_account_request_route
@@ -167,6 +171,12 @@ module Rodauth
167
171
  unlock_account
168
172
  end
169
173
 
174
+ def _setup_account_lockouts_hash(account_id, key)
175
+ hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>key}
176
+ set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
177
+ hash
178
+ end
179
+
170
180
  def invalid_login_attempted
171
181
  ds = account_login_failures_ds.
172
182
  where(account_login_failures_id_column=>account_id)
@@ -192,8 +202,7 @@ module Rodauth
192
202
 
193
203
  if number >= max_invalid_logins
194
204
  @unlock_account_key_value = generate_unlock_account_key
195
- hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>unlock_account_key_value}
196
- set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
205
+ hash = _setup_account_lockouts_hash(account_id, unlock_account_key_value)
197
206
 
198
207
  if e = raised_uniqueness_violation{account_lockouts_ds.insert(hash)}
199
208
  # If inserting into the lockout table raises a violation, we should just be able to pull the already inserted
@@ -25,6 +25,9 @@ module Rodauth
25
25
 
26
26
  auth_value_methods :login_return_to_requested_location_path
27
27
 
28
+ internal_request_method
29
+ internal_request_method :valid_login_and_password?
30
+
28
31
  route do |r|
29
32
  check_already_logged_in
30
33
  before_login_route
@@ -96,6 +96,12 @@ module Rodauth
96
96
  :otp_tmp_key
97
97
  )
98
98
 
99
+ internal_request_method :otp_setup_params
100
+ internal_request_method :otp_setup
101
+ internal_request_method :otp_auth
102
+ internal_request_method :valid_otp_auth?
103
+ internal_request_method :otp_disable
104
+
99
105
  route(:otp_auth) do |r|
100
106
  require_login
101
107
  require_account_session
@@ -0,0 +1,22 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:path_class_methods, :PathClassMethods) do
5
+ def post_configure
6
+ super
7
+
8
+ klass = self.class
9
+ klass.features.each do |feature_name|
10
+ feature = FEATURES[feature_name]
11
+ feature.routes.each do |handle_meth|
12
+ route = handle_meth.to_s.sub(/\Ahandle_/, '')
13
+ path_meth = :"#{route}_path"
14
+ url_meth = :"#{route}_url"
15
+ instance = klass.allocate.freeze
16
+ klass.define_singleton_method(path_meth){|opts={}| instance.send(path_meth, opts)}
17
+ klass.define_singleton_method(url_meth){|opts={}| instance.send(url_meth, opts)}
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -59,6 +59,10 @@ module Rodauth
59
59
  :recovery_code_match?,
60
60
  )
61
61
 
62
+ internal_request_method :recovery_codes
63
+ internal_request_method :recovery_auth
64
+ internal_request_method :valid_recovery_auth?
65
+
62
66
  route(:recovery_auth) do |r|
63
67
  require_login
64
68
  require_account_session