File: internal_request.rb

package info (click to toggle)
ruby-rodauth 2.42.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 812 kB
  • sloc: ruby: 7,524; javascript: 100; makefile: 4
file content (422 lines) | stat: -rw-r--r-- 11,841 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# frozen-string-literal: true

require 'stringio'

module Rodauth
  INVALID_DOMAIN = "invalidurl @@.com"

  class InternalRequestError < StandardError
    attr_accessor :flash
    attr_accessor :reason
    attr_accessor :field_errors

    def initialize(attrs)
      return super if attrs.is_a?(String)

      @flash = attrs[:flash]
      @reason = attrs[:reason]
      @field_errors = attrs[:field_errors] || {}

      super(build_message)
    end

    private

    def build_message
      extras = []
      extras << reason if reason
      extras << field_errors unless field_errors.empty?
      extras = (" (#{extras.join(", ")})" unless extras.empty?)

      "#{flash}#{extras}"
    end
  end

  module InternalRequestMethods
    attr_accessor :session
    attr_accessor :params
    attr_reader :flash
    attr_accessor :internal_request_block

    def domain
      d = super
      if d.nil? || d == INVALID_DOMAIN
        raise InternalRequestError, "must set domain in configuration, as it cannot be determined from internal request"
      end
      d
    end

    def raw_param(k)
      @params[k]
    end

    def clear_session
      @session.clear
    end

    def set_error_flash(message)
      @flash = message
      _handle_internal_request_error
    end
    alias set_redirect_error_flash set_error_flash

    def set_notice_flash(message)
      @flash = message
    end
    alias set_notice_now_flash set_notice_flash

    def modifications_require_password?
      false
    end
    alias require_login_confirmation? modifications_require_password?
    alias require_password_confirmation? modifications_require_password?
    alias change_login_requires_password? modifications_require_password?
    alias change_password_requires_password? modifications_require_password?
    alias close_account_requires_password? modifications_require_password?
    alias two_factor_modifications_require_password? modifications_require_password?

    def otp_setup_view
      hash = {:otp_setup=>otp_user_key}
      hash[:otp_setup_raw] = otp_key if hmac_secret
      _return_from_internal_request(hash)
    end

    def add_recovery_codes_view
      _return_from_internal_request(recovery_codes)
    end

    def webauthn_setup_view
      cred = new_webauthn_credential
      _return_from_internal_request({
        webauthn_setup: cred.as_json,
        webauthn_setup_challenge: cred.challenge,
        webauthn_setup_challenge_hmac: compute_hmac(cred.challenge)
      })
    end

    def webauthn_auth_view
      cred = webauthn_credential_options_for_get
      _return_from_internal_request({
        webauthn_auth: cred.as_json,
        webauthn_auth_challenge: cred.challenge,
        webauthn_auth_challenge_hmac: compute_hmac(cred.challenge)
      })
    end

    def handle_internal_request(meth)
      catch(:halt) do
        _around_rodauth do
          before_rodauth
          send(meth, request)
        end
      end

      @internal_request_return_value
    end

    def only_json?
      false
    end

    private

    def internal_request?
      true
    end

    def set_error_reason(reason)
      @error_reason = reason
    end

    def after_login
      super
      _set_internal_request_return_value(account_id) unless @return_false_on_error
    end

    def after_remember
      super
      if params[remember_param] == remember_remember_param_value
        _set_internal_request_return_value("#{account_id}_#{convert_token_key(remember_key_value)}")
      end
    end

    def after_load_memory
      super
      _return_from_internal_request(session_value)
    end

    def before_change_password_route
      super
      params[new_password_param] ||= params[password_param]
    end

    def before_email_auth_request_route
      super
      _set_login_param_from_account
    end

    def before_login_route
      super
      _set_login_param_from_account
    end

    def before_unlock_account_request_route
      super
      _set_login_param_from_account
    end

    def before_reset_password_request_route
      super
      _set_login_param_from_account
    end

    def before_verify_account_resend_route
      super
      _set_login_param_from_account
    end

    def before_webauthn_login_route
      super
      _set_login_param_from_account
    end

    def account_from_key(token, status_id=nil)
      return super unless session_value
      return unless yield session_value
      _account_from_id(session_value, status_id)
    end

    def _set_internal_request_return_value(value)
      @internal_request_return_value = value
    end

    def _return_from_internal_request(value)
      _set_internal_request_return_value(value)
      throw(:halt)
    end

    def _handle_internal_request_error
      if @return_false_on_error
        _return_from_internal_request(false)
      else
        raise InternalRequestError.new(flash: @flash, reason: @error_reason, field_errors: @field_errors)
      end
    end

    def _return_false_on_error!
      @return_false_on_error = true
    end

    def _set_login_param_from_account
      if session_value && !params[login_param] && (account = _account_from_id(session_value))
        params[login_param] = account[login_column]
      end
    end

    def _get_remember_cookie
      params[remember_param]
    end

    def _handle_internal_request_eval(_)
      v = instance_eval(&internal_request_block)
      _set_internal_request_return_value(v) unless defined?(@internal_request_return_value)
    end

    def _handle_account_id_for_login(_)
      raise InternalRequestError, "no login provided" unless param_or_nil(login_param)
      raise InternalRequestError, "no account for login" unless account = account_from_login(login_param_value)
      _return_from_internal_request(account[account_id_column])
    end

    def _handle_account_exists?(_)
      raise InternalRequestError, "no login provided" unless param_or_nil(login_param)
      _return_from_internal_request(!!account_from_login(login_param_value))
    end

    def _handle_lock_account(_)
      raised_uniqueness_violation{account_lockouts_ds(session_value).insert(_setup_account_lockouts_hash(session_value, generate_unlock_account_key))}
    end

    def _handle_remember_setup(request)
      params[remember_param] = remember_remember_param_value
      _handle_remember(request)
    end

    def _handle_remember_disable(request)
      params[remember_param] = remember_disable_param_value
      _handle_remember(request)
    end

    def _handle_account_id_for_remember_key(request)
      load_memory
      raise InternalRequestError, "invalid remember key"
    end

    def _handle_otp_setup_params(request)
      request.env['REQUEST_METHOD'] = 'GET'
      _handle_otp_setup(request)
    end

    def _handle_webauthn_setup_params(request)
      request.env['REQUEST_METHOD'] = 'GET'
      _handle_webauthn_setup(request)
    end

    def _handle_webauthn_auth_params(request)
      request.env['REQUEST_METHOD'] = 'GET'
      _handle_webauthn_auth(request)
    end

    def _handle_webauthn_login_params(request)
      _set_login_param_from_account
      unless webauthn_login_options?
        raise InternalRequestError, "no login provided" unless param_or_nil(login_param)
        raise InternalRequestError, "no account for login"
      end
      webauthn_auth_view
    end

    def _predicate_internal_request(meth, request)
      _return_false_on_error!
      _set_internal_request_return_value(true)
      send(meth, request)
    end

    def _handle_valid_login_and_password?(request)
      _predicate_internal_request(:_handle_login, request)
    end

    def _handle_valid_email_auth?(request)
      _predicate_internal_request(:_handle_email_auth, request)
    end

    def _handle_valid_otp_auth?(request)
      _predicate_internal_request(:_handle_otp_auth, request)
    end

    def _handle_valid_recovery_auth?(request)
      _predicate_internal_request(:_handle_recovery_auth, request)
    end

    def _handle_valid_sms_auth?(request)
      _predicate_internal_request(:_handle_sms_auth, request)
    end
  end

  module InternalRequestClassMethods
    def internal_request(route, opts={}, &block)
      opts = opts.dup
      
      env = {
         'REQUEST_METHOD'=>'POST',
         'PATH_INFO'=>'/'.dup,
         "SCRIPT_NAME" => "",
         "HTTP_HOST" => INVALID_DOMAIN,
         "SERVER_NAME" => INVALID_DOMAIN,
         "SERVER_PORT" => 443,
         "CONTENT_TYPE" => "application/x-www-form-urlencoded",
         "rack.input"=>StringIO.new(''),
         "rack.url_scheme"=>"https"
      }
      env.merge!(opts.delete(:env)) if opts[:env]

      session = {}
      session.merge!(opts.delete(:session)) if opts[:session]

      params = {}
      params.merge!(opts.delete(:params)) if opts[:params]

      scope = roda_class.new(env)
      rodauth = new(scope)
      rodauth.session = session
      rodauth.params = params
      rodauth.internal_request_block = block

      unless account_id = opts.delete(:account_id)
        if (account_login = opts.delete(:account_login))
          if (account = rodauth.send(:_account_from_login, account_login))
            account_id = account[rodauth.account_id_column]
          else
            raise InternalRequestError, "no account for login: #{account_login.inspect}"
          end
        end
      end

      if account_id
        session[rodauth.session_key] = account_id
        unless authenticated_by = opts.delete(:authenticated_by)
          authenticated_by = case route
          when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :webauthn_auth, :webauthn_auth_params, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
            ['internal1']
          else
            ['internal1', 'internal2']
          end
        end
        session[rodauth.authenticated_by_session_key] = authenticated_by
      end

      opts.keys.each do |k|
        meth = :"#{k}_param"
        params[rodauth.public_send(meth).to_s] = opts.delete(k) if rodauth.respond_to?(meth)
      end

      unless opts.empty?
        warn "unhandled options passed to #{route}: #{opts.inspect}"
      end

      rodauth.handle_internal_request(:"_handle_#{route}")
    end
  end

  Feature.define(:internal_request, :InternalRequest) do
    configuration_module_eval do
      def internal_request_configuration(&block)
        @auth.instance_exec do
          (@internal_request_configuration_blocks ||= []) << block
        end
      end
    end

    def post_configure
      super

      return if is_a?(InternalRequestMethods)

      superklasses = []
      superklass = self.class
      until superklass == Rodauth::Auth
        superklasses << superklass
        superklass = superklass.superclass
      end

      klass = self.class
      internal_class = Class.new(klass)
      internal_class.instance_variable_set(:@configuration_name, klass.configuration_name)
      configuration = internal_class.configuration

      superklasses.reverse_each do |superklass|
        if blocks = superklass.instance_variable_get(:@internal_request_configuration_blocks)
          blocks.each do |block|
            configuration.instance_exec(&block)
          end
        end
      end

      internal_class.send(:extend, InternalRequestClassMethods)
      internal_class.send(:include, InternalRequestMethods)
      internal_class.allocate.post_configure

      ([:base] + internal_class.features).each do |feature_name|
        feature = FEATURES[feature_name]
        if meths = feature.internal_request_methods
          meths.each do |name|
            klass.define_singleton_method(name){|opts={}, &block| internal_class.internal_request(name, opts, &block)}
          end
        end
      end

      klass.const_set(:InternalRequest, internal_class)
      klass.private_constant :InternalRequest
    end
  end
end