haveapi 0.27.3 → 0.28.1

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/haveapi.gemspec +1 -1
  4. data/lib/haveapi/action.rb +127 -36
  5. data/lib/haveapi/actions/paginable.rb +3 -1
  6. data/lib/haveapi/authentication/basic/provider.rb +2 -0
  7. data/lib/haveapi/authentication/chain.rb +11 -7
  8. data/lib/haveapi/authentication/oauth2/config.rb +25 -3
  9. data/lib/haveapi/authentication/oauth2/provider.rb +92 -11
  10. data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +44 -3
  11. data/lib/haveapi/authentication/token/provider.rb +53 -15
  12. data/lib/haveapi/authorization.rb +42 -18
  13. data/lib/haveapi/client_examples/php_client.rb +1 -1
  14. data/lib/haveapi/client_examples/ruby_client.rb +1 -1
  15. data/lib/haveapi/context.rb +10 -4
  16. data/lib/haveapi/example.rb +15 -16
  17. data/lib/haveapi/extensions/action_exceptions.rb +6 -6
  18. data/lib/haveapi/model_adapters/active_record.rb +140 -68
  19. data/lib/haveapi/model_adapters/hash.rb +1 -1
  20. data/lib/haveapi/parameters/resource.rb +35 -3
  21. data/lib/haveapi/parameters/typed.rb +26 -7
  22. data/lib/haveapi/params.rb +27 -8
  23. data/lib/haveapi/resource.rb +4 -1
  24. data/lib/haveapi/resources/action_state.rb +8 -1
  25. data/lib/haveapi/route.rb +2 -2
  26. data/lib/haveapi/server.rb +137 -45
  27. data/lib/haveapi/validator.rb +2 -2
  28. data/lib/haveapi/validator_chain.rb +1 -0
  29. data/lib/haveapi/validators/confirmation.rb +1 -0
  30. data/lib/haveapi/validators/format.rb +6 -2
  31. data/lib/haveapi/validators/length.rb +2 -0
  32. data/lib/haveapi/validators/numericality.rb +2 -0
  33. data/lib/haveapi/validators/presence.rb +1 -1
  34. data/lib/haveapi/version.rb +1 -1
  35. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  36. data/lib/haveapi/views/version_page/client_example.erb +3 -3
  37. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  38. data/lib/haveapi/views/version_page.erb +2 -2
  39. data/lib/haveapi/views/version_sidebar.erb +4 -2
  40. data/spec/action/authorize_spec.rb +99 -0
  41. data/spec/action/runtime_spec.rb +433 -0
  42. data/spec/action_state_spec.rb +52 -0
  43. data/spec/authentication/basic_spec.rb +29 -0
  44. data/spec/authentication/oauth2_spec.rb +329 -0
  45. data/spec/authentication/token_spec.rb +195 -0
  46. data/spec/authentication/token_version_routes_spec.rb +164 -0
  47. data/spec/authorization_spec.rb +66 -0
  48. data/spec/documentation/auth_filtering_spec.rb +195 -1
  49. data/spec/documentation/current_user_html_escaping_spec.rb +47 -0
  50. data/spec/documentation/examples_spec.rb +97 -0
  51. data/spec/documentation/host_html_escaping_spec.rb +41 -0
  52. data/spec/documentation_spec.rb +13 -0
  53. data/spec/extensions/action_exceptions_spec.rb +30 -0
  54. data/spec/model_adapters/active_record_spec.rb +419 -1
  55. data/spec/parameters/typed_spec.rb +42 -0
  56. data/spec/params_spec.rb +41 -0
  57. data/spec/server/integration_spec.rb +90 -0
  58. data/spec/validator_chain_spec.rb +39 -0
  59. data/spec/validators/confirmation_spec.rb +14 -0
  60. data/spec/validators/format_spec.rb +7 -0
  61. data/spec/validators/length_spec.rb +6 -0
  62. data/spec/validators/numericality_spec.rb +7 -0
  63. data/spec/validators/presence_spec.rb +2 -0
  64. data/test_support/client_test_api.rb +28 -0
  65. metadata +8 -4
  66. data/shell.nix +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec45b62af7fa8653e5f1a304b57746a9a2858db8b75c6a1d8a3deb5536cc7946
4
- data.tar.gz: 0a991e394ccf4a4629f8d755fcfdfa9fd76af6fe37c696d361c1d9340ea8763c
3
+ metadata.gz: 57ff88678f46548f5a2717e7aae799df778723b95b0384471e873b17b0791669
4
+ data.tar.gz: 2062ffb0fef65373b74d7ecef1eae40c0cf16433eef60d3d28c52c3b8b4c7e9e
5
5
  SHA512:
6
- metadata.gz: 693eb912e8215aa93c1c7b009475d91a752bfee1a2d4faeb33d59728168def03128dc7738c264e52f6cef3a87b7e7e11362a4da2f227deb057a9a89e50813705
7
- data.tar.gz: e11fd922568a2ed631a702ec71e8e4631759bee27ecd122029e50af3fb389cde4f7167a0c26876b79b1db220d434b4c3117cb338e1a1fb3f529d85675fa91f1d
6
+ metadata.gz: 5c9d3bc013ac270b3de0b411838a69ac20c920eef8d11cf7552da3e9a852328ab493a5642fc3d4490e7d126d8114f6eda38269b06da2ad3f32a711fa673cbdb3
7
+ data.tar.gz: 3deaf826dbc931825edd8babd1b453faf9521015bf07450d24e2c290bd37cbd0f47cf2dbde90e2245cf5cefa4c673723c97283c1c836f0826cd517117ad44500
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ group :test do
7
7
  gem 'rack-test'
8
8
  gem 'rackup'
9
9
  gem 'rspec'
10
- gem 'rubocop', require: false
10
+ gem 'rubocop', '~> 1.85.0', require: false
11
11
  gem 'rubocop-rspec', require: false
12
12
  gem 'webrick'
13
13
 
data/haveapi.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
16
16
 
17
17
  s.add_dependency 'activesupport', '>= 7.1'
18
- s.add_dependency 'haveapi-client', '~> 0.27.3'
18
+ s.add_dependency 'haveapi-client', '~> 0.28.1'
19
19
  s.add_dependency 'json'
20
20
  s.add_dependency 'mail'
21
21
  s.add_dependency 'nesty', '~> 1.0'
@@ -4,6 +4,8 @@ require 'haveapi/metadata'
4
4
 
5
5
  module HaveAPI
6
6
  class Action < Common
7
+ PATH_PARAM_PATTERN = /\{([a-zA-Z0-9\-_]+)\}/
8
+
7
9
  obj_type :action
8
10
  has_attr :version
9
11
  has_attr :desc
@@ -206,7 +208,7 @@ module HaveAPI
206
208
  end
207
209
 
208
210
  def describe(context)
209
- authorization = (@authorization && @authorization.clone) || Authorization.new
211
+ authorization = (@authorization && @authorization.clone) || Authorization.new {}
210
212
  add_pre_authorize_blocks(authorization, context)
211
213
 
212
214
  if (context.endpoint || context.current_user) \
@@ -276,6 +278,23 @@ module HaveAPI
276
278
  end
277
279
  end
278
280
 
281
+ def path_param_names(path)
282
+ path.scan(PATH_PARAM_PATTERN).map(&:first)
283
+ end
284
+
285
+ def path_params(path, args)
286
+ values = args.is_a?(Array) ? args.dup : [args]
287
+ params = {}
288
+
289
+ path_param_names(path).each do |name|
290
+ value = values.shift.to_s
291
+ params[name] = value
292
+ params[name.to_sym] = value
293
+ end
294
+
295
+ params
296
+ end
297
+
279
298
  def add_pre_authorize_blocks(authorization, context)
280
299
  ret = Action.call_hooks(
281
300
  :pre_authorize,
@@ -293,8 +312,10 @@ module HaveAPI
293
312
  super()
294
313
  @request = request
295
314
  @version = version
315
+ @route_params = params.dup
316
+ @body = body || {}
296
317
  @params = params
297
- @params.update(body) if body
318
+ @params.update(@body)
298
319
  @context = context
299
320
  @context.action = self.class
300
321
  @context.action_instance = self
@@ -316,7 +337,7 @@ module HaveAPI
316
337
  def validate!
317
338
  @params = validate
318
339
  rescue ValidationError => e
319
- error!(e.message, e.to_hash)
340
+ error!(e.message, e.to_hash, http_status: 400)
320
341
  end
321
342
 
322
343
  def authorized?(user)
@@ -329,7 +350,10 @@ module HaveAPI
329
350
  end
330
351
 
331
352
  def input
332
- @safe_params[self.class.input.namespace] if self.class.input
353
+ return unless self.class.input
354
+
355
+ ns = self.class.input.namespace
356
+ ns ? @safe_params[ns] : @safe_params
333
357
  end
334
358
 
335
359
  def meta
@@ -420,7 +444,7 @@ module HaveAPI
420
444
  out,
421
445
  true
422
446
  )
423
- @reply_meta[:global].update(out.meta)
447
+ @reply_meta[:global].update(filtered_object_meta(out.meta, safe_ret))
424
448
 
425
449
  when :object_list
426
450
  safe_ret = []
@@ -433,7 +457,11 @@ module HaveAPI
433
457
  out,
434
458
  true
435
459
  )
436
- safe_ret.last.update({ Metadata.namespace => out.meta }) unless meta[:no]
460
+ next if meta[:no]
461
+
462
+ safe_ret.last.update({
463
+ Metadata.namespace => filtered_object_meta(out.meta, safe_ret.last)
464
+ })
437
465
  end
438
466
 
439
467
  when :hash
@@ -444,8 +472,7 @@ module HaveAPI
444
472
  )
445
473
 
446
474
  when :hash_list
447
- safe_ret = ret
448
- safe_ret.map! do |hash|
475
+ safe_ret = ret.map do |hash|
449
476
  @authorization.filter_output(
450
477
  out_params,
451
478
  adapter.output(@context, hash),
@@ -462,7 +489,7 @@ module HaveAPI
462
489
  end
463
490
 
464
491
  ns = { output.namespace => safe_ret }
465
- ns[Metadata.namespace] = @reply_meta[:global] unless meta[:no]
492
+ ns[Metadata.namespace] = filtered_global_meta unless meta[:no]
466
493
 
467
494
  [true, ns]
468
495
 
@@ -568,74 +595,138 @@ module HaveAPI
568
595
 
569
596
  def validate
570
597
  # Validate standard input
571
- @safe_params = @params.dup
598
+ @safe_params = @route_params.dup
572
599
  input = self.class.input
573
600
 
574
601
  if input
602
+ raw_params = @route_params.merge(@body)
603
+
575
604
  # First check layout
576
- input.check_layout(@safe_params)
605
+ input.check_layout(raw_params)
577
606
 
578
607
  # Then filter allowed params
579
608
  case input.layout
580
609
  when :object_list, :hash_list
581
- @safe_params[input.namespace].map! do |obj|
610
+ filtered = raw_params[input.namespace].map do |obj|
582
611
  @authorization.filter_input(
583
612
  self.class.input.params,
584
613
  self.class.model_adapter(self.class.input.layout).input(obj)
585
614
  )
586
615
  end
616
+ if input.namespace
617
+ @safe_params[input.namespace] = filtered
618
+ else
619
+ @safe_params = filtered
620
+ end
587
621
 
588
622
  else
589
- @safe_params[input.namespace] = @authorization.filter_input(
623
+ filtered = @authorization.filter_input(
590
624
  self.class.input.params,
591
- self.class.model_adapter(self.class.input.layout).input(@safe_params[input.namespace])
625
+ self.class.model_adapter(self.class.input.layout).input(
626
+ input.namespace ? raw_params[input.namespace] : raw_params
627
+ )
592
628
  )
629
+ if input.namespace
630
+ @safe_params[input.namespace] = filtered
631
+ else
632
+ self.class.input.params.each do |p|
633
+ @safe_params.delete(p.name)
634
+ @safe_params.delete(p.name.to_s)
635
+ end
636
+ @safe_params.update(filtered)
637
+ end
593
638
  end
594
639
 
595
640
  # Now check required params, convert types and set defaults
596
- input.validate(@safe_params)
641
+ input.validate(
642
+ @safe_params,
643
+ context: @context,
644
+ only: @authorization.permitted_input_names(self.class.input.params)
645
+ )
597
646
  end
598
647
 
599
- # Validate metadata input
600
- auth = Authorization.new { allow }
648
+ validate_metadata(input)
649
+ end
650
+
651
+ def validate_metadata(input)
601
652
  @metadata = {}
602
653
 
603
- return if input && %i[object_list hash_list].include?(input.layout)
654
+ validate_metadata_type(:global, @authorization)
655
+ validate_metadata_type(:object, @authorization, input) if input && !%i[object_list hash_list].include?(input.layout)
656
+ end
604
657
 
605
- %i[object global].each do |v|
606
- meta = self.class.meta(v)
607
- next unless meta
658
+ def validate_metadata_type(type, auth, input = nil)
659
+ meta = self.class.meta(type)
660
+ return unless meta && meta.input
608
661
 
609
- raw_meta = nil
662
+ params = metadata_params(type, input)
663
+ params = {} if params.nil?
664
+ meta.input.check_layout(params)
610
665
 
611
- [Metadata.namespace, Metadata.namespace.to_s].each do |ns|
612
- params = v == :object ? (@params[input.namespace] && @params[input.namespace][ns]) : @params[ns]
613
- next unless params
666
+ raw_meta = auth.filter_input(
667
+ meta.input.params,
668
+ self.class.model_adapter(meta.input.layout).input(params)
669
+ )
614
670
 
615
- raw_meta = auth.filter_input(
616
- meta.input.params,
617
- self.class.model_adapter(meta.input.layout).input(params)
618
- )
671
+ @metadata.update(
672
+ meta.input.validate(
673
+ raw_meta,
674
+ context: @context,
675
+ only: auth.permitted_input_names(meta.input.params)
676
+ )
677
+ )
678
+ end
619
679
 
620
- break if raw_meta
621
- end
680
+ def metadata_params(type, input)
681
+ case type
682
+ when :global
683
+ fetch_metadata_from(@params)
684
+ when :object
685
+ return unless input && input.namespace
622
686
 
623
- next unless raw_meta
687
+ obj_params = @params[input.namespace]
688
+ fetch_metadata_from(obj_params) if obj_params.is_a?(Hash)
689
+ end
690
+ end
624
691
 
625
- @metadata.update(meta.input.validate(raw_meta))
692
+ def fetch_metadata_from(params)
693
+ [Metadata.namespace, Metadata.namespace.to_s].each do |ns|
694
+ return params[ns] if params && params.has_key?(ns)
626
695
  end
696
+
697
+ nil
627
698
  end
628
699
 
629
700
  # @return <Hash<Symbol, String>> path parameters and their values
630
701
  def extract_path_params
631
702
  ret = {}
632
703
 
633
- @context.path.scan(/\{([a-zA-Z\-_]+)\}/) do |match|
634
- path_param = match.first
635
- ret[path_param] = @params[path_param]
704
+ self.class.path_param_names(@context.path).each do |path_param|
705
+ ret[path_param] = if @route_params.has_key?(path_param)
706
+ @route_params[path_param]
707
+ else
708
+ @route_params[path_param.to_sym]
709
+ end
636
710
  end
637
711
 
638
712
  ret
639
713
  end
714
+
715
+ def filtered_global_meta
716
+ global_meta = self.class.meta(:global)
717
+ return @reply_meta[:global] unless global_meta && global_meta.output
718
+
719
+ @authorization.filter_output(
720
+ global_meta.output.params,
721
+ self.class.model_adapter(global_meta.output.layout).output(@context, @reply_meta[:global]),
722
+ true
723
+ )
724
+ end
725
+
726
+ def filtered_object_meta(object_meta, safe_object)
727
+ return object_meta if safe_object.has_key?(:id) || safe_object.has_key?('id')
728
+
729
+ object_meta.except(:path_params, 'path_params')
730
+ end
640
731
  end
641
732
  end
@@ -1,11 +1,13 @@
1
1
  module HaveAPI::Actions
2
2
  module Paginable
3
+ MAX_LIMIT = 1_000
4
+
3
5
  def self.included(action)
4
6
  action.input do
5
7
  integer :from_id, label: 'From ID', desc: 'List objects with greater/lesser ID',
6
8
  number: { min: 0 }
7
9
  integer :limit, label: 'Limit', desc: 'Number of objects to retrieve',
8
- number: { min: 0 }
10
+ number: { min: 0, max: MAX_LIMIT }
9
11
  end
10
12
  end
11
13
  end
@@ -32,6 +32,8 @@ module HaveAPI::Authentication
32
32
  end
33
33
 
34
34
  user
35
+ rescue ArgumentError, EncodingError
36
+ nil
35
37
  end
36
38
 
37
39
  def describe
@@ -12,13 +12,13 @@ module HaveAPI::Authentication
12
12
  versions.each do |v|
13
13
  @instances[v] ||= []
14
14
 
15
- @chain[v] && @chain[v].each { |p| register_provider(v, p) }
15
+ @chain[v] && @chain[v].each { |p| register_provider(v, p, global: false) }
16
16
  end
17
17
 
18
18
  return unless @chain[:all]
19
19
 
20
20
  @chain[:all].each do |p|
21
- @instances.each_key { |v| register_provider(v, p) }
21
+ @instances.each_key { |v| register_provider(v, p, global: true) }
22
22
  end
23
23
 
24
24
  # @chain.each do |p|
@@ -61,7 +61,10 @@ module HaveAPI::Authentication
61
61
  ret[provider.name][:resources] = {}
62
62
 
63
63
  @server.routes[context.version][:authentication][provider.name][:resources].each do |r, children|
64
- ret[provider.name][:resources][r.resource_name.underscore.to_sym] = r.describe(children, context)
64
+ desc = r.describe(children, context)
65
+ next if desc[:actions].empty? && desc[:resources].empty?
66
+
67
+ ret[provider.name][:resources][r.resource_name.underscore.to_sym] = desc
65
68
  end
66
69
  end
67
70
 
@@ -91,16 +94,16 @@ module HaveAPI::Authentication
91
94
  end
92
95
 
93
96
  def empty?
94
- @chain.empty?
97
+ @chain.empty? || @chain.values.all?(&:empty?)
95
98
  end
96
99
 
97
100
  protected
98
101
 
99
- def register_provider(v, p)
102
+ def register_provider(v, p, global:)
100
103
  instance = p.new(@server, v)
101
104
  @instances[v] << instance
102
105
 
103
- @server.add_auth_routes(v, instance, prefix: instance.name.to_s)
106
+ @server.add_auth_routes(v, instance, prefix: instance.name.to_s, global:)
104
107
 
105
108
  resource_module = instance.resource_module
106
109
  return if resource_module.nil?
@@ -109,7 +112,8 @@ module HaveAPI::Authentication
109
112
  v,
110
113
  instance.name,
111
114
  resource_module,
112
- prefix: instance.name.to_s
115
+ prefix: instance.name.to_s,
116
+ global:
113
117
  )
114
118
  end
115
119
  end
@@ -88,7 +88,8 @@ module HaveAPI::Authentication
88
88
  # @param token [String]
89
89
  # @param token_type_hint [nil, 'access_token', 'refresh_token']
90
90
  # @return [:revoked, :unsupported]
91
- def handle_post_revoke(sinatra_request, token, token_type_hint: nil); end
91
+ # @param client [Client, nil] authenticated revoking client
92
+ def handle_post_revoke(sinatra_request, token, token_type_hint: nil, client: nil); end
92
93
 
93
94
  # Find client by ID
94
95
  # @param client_id [String]
@@ -156,15 +157,36 @@ module HaveAPI::Authentication
156
157
  state: req.state
157
158
  }
158
159
 
159
- if req.code_challenge.present? && req.code_challenge_method.present?
160
+ if req.code_challenge.present?
161
+ scalar_oauth2_param!(req.code_challenge, 'code_challenge')
162
+ code_challenge_method = if req.code_challenge_method.present?
163
+ scalar_oauth2_param!(
164
+ req.code_challenge_method,
165
+ 'code_challenge_method'
166
+ )
167
+ else
168
+ 'plain'
169
+ end
170
+
160
171
  ret.update(
161
172
  code_challenge: req.code_challenge,
162
- code_challenge_method: req.code_challenge_method
173
+ code_challenge_method:
163
174
  )
164
175
  end
165
176
 
166
177
  ret
167
178
  end
179
+
180
+ private
181
+
182
+ def scalar_oauth2_param!(value, name)
183
+ return value if value.is_a?(String)
184
+
185
+ raise Rack::OAuth2::Server::Abstract::BadRequest.new(
186
+ :invalid_request,
187
+ "\"#{name}\" must be a string."
188
+ )
189
+ end
168
190
  end
169
191
  end
170
192
  end
@@ -96,19 +96,19 @@ module HaveAPI::Authentication
96
96
  that = self
97
97
 
98
98
  sinatra.get @authorize_path do
99
- that.authorization_endpoint(self).call(request.env)
99
+ that.call_authorization_endpoint(self)
100
100
  end
101
101
 
102
102
  sinatra.post @authorize_path do
103
- that.authorization_endpoint(self).call(request.env)
103
+ that.call_authorization_endpoint(self)
104
104
  end
105
105
 
106
106
  sinatra.post @token_path do
107
- that.token_endpoint(self).call(request.env)
107
+ that.call_token_endpoint(self)
108
108
  end
109
109
 
110
110
  sinatra.post @revoke_path do
111
- that.revoke_endpoint(self).call(request.env)
111
+ that.call_revoke_endpoint(self)
112
112
  end
113
113
  end
114
114
 
@@ -131,6 +131,8 @@ module HaveAPI::Authentication
131
131
  end
132
132
 
133
133
  token && config.find_user_by_access_token(request, token)
134
+ rescue HaveAPI::AuthenticationError
135
+ nil
134
136
  end
135
137
 
136
138
  def token_from_authorization_header(request)
@@ -139,6 +141,8 @@ module HaveAPI::Authentication
139
141
  return unless auth_header.provided? && !auth_header.parts.first.nil? && auth_header.scheme.to_s == 'bearer'
140
142
 
141
143
  auth_header.params
144
+ rescue ArgumentError, EncodingError
145
+ nil
142
146
  end
143
147
 
144
148
  def token_from_haveapi_header(request)
@@ -249,10 +253,15 @@ module HaveAPI::Authentication
249
253
  req.invalid_grant!
250
254
  end
251
255
 
252
- if authorization.code_challenge && authorization.code_challenge_method
256
+ if authorization.code_challenge
257
+ req.invalid_grant! unless authorization.code_challenge.is_a?(String)
258
+
259
+ code_challenge_method = authorization.code_challenge_method || 'plain'
260
+ req.invalid_grant! unless code_challenge_method.is_a?(String)
261
+
253
262
  req.verify_code_verifier!(
254
263
  authorization.code_challenge,
255
- authorization.code_challenge_method.to_sym
264
+ code_challenge_method.to_sym
256
265
  )
257
266
  end
258
267
 
@@ -289,10 +298,16 @@ module HaveAPI::Authentication
289
298
 
290
299
  def revoke_endpoint(handler)
291
300
  RevokeEndpoint.new do |req, _res|
301
+ req.invalid_client! if req.client_id.nil? || req.client_id.empty?
302
+ req.invalid_client! if req.client_secret.nil? || req.client_secret.empty?
303
+
304
+ client = config.find_client_by_id(req.client_id)
305
+ req.invalid_client! if client.nil? || !client.check_secret(req.client_secret)
306
+
292
307
  ret = config.handle_post_revoke(
293
308
  handler.request,
294
309
  req.token,
295
- token_type_hint: req.token_type_hint
310
+ **revoke_kwargs(req, client)
296
311
  )
297
312
 
298
313
  case ret
@@ -306,17 +321,83 @@ module HaveAPI::Authentication
306
321
  end
307
322
  end
308
323
 
324
+ def call_authorization_endpoint(handler)
325
+ return invalid_request unless scalar_params?(
326
+ handler.params,
327
+ %w[client_id response_type redirect_uri state scope response_mode
328
+ code_challenge code_challenge_method]
329
+ )
330
+
331
+ finish_oauth_errors do
332
+ authorization_endpoint(handler).call(handler.request.env)
333
+ end
334
+ end
335
+
336
+ def call_token_endpoint(handler)
337
+ return invalid_request unless scalar_params?(
338
+ handler.params,
339
+ %w[grant_type client_id client_secret code redirect_uri refresh_token
340
+ code_verifier client_assertion client_assertion_type]
341
+ )
342
+
343
+ finish_oauth_errors do
344
+ token_endpoint(handler).call(handler.request.env)
345
+ end
346
+ end
347
+
348
+ def call_revoke_endpoint(handler)
349
+ return invalid_request unless scalar_params?(
350
+ handler.params,
351
+ %w[token token_type_hint client_id client_secret]
352
+ )
353
+
354
+ finish_oauth_errors do
355
+ revoke_endpoint(handler).call(handler.request.env)
356
+ end
357
+ end
358
+
309
359
  private
310
360
 
361
+ def scalar_params?(params, names)
362
+ names.all? do |name|
363
+ value = params[name]
364
+ value.nil? || value.is_a?(String)
365
+ end
366
+ end
367
+
368
+ def finish_oauth_errors
369
+ yield
370
+ rescue Rack::OAuth2::Server::Abstract::Error => e
371
+ begin
372
+ e.finish
373
+ rescue Rack::OAuth2::Server::Abstract::Error
374
+ Rack::OAuth2::Server::Abstract::BadRequest.new(e.error, e.description).finish
375
+ end
376
+ rescue ArgumentError, EncodingError
377
+ invalid_request
378
+ end
379
+
380
+ def invalid_request
381
+ Rack::OAuth2::Server::Abstract::BadRequest.new(:invalid_request).finish
382
+ end
383
+
384
+ def revoke_kwargs(req, client)
385
+ kwargs = { token_type_hint: req.token_type_hint }
386
+ params = config.method(:handle_post_revoke).parameters
387
+
388
+ if params.any? { |type, name| type == :keyrest || (type == :key && name == :client) }
389
+ kwargs[:client] = client
390
+ end
391
+
392
+ kwargs
393
+ end
394
+
311
395
  def header_to_env(header)
312
396
  "HTTP_#{header.upcase.gsub('-', '_')}"
313
397
  end
314
398
 
315
399
  def token_present?(value)
316
- return false if value.nil?
317
- return false if value.respond_to?(:empty?) && value.empty?
318
-
319
- true
400
+ value.is_a?(String) && !value.empty?
320
401
  end
321
402
  end
322
403
  end
@@ -1,22 +1,55 @@
1
1
  require 'rack/oauth2'
2
+ require 'rack/auth/basic'
2
3
 
3
4
  module HaveAPI::Authentication
4
5
  module OAuth2
5
6
  class RevokeEndpoint < Rack::OAuth2::Server::Abstract::Handler
6
7
  def _call(env)
7
- @request = Request.new(env)
8
+ @request = Request.new(env)
9
+ @request.attr_missing!
8
10
  @response = Response.new(request)
9
- super
11
+ super.finish
12
+ rescue Rack::OAuth2::Server::Abstract::Error => e
13
+ e.finish
10
14
  end
11
15
 
12
16
  class Request < Rack::OAuth2::Server::Abstract::Request
13
17
  attr_required :token
14
- attr_optional :token_type_hint
18
+ attr_optional :client_id, :client_secret, :token_type_hint
15
19
 
16
20
  def initialize(env)
21
+ auth = Rack::Auth::Basic::Request.new(env)
22
+
23
+ if auth.provided? && auth.basic?
24
+ @client_id, @client_secret = auth.credentials.map do |credential|
25
+ Rack::OAuth2::Util.www_form_url_decode(credential)
26
+ end
27
+ end
28
+
17
29
  super
30
+ @client_secret ||= params['client_secret']
18
31
  @token = params['token']
19
32
  @token_type_hint = params['token_type_hint']
33
+
34
+ invalid_request! unless scalar_request_params?
35
+ rescue ArgumentError, EncodingError
36
+ invalid_request!
37
+ end
38
+
39
+ def invalid_client!(description = nil, options = {})
40
+ raise Rack::OAuth2::Server::Token::Unauthorized.new(
41
+ :invalid_client,
42
+ description,
43
+ options
44
+ )
45
+ end
46
+
47
+ def invalid_request!(description = nil, options = {})
48
+ raise Rack::OAuth2::Server::Abstract::BadRequest.new(
49
+ :invalid_request,
50
+ description,
51
+ options
52
+ )
20
53
  end
21
54
 
22
55
  def unsupported_token_type!(description = nil, options = {})
@@ -26,6 +59,14 @@ module HaveAPI::Authentication
26
59
  options
27
60
  )
28
61
  end
62
+
63
+ private
64
+
65
+ def scalar_request_params?
66
+ [client_id, client_secret, token, token_type_hint].all? do |value|
67
+ value.nil? || value.is_a?(String)
68
+ end
69
+ end
29
70
  end
30
71
 
31
72
  class Response < Rack::OAuth2::Server::Abstract::Response