haveapi 0.27.3 → 0.28.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.
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 +125 -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 +426 -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 +406 -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: 4dc49769be220f91aa34770fa280e1507a3eef56ad97f1cdda575a4d42f42ad0
4
+ data.tar.gz: '09177e0cf827dbed3e8a31290c2f0088a7e7857bab9c4cbd2d3953a55a84747e'
5
5
  SHA512:
6
- metadata.gz: 693eb912e8215aa93c1c7b009475d91a752bfee1a2d4faeb33d59728168def03128dc7738c264e52f6cef3a87b7e7e11362a4da2f227deb057a9a89e50813705
7
- data.tar.gz: e11fd922568a2ed631a702ec71e8e4631759bee27ecd122029e50af3fb389cde4f7167a0c26876b79b1db220d434b4c3117cb338e1a1fb3f529d85675fa91f1d
6
+ metadata.gz: 0bbac0d82fcfcba37da6874b54b667036cac326aead8da3e573ce1fac77a986fe191d3e6fb1c1754d65cb0fc5f7fb4d7c7d1d7f5fe329e0135a019fb00100561
7
+ data.tar.gz: d031c935fffbb7ccd5f4262598fae87e93b8055d393576336d4afd5f4b14a3e19c77d024dcfede042d1e37ac72f8a5d79fc84e96dd94f2a5e0e74ca9984840d9
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.0'
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,21 @@ 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
+ params[name] = values.shift.to_s
291
+ end
292
+
293
+ params
294
+ end
295
+
279
296
  def add_pre_authorize_blocks(authorization, context)
280
297
  ret = Action.call_hooks(
281
298
  :pre_authorize,
@@ -293,8 +310,10 @@ module HaveAPI
293
310
  super()
294
311
  @request = request
295
312
  @version = version
313
+ @route_params = params.dup
314
+ @body = body || {}
296
315
  @params = params
297
- @params.update(body) if body
316
+ @params.update(@body)
298
317
  @context = context
299
318
  @context.action = self.class
300
319
  @context.action_instance = self
@@ -316,7 +335,7 @@ module HaveAPI
316
335
  def validate!
317
336
  @params = validate
318
337
  rescue ValidationError => e
319
- error!(e.message, e.to_hash)
338
+ error!(e.message, e.to_hash, http_status: 400)
320
339
  end
321
340
 
322
341
  def authorized?(user)
@@ -329,7 +348,10 @@ module HaveAPI
329
348
  end
330
349
 
331
350
  def input
332
- @safe_params[self.class.input.namespace] if self.class.input
351
+ return unless self.class.input
352
+
353
+ ns = self.class.input.namespace
354
+ ns ? @safe_params[ns] : @safe_params
333
355
  end
334
356
 
335
357
  def meta
@@ -420,7 +442,7 @@ module HaveAPI
420
442
  out,
421
443
  true
422
444
  )
423
- @reply_meta[:global].update(out.meta)
445
+ @reply_meta[:global].update(filtered_object_meta(out.meta, safe_ret))
424
446
 
425
447
  when :object_list
426
448
  safe_ret = []
@@ -433,7 +455,11 @@ module HaveAPI
433
455
  out,
434
456
  true
435
457
  )
436
- safe_ret.last.update({ Metadata.namespace => out.meta }) unless meta[:no]
458
+ next if meta[:no]
459
+
460
+ safe_ret.last.update({
461
+ Metadata.namespace => filtered_object_meta(out.meta, safe_ret.last)
462
+ })
437
463
  end
438
464
 
439
465
  when :hash
@@ -444,8 +470,7 @@ module HaveAPI
444
470
  )
445
471
 
446
472
  when :hash_list
447
- safe_ret = ret
448
- safe_ret.map! do |hash|
473
+ safe_ret = ret.map do |hash|
449
474
  @authorization.filter_output(
450
475
  out_params,
451
476
  adapter.output(@context, hash),
@@ -462,7 +487,7 @@ module HaveAPI
462
487
  end
463
488
 
464
489
  ns = { output.namespace => safe_ret }
465
- ns[Metadata.namespace] = @reply_meta[:global] unless meta[:no]
490
+ ns[Metadata.namespace] = filtered_global_meta unless meta[:no]
466
491
 
467
492
  [true, ns]
468
493
 
@@ -568,74 +593,138 @@ module HaveAPI
568
593
 
569
594
  def validate
570
595
  # Validate standard input
571
- @safe_params = @params.dup
596
+ @safe_params = @route_params.dup
572
597
  input = self.class.input
573
598
 
574
599
  if input
600
+ raw_params = @route_params.merge(@body)
601
+
575
602
  # First check layout
576
- input.check_layout(@safe_params)
603
+ input.check_layout(raw_params)
577
604
 
578
605
  # Then filter allowed params
579
606
  case input.layout
580
607
  when :object_list, :hash_list
581
- @safe_params[input.namespace].map! do |obj|
608
+ filtered = raw_params[input.namespace].map do |obj|
582
609
  @authorization.filter_input(
583
610
  self.class.input.params,
584
611
  self.class.model_adapter(self.class.input.layout).input(obj)
585
612
  )
586
613
  end
614
+ if input.namespace
615
+ @safe_params[input.namespace] = filtered
616
+ else
617
+ @safe_params = filtered
618
+ end
587
619
 
588
620
  else
589
- @safe_params[input.namespace] = @authorization.filter_input(
621
+ filtered = @authorization.filter_input(
590
622
  self.class.input.params,
591
- self.class.model_adapter(self.class.input.layout).input(@safe_params[input.namespace])
623
+ self.class.model_adapter(self.class.input.layout).input(
624
+ input.namespace ? raw_params[input.namespace] : raw_params
625
+ )
592
626
  )
627
+ if input.namespace
628
+ @safe_params[input.namespace] = filtered
629
+ else
630
+ self.class.input.params.each do |p|
631
+ @safe_params.delete(p.name)
632
+ @safe_params.delete(p.name.to_s)
633
+ end
634
+ @safe_params.update(filtered)
635
+ end
593
636
  end
594
637
 
595
638
  # Now check required params, convert types and set defaults
596
- input.validate(@safe_params)
639
+ input.validate(
640
+ @safe_params,
641
+ context: @context,
642
+ only: @authorization.permitted_input_names(self.class.input.params)
643
+ )
597
644
  end
598
645
 
599
- # Validate metadata input
600
- auth = Authorization.new { allow }
646
+ validate_metadata(input)
647
+ end
648
+
649
+ def validate_metadata(input)
601
650
  @metadata = {}
602
651
 
603
- return if input && %i[object_list hash_list].include?(input.layout)
652
+ validate_metadata_type(:global, @authorization)
653
+ validate_metadata_type(:object, @authorization, input) if input && !%i[object_list hash_list].include?(input.layout)
654
+ end
604
655
 
605
- %i[object global].each do |v|
606
- meta = self.class.meta(v)
607
- next unless meta
656
+ def validate_metadata_type(type, auth, input = nil)
657
+ meta = self.class.meta(type)
658
+ return unless meta && meta.input
608
659
 
609
- raw_meta = nil
660
+ params = metadata_params(type, input)
661
+ params = {} if params.nil?
662
+ meta.input.check_layout(params)
610
663
 
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
664
+ raw_meta = auth.filter_input(
665
+ meta.input.params,
666
+ self.class.model_adapter(meta.input.layout).input(params)
667
+ )
614
668
 
615
- raw_meta = auth.filter_input(
616
- meta.input.params,
617
- self.class.model_adapter(meta.input.layout).input(params)
618
- )
669
+ @metadata.update(
670
+ meta.input.validate(
671
+ raw_meta,
672
+ context: @context,
673
+ only: auth.permitted_input_names(meta.input.params)
674
+ )
675
+ )
676
+ end
619
677
 
620
- break if raw_meta
621
- end
678
+ def metadata_params(type, input)
679
+ case type
680
+ when :global
681
+ fetch_metadata_from(@params)
682
+ when :object
683
+ return unless input && input.namespace
622
684
 
623
- next unless raw_meta
685
+ obj_params = @params[input.namespace]
686
+ fetch_metadata_from(obj_params) if obj_params.is_a?(Hash)
687
+ end
688
+ end
624
689
 
625
- @metadata.update(meta.input.validate(raw_meta))
690
+ def fetch_metadata_from(params)
691
+ [Metadata.namespace, Metadata.namespace.to_s].each do |ns|
692
+ return params[ns] if params && params.has_key?(ns)
626
693
  end
694
+
695
+ nil
627
696
  end
628
697
 
629
698
  # @return <Hash<Symbol, String>> path parameters and their values
630
699
  def extract_path_params
631
700
  ret = {}
632
701
 
633
- @context.path.scan(/\{([a-zA-Z\-_]+)\}/) do |match|
634
- path_param = match.first
635
- ret[path_param] = @params[path_param]
702
+ self.class.path_param_names(@context.path).each do |path_param|
703
+ ret[path_param] = if @route_params.has_key?(path_param)
704
+ @route_params[path_param]
705
+ else
706
+ @route_params[path_param.to_sym]
707
+ end
636
708
  end
637
709
 
638
710
  ret
639
711
  end
712
+
713
+ def filtered_global_meta
714
+ global_meta = self.class.meta(:global)
715
+ return @reply_meta[:global] unless global_meta && global_meta.output
716
+
717
+ @authorization.filter_output(
718
+ global_meta.output.params,
719
+ self.class.model_adapter(global_meta.output.layout).output(@context, @reply_meta[:global]),
720
+ true
721
+ )
722
+ end
723
+
724
+ def filtered_object_meta(object_meta, safe_object)
725
+ return object_meta if safe_object.has_key?(:id) || safe_object.has_key?('id')
726
+
727
+ object_meta.except(:path_params, 'path_params')
728
+ end
640
729
  end
641
730
  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