haveapi 0.18.2 → 0.19.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/haveapi.gemspec +2 -1
  3. data/lib/haveapi/action.rb +72 -31
  4. data/lib/haveapi/authentication/base.rb +1 -1
  5. data/lib/haveapi/authentication/basic/provider.rb +2 -2
  6. data/lib/haveapi/authentication/chain.rb +4 -4
  7. data/lib/haveapi/authentication/oauth2/config.rb +52 -14
  8. data/lib/haveapi/authentication/oauth2/provider.rb +98 -17
  9. data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +36 -0
  10. data/lib/haveapi/authentication/token/config.rb +1 -0
  11. data/lib/haveapi/authorization.rb +19 -12
  12. data/lib/haveapi/client_examples/js_client.rb +11 -1
  13. data/lib/haveapi/client_examples/php_client.rb +43 -1
  14. data/lib/haveapi/context.rb +21 -2
  15. data/lib/haveapi/example.rb +9 -9
  16. data/lib/haveapi/hooks.rb +23 -23
  17. data/lib/haveapi/metadata.rb +1 -1
  18. data/lib/haveapi/model_adapter.rb +14 -14
  19. data/lib/haveapi/model_adapters/active_record.rb +20 -20
  20. data/lib/haveapi/output_formatter.rb +4 -4
  21. data/lib/haveapi/output_formatters/base.rb +1 -1
  22. data/lib/haveapi/parameters/resource.rb +22 -22
  23. data/lib/haveapi/parameters/typed.rb +7 -7
  24. data/lib/haveapi/params.rb +24 -22
  25. data/lib/haveapi/resource.rb +9 -3
  26. data/lib/haveapi/resources/action_state.rb +16 -16
  27. data/lib/haveapi/route.rb +3 -2
  28. data/lib/haveapi/server.rb +113 -98
  29. data/lib/haveapi/spec/mock_action.rb +7 -7
  30. data/lib/haveapi/spec/spec_methods.rb +8 -8
  31. data/lib/haveapi/tasks/yard.rb +2 -2
  32. data/lib/haveapi/validator.rb +13 -13
  33. data/lib/haveapi/validator_chain.rb +6 -6
  34. data/lib/haveapi/validators/acceptance.rb +2 -2
  35. data/lib/haveapi/validators/confirmation.rb +4 -4
  36. data/lib/haveapi/validators/exclusion.rb +4 -4
  37. data/lib/haveapi/validators/format.rb +4 -4
  38. data/lib/haveapi/validators/inclusion.rb +3 -3
  39. data/lib/haveapi/validators/length.rb +1 -1
  40. data/lib/haveapi/validators/numericality.rb +3 -3
  41. data/lib/haveapi/validators/presence.rb +2 -2
  42. data/lib/haveapi/version.rb +1 -1
  43. data/lib/haveapi/views/version_page/auth_body.erb +6 -4
  44. data/lib/haveapi/views/version_page/resource_body.erb +2 -0
  45. data/lib/haveapi.rb +1 -0
  46. data/spec/authorization_spec.rb +28 -28
  47. data/spec/envelope_spec.rb +4 -4
  48. data/spec/parameters/typed_spec.rb +3 -3
  49. data/spec/params_spec.rb +2 -2
  50. data/spec/validators/acceptance_spec.rb +2 -2
  51. data/spec/validators/confirmation_spec.rb +4 -4
  52. data/spec/validators/exclusion_spec.rb +2 -2
  53. data/spec/validators/format_spec.rb +5 -5
  54. data/spec/validators/inclusion_spec.rb +8 -8
  55. data/spec/validators/presence_spec.rb +1 -1
  56. metadata +19 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16eef5420692ea73c7b4969ec61c7efc26c74692c7a9a403d8f664fdf2dafbf8
4
- data.tar.gz: 26e5cf981901ca19dc5c0d60c5b567f2cd81e66df2cf70c5333c24b920136554
3
+ metadata.gz: c0c1d6bdc4ae1f2792d4f88b4aa85654e651932ca2633985f293f02f2e97bd56
4
+ data.tar.gz: 7b83f12da3c6bb264833774a4dd3c895b82c59af80072db0ed4fe29821216578
5
5
  SHA512:
6
- metadata.gz: '0876a3b9b10ad452c89a62e06fc56e29cfb6aff188e4943274adf4e4180464b90daa420e4ab0953faba1f1ae9a5df0a8a19ae0362acd7290043efefd050f8b19'
7
- data.tar.gz: cddc8f6c09dea69afc3a359ab18cd947a5a338ec9413d2f16eb600fa4893c88f0cbe3e3224b9ae58ac0d3de5707b96ec0e0891162d3d0ee9c97e906bff0c8e44
6
+ metadata.gz: 1ebadd5b6efb27081c836b00841781f8569465f8c6e6f6396a4a5575b5ebb6e3984dd5a53ecc90e1d045b04a879d4dd454160c515a019fa6c34f0cc40af2b31b
7
+ data.tar.gz: a44e4c95e74ae2b5aa0df51fab3f3197ec3d4828f23947e3df4bcfa9b2ef3a9c7d9216c7ed68d9e8295dcc7e9b9ee6543121faca44e09ccea1f2f107ea7e7feb
data/haveapi.gemspec CHANGED
@@ -18,12 +18,13 @@ Gem::Specification.new do |s|
18
18
  s.add_runtime_dependency 'json'
19
19
  s.add_runtime_dependency 'activesupport', '>= 7.0'
20
20
  s.add_runtime_dependency 'sinatra', '~> 3.1.0'
21
+ s.add_runtime_dependency 'sinatra-contrib', '~> 3.1.0'
21
22
  s.add_runtime_dependency 'tilt', '~> 2.3.0'
22
23
  s.add_runtime_dependency 'redcarpet', '~> 3.6'
23
24
  s.add_runtime_dependency 'rake'
24
25
  s.add_runtime_dependency 'github-markdown'
25
26
  s.add_runtime_dependency 'nesty', '~> 1.0'
26
- s.add_runtime_dependency 'haveapi-client', '~> 0.18.2'
27
+ s.add_runtime_dependency 'haveapi-client', '~> 0.19.1'
27
28
  s.add_runtime_dependency 'mail'
28
29
  s.add_runtime_dependency 'rack-oauth2', '~> 2.2.0'
29
30
  end
@@ -16,6 +16,18 @@ module HaveAPI
16
16
 
17
17
  include Hookable
18
18
 
19
+ has_hook :pre_authorize,
20
+ desc: 'Called to provide additional authorization blocks. These blocks are '+
21
+ 'called before action\'s own authorization block. Note that if any '+
22
+ 'of the blocks uses allow/deny rule, it will be the final authorization '+
23
+ 'decision and even action\'s own authorization block will not be called.',
24
+ args: {
25
+ context: 'HaveAPI::Context instance',
26
+ },
27
+ ret: {
28
+ blocks: 'array of authorization blocks',
29
+ }
30
+
19
31
  has_hook :exec_exception,
20
32
  desc: 'Called when unhandled exceptions occurs during Action.exec',
21
33
  args: {
@@ -207,7 +219,10 @@ module HaveAPI
207
219
  def describe(context)
208
220
  authorization = (@authorization && @authorization.clone) || Authorization.new
209
221
 
210
- return false if (context.endpoint || context.current_user) && !authorization.authorized?(context.current_user)
222
+ if (context.endpoint || context.current_user) \
223
+ && !authorization.authorized?(context.current_user, context.path_params_from_args)
224
+ return false
225
+ end
211
226
 
212
227
  route_method = context.action.http_method.to_s.upcase
213
228
  context.authorization = authorization
@@ -223,17 +238,18 @@ module HaveAPI
223
238
  end
224
239
 
225
240
  {
226
- auth: @auth,
227
- description: @desc,
228
- aliases: @aliases,
229
- blocking: @blocking ? true : false,
230
- input: @input ? @input.describe(context) : {parameters: {}},
231
- output: @output ? @output.describe(context) : {parameters: {}},
232
- meta: @meta ? @meta.merge(@meta) { |_, v| v && v.describe(context) } : nil,
233
- examples: @examples ? @examples.describe(context) : [],
234
- path: context.resolved_path,
235
- method: route_method,
236
- help: "#{context.path}?method=#{route_method}"
241
+ auth: @auth,
242
+ description: @desc,
243
+ aliases: @aliases,
244
+ blocking: @blocking ? true : false,
245
+ input: @input ? @input.describe(context) : {parameters: {}},
246
+ output: @output ? @output.describe(context) : {parameters: {}},
247
+ meta: @meta ? @meta.merge(@meta) { |_, v| v && v.describe(context) } : nil,
248
+ examples: @examples ? @examples.describe(context) : [],
249
+ scope: context.action_scope,
250
+ path: context.resolved_path,
251
+ method: route_method,
252
+ help: "#{context.path}?method=#{route_method}",
237
253
  }
238
254
  end
239
255
 
@@ -291,6 +307,17 @@ module HaveAPI
291
307
  else
292
308
  @authorization = Authorization.new {}
293
309
  end
310
+
311
+ ret = call_class_hooks_as_for(
312
+ Action,
313
+ :pre_authorize,
314
+ args: [@context],
315
+ initial: {blocks: []},
316
+ )
317
+
318
+ ret[:blocks].reverse_each do |block|
319
+ @authorization.prepend_block(block)
320
+ end
294
321
  end
295
322
 
296
323
  def validate!
@@ -303,7 +330,7 @@ module HaveAPI
303
330
 
304
331
  def authorized?(user)
305
332
  @current_user = user
306
- @authorization.authorized?(user)
333
+ @authorization.authorized?(user, extract_path_params)
307
334
  end
308
335
 
309
336
  def current_user
@@ -412,9 +439,9 @@ module HaveAPI
412
439
  when :object
413
440
  out = adapter.output(@context, ret)
414
441
  safe_ret = @authorization.filter_output(
415
- out_params,
416
- out,
417
- true
442
+ out_params,
443
+ out,
444
+ true
418
445
  )
419
446
  @reply_meta[:global].update(out.meta)
420
447
 
@@ -425,27 +452,27 @@ module HaveAPI
425
452
  out = adapter.output(@context, obj)
426
453
 
427
454
  safe_ret << @authorization.filter_output(
428
- out_params,
429
- out,
430
- true
455
+ out_params,
456
+ out,
457
+ true
431
458
  )
432
459
  safe_ret.last.update({Metadata.namespace => out.meta}) unless meta[:no]
433
460
  end
434
461
 
435
462
  when :hash
436
463
  safe_ret = @authorization.filter_output(
437
- out_params,
438
- adapter.output(@context, ret),
439
- true
464
+ out_params,
465
+ adapter.output(@context, ret),
466
+ true
440
467
  )
441
468
 
442
469
  when :hash_list
443
470
  safe_ret = ret
444
471
  safe_ret.map! do |hash|
445
472
  @authorization.filter_output(
446
- out_params,
447
- adapter.output(@context, hash),
448
- true
473
+ out_params,
474
+ adapter.output(@context, hash),
475
+ true
449
476
  )
450
477
  end
451
478
 
@@ -574,14 +601,16 @@ module HaveAPI
574
601
  when :object_list, :hash_list
575
602
  @safe_params[input.namespace].map! do |obj|
576
603
  @authorization.filter_input(
577
- self.class.input.params,
578
- self.class.model_adapter(self.class.input.layout).input(obj))
604
+ self.class.input.params,
605
+ self.class.model_adapter(self.class.input.layout).input(obj)
606
+ )
579
607
  end
580
608
 
581
609
  else
582
610
  @safe_params[input.namespace] = @authorization.filter_input(
583
- self.class.input.params,
584
- self.class.model_adapter(self.class.input.layout).input(@safe_params[input.namespace]))
611
+ self.class.input.params,
612
+ self.class.model_adapter(self.class.input.layout).input(@safe_params[input.namespace])
613
+ )
585
614
  end
586
615
 
587
616
  # Now check required params, convert types and set defaults
@@ -605,8 +634,8 @@ module HaveAPI
605
634
  next unless params
606
635
 
607
636
  raw_meta = auth.filter_input(
608
- meta.input.params,
609
- self.class.model_adapter(meta.input.layout).input(params)
637
+ meta.input.params,
638
+ self.class.model_adapter(meta.input.layout).input(params)
610
639
  )
611
640
 
612
641
  break if raw_meta
@@ -617,5 +646,17 @@ module HaveAPI
617
646
  @metadata.update(meta.input.validate(raw_meta))
618
647
  end
619
648
  end
649
+
650
+ # @return <Hash<Symbol, String>> path parameters and their values
651
+ def extract_path_params
652
+ ret = {}
653
+
654
+ @context.path.scan(/\{([a-zA-Z\-_]+)\}/) do |match|
655
+ path_param = match.first
656
+ ret[path_param] = @params[path_param]
657
+ end
658
+
659
+ ret
660
+ end
620
661
  end
621
662
  end
@@ -39,7 +39,7 @@ module HaveAPI
39
39
  end
40
40
 
41
41
  # Reimplement this method in your authentication provider.
42
- # +request+ is passed directly from Sinatra.
42
+ # `request` is passed directly from Sinatra.
43
43
  def authenticate(request)
44
44
 
45
45
  end
@@ -36,8 +36,8 @@ module HaveAPI::Authentication
36
36
 
37
37
  def describe
38
38
  {
39
- description: "Authentication using HTTP basic. Username and password is passed "+
40
- "via HTTP header. Its use is forbidden from web browsers."
39
+ description: "Authentication using HTTP basic. Username and password is passed "+
40
+ "via HTTP header. Its use is forbidden from web browsers."
41
41
  }
42
42
  end
43
43
 
@@ -31,7 +31,7 @@ module HaveAPI::Authentication
31
31
  # end
32
32
  end
33
33
 
34
- # Iterate through authentication providers registered for version +v+
34
+ # Iterate through authentication providers registered for version `v`
35
35
  # until authentication is successful or the end is reached and user
36
36
  # is not authenticated.
37
37
  # Authentication provider can deny the user access by calling Base#deny.
@@ -68,7 +68,7 @@ module HaveAPI::Authentication
68
68
  ret
69
69
  end
70
70
 
71
- # Return provider list for version +v+.
71
+ # Return provider list for version `v`.
72
72
  # Used for registering providers to specific version, e.g.
73
73
  # api.auth_chain[1] << MyAuthProvider
74
74
  def [](v)
@@ -76,8 +76,8 @@ module HaveAPI::Authentication
76
76
  @chain[v]
77
77
  end
78
78
 
79
- # Register authentication +provider+ for all available API versions.
80
- # +provider+ may also be an Array of providers.
79
+ # Register authentication `provider` for all available API versions.
80
+ # `provider` may also be an Array of providers.
81
81
  def <<(provider)
82
82
  @chain[:all] ||= []
83
83
 
@@ -6,7 +6,7 @@ module HaveAPI::Authentication
6
6
  # The created provider can then be added to authentication chain.
7
7
  #
8
8
  # In general, it is up to the implementation to provide the authentication flow
9
- # -- render HTML page in {#render_authorize_page} and then process it in
9
+ # -- render HTML page in {#handle_get_authorize} and then process it in
10
10
  # {#handle_post_authorize}. The implementation must also handle generation
11
11
  # of all needed tokens, their persistence and validity checking.
12
12
  class Config
@@ -16,36 +16,39 @@ module HaveAPI::Authentication
16
16
  @version = v
17
17
  end
18
18
 
19
- # Render authorization page
19
+ # Handle GET authorize requests
20
20
  #
21
- # This method can be called on both GET and POST requests, e.g. if the user
22
- # provided incorrect credentials or if there are multiple authentication
23
- # steps.
21
+ # This method usually writes HTML to `oauth2_response`, you must also set
22
+ # content type.
24
23
  #
25
- # It should return full HTML page that will be sent to the user. The page
26
- # usually contains a login form.
27
- #
28
- # @param oauth2_request [Rack::OAuth2::Server::Authorize::Request]
24
+ # @param sinatra_handler [Object]
25
+ # @param sinatra_request [Sinatra::Request]
29
26
  # @param sinatra_params [Hash] request params
27
+ # @param oauth2_request [Rack::OAuth2::Server::Authorize::Request]
28
+ # @param oauth2_response [Rack::OAuth2::Server::Authorize::Response]
30
29
  # @param client [Client]
31
- # @param auth_result [AuthResult, nil]
32
- # @return [String] HTML
33
- def render_authorize_page(oauth2_request, sinatra_params, client, auth_result: nil)
30
+ # @return [AuthResult, nil]
31
+ def handle_get_authorize(sinatra_handler:, sinatra_request:, sinatra_params:, oauth2_request:, oauth2_response:, client:)
34
32
 
35
33
  end
36
34
 
37
- # Handle POST requests made from {#render_authorize_page}
35
+ # Handle POST authorize requests
38
36
  #
39
37
  # Process form data and return {AuthResult} or nil. When nil is returned
40
38
  # the authorization process is aborted and the user is redirected back
41
39
  # to the client.
42
40
  #
41
+ # If the authentication is incomplete, this method must also write output
42
+ # to `oauth2_response`, usually HTML. Content type must be set.
43
+ #
44
+ # @param sinatra_handler [Object]
43
45
  # @param sinatra_request [Sinatra::Request]
44
46
  # @param sinatra_params [Hash] request params
45
47
  # @param oauth2_request [Rack::OAuth2::Server::Authorize::Request]
48
+ # @param oauth2_response [Rack::OAuth2::Server::Authorize::Response]
46
49
  # @param client [Client]
47
50
  # @return [AuthResult, nil]
48
- def handle_post_authorize(sinatra_request, sinatra_params, oauth2_request, client)
51
+ def handle_post_authorize(sinatra_handler:, sinatra_request:, sinatra_params:, oauth2_request:, oauth2_response:, client:)
49
52
 
50
53
  end
51
54
 
@@ -86,6 +89,19 @@ module HaveAPI::Authentication
86
89
 
87
90
  end
88
91
 
92
+ # Revoke access or refresh token
93
+ #
94
+ # Note that even if the token is not found, this method should return
95
+ # `:revoked`.
96
+ #
97
+ # @param sinatra_request [Sinatra::Request]
98
+ # @param token [String]
99
+ # @param token_type_hint [nil, 'access_token', 'refresh_token']
100
+ # @return [:revoked, :unsupported]
101
+ def handle_post_revoke(sinatra_request, token, token_type_hint: nil)
102
+
103
+ end
104
+
89
105
  # Find client by ID
90
106
  # @param client_id [String]
91
107
  # @return [Client, nil]
@@ -117,12 +133,34 @@ module HaveAPI::Authentication
117
133
 
118
134
  end
119
135
 
136
+ # Base URL of the authorization server, including protocol
137
+ #
138
+ # This should in general be the same URL at which your API is located.
139
+ # It can be useful if you wish to have a separate domain for authentication.
140
+ #
141
+ # Example: `https://api.domain.tld`
142
+ #
143
+ # @return [String]
144
+ def base_url
145
+ raise NotImplementedError
146
+ end
147
+
120
148
  # Path to the authorization endpoint on this API
121
149
  # @return [String]
122
150
  def authorize_path
123
151
  @provider.authorize_path
124
152
  end
125
153
 
154
+ # Custom HTTP header that is searched for the access token
155
+ #
156
+ # The authorization header is not feasible from web browsers, so we optionally
157
+ # use our own header for the purpose.
158
+ #
159
+ # @return [String]
160
+ def http_header
161
+ 'X-HaveAPI-OAuth2-Token'
162
+ end
163
+
126
164
  # Parameters needed for the authorization process
127
165
  #
128
166
  # Use these in {#render_authorization_page}, put them e.g. in hidden form
@@ -78,9 +78,21 @@ module HaveAPI::Authentication
78
78
  super(server, v)
79
79
  end
80
80
 
81
+ def setup
82
+ @server.allow_header(config.http_header)
83
+ end
84
+
81
85
  def register_routes(sinatra, prefix)
82
86
  @authorize_path = File.join(prefix, 'authorize')
83
87
  @token_path = File.join(prefix, 'token')
88
+ @revoke_path = File.join(prefix, 'revoke')
89
+
90
+ base_url = config.base_url
91
+
92
+ @authorize_url = File.join(base_url, @authorize_path)
93
+ @token_url = File.join(base_url, @token_path)
94
+ @revoke_url = File.join(base_url, @revoke_path)
95
+
84
96
  that = self
85
97
 
86
98
  sinatra.get @authorize_path do
@@ -94,12 +106,17 @@ module HaveAPI::Authentication
94
106
  sinatra.post @token_path do
95
107
  that.token_endpoint(self).call(request.env)
96
108
  end
109
+
110
+ sinatra.post @revoke_path do
111
+ that.revoke_endpoint(self).call(request.env)
112
+ end
97
113
  end
98
114
 
99
115
  def authenticate(request)
100
116
  tokens = [
101
117
  request['access_token'],
102
- token_from_header(request)
118
+ token_from_authorization_header(request),
119
+ token_from_haveapi_header(request),
103
120
  ].compact
104
121
 
105
122
  token =
@@ -115,7 +132,7 @@ module HaveAPI::Authentication
115
132
  token && config.find_user_by_access_token(request, token)
116
133
  end
117
134
 
118
- def token_from_header(request)
135
+ def token_from_authorization_header(request)
119
136
  auth_header = Rack::Auth::AbstractRequest.new(request.env)
120
137
 
121
138
  if auth_header.provided? && !auth_header.parts.first.nil? && auth_header.scheme.to_s == 'bearer'
@@ -125,22 +142,36 @@ module HaveAPI::Authentication
125
142
  end
126
143
  end
127
144
 
145
+ def token_from_haveapi_header(request)
146
+ request.env[header_to_env(config.http_header)]
147
+ end
148
+
128
149
  def describe
129
150
  desc = <<-END
130
- OAuth2 authorization provider. While OAuth2 is not supported by HaveAPI
131
- clients, it is possible to use your API as an authentication source.
151
+ OAuth2 authorization provider. While OAuth2 support in HaveAPI clients
152
+ is limited, it is possible to use your API as an authentication source
153
+ and to use OAuth2 tokens to access the API.
132
154
 
133
155
  HaveAPI partially implements RFC 6749: authorization response type "code"
134
156
  and token grant types "authorization_code" and "refresh_token". Other
135
157
  response and grant types are not supported at this time.
136
158
 
137
- The access token can be passed as bearer token according to RFC 6750.
159
+ The access token can be passed as bearer token according to RFC 6750,
160
+ or using a custom HTTP header when the Authorization header is not
161
+ practical.
162
+
163
+ The access and refresh tokens can be revoked as per RFC 7009.
138
164
  END
139
165
 
140
166
  {
141
167
  description: desc,
168
+ http_header: config.http_header,
169
+ authorize_url: @authorize_url,
142
170
  authorize_path: @authorize_path,
171
+ token_url: @token_url,
143
172
  token_path: @token_path,
173
+ revoke_url: @revoke_url,
174
+ revoke_path: @revoke_path,
144
175
  }
145
176
  end
146
177
 
@@ -152,7 +183,14 @@ module HaveAPI::Authentication
152
183
  res.redirect_uri = req.verify_redirect_uri!(client.redirect_uri)
153
184
 
154
185
  if req.post?
155
- auth_res = config.handle_post_authorize(handler.request, handler.params, req, client)
186
+ auth_res = config.handle_post_authorize(
187
+ sinatra_handler: handler,
188
+ sinatra_request: handler.request,
189
+ sinatra_params: handler.params,
190
+ oauth2_request: req,
191
+ oauth2_response: res,
192
+ client:,
193
+ )
156
194
 
157
195
  if auth_res.nil?
158
196
  # Authentication failed
@@ -170,18 +208,33 @@ module HaveAPI::Authentication
170
208
  end
171
209
 
172
210
  res.approve!
173
- elsif auth_res.authenticated && !auth_res.complete
174
- # Continue with another authentication step
175
- res.content_type = 'text/html'
176
- res.write(config.render_authorize_page(req, handler.params, client, auth_result: auth_res))
177
- else
178
- # Authentication failed, report errors and let the user retry
179
- res.content_type = 'text/html'
180
- res.write(config.render_authorize_page(req, handler.params, client, auth_result: auth_res))
181
211
  end
182
212
  else
183
- res.content_type = 'text/html'
184
- res.write(config.render_authorize_page(req, handler.params, client))
213
+ auth_res = config.handle_get_authorize(
214
+ sinatra_handler: handler,
215
+ sinatra_request: handler.request,
216
+ sinatra_params: handler.params,
217
+ oauth2_request: req,
218
+ oauth2_response: res,
219
+ client:,
220
+ )
221
+
222
+ if auth_res.nil?
223
+ # We expect `config.handle_get_authorize` has sent response body
224
+ elsif auth_res.cancel
225
+ # Cancel the process
226
+ req.access_denied!
227
+ elsif auth_res.authenticated && auth_res.complete
228
+ # Authentication was successful
229
+ case req.response_type
230
+ when :code
231
+ res.code = config.get_authorization_code(auth_res)
232
+ when :token
233
+ req.unsupported_response_type!
234
+ end
235
+
236
+ res.approve!
237
+ end
185
238
  end
186
239
  end
187
240
  end
@@ -223,7 +276,11 @@ module HaveAPI::Authentication
223
276
  req.unsupported_grant_type!
224
277
 
225
278
  when :refresh_token
226
- config.find_authorization_by_refresh_token(client, req.refresh_token)
279
+ authorization = config.find_authorization_by_refresh_token(client, req.refresh_token)
280
+
281
+ if authorization.nil?
282
+ req.invalid_grant!
283
+ end
227
284
 
228
285
  access_token, expires_at, refresh_token = config.refresh_tokens(authorization, handler.request)
229
286
 
@@ -239,6 +296,30 @@ module HaveAPI::Authentication
239
296
  end
240
297
  end
241
298
  end
299
+
300
+ def revoke_endpoint(handler)
301
+ RevokeEndpoint.new do |req, res|
302
+ ret = config.handle_post_revoke(
303
+ handler.request,
304
+ req.token,
305
+ token_type_hint: req.token_type_hint,
306
+ )
307
+
308
+ case ret
309
+ when :revoked
310
+ # ok
311
+ when :unsupported
312
+ req.unsupported_token_type!
313
+ else
314
+ raise Rack::OAuth2::Server::Abstract::ServerError
315
+ end
316
+ end
317
+ end
318
+
319
+ private
320
+ def header_to_env(header)
321
+ "HTTP_#{header.upcase.gsub(/\-/, '_')}"
322
+ end
242
323
  end
243
324
  end
244
325
  end
@@ -0,0 +1,36 @@
1
+ require 'rack/oauth2'
2
+
3
+ module HaveAPI::Authentication
4
+ module OAuth2
5
+ class RevokeEndpoint < Rack::OAuth2::Server::Abstract::Handler
6
+ def _call(env)
7
+ @request = Request.new(env)
8
+ @response = Response.new(request)
9
+ super
10
+ end
11
+
12
+ class Request < Rack::OAuth2::Server::Abstract::Request
13
+ attr_required :token
14
+ attr_optional :token_type_hint
15
+
16
+ def initialize(env)
17
+ super
18
+ @token = params['token']
19
+ @token_type_hint = params['token_type_hint']
20
+ end
21
+
22
+ def unsupported_token_type!(description = nil, options = {})
23
+ raise Rack::OAuth2::Server::Abstract::BadRequest.new(
24
+ :unsupported_token_type,
25
+ description,
26
+ options,
27
+ )
28
+ end
29
+ end
30
+
31
+ class Response < Rack::OAuth2::Server::Abstract::Response
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -75,6 +75,7 @@ module HaveAPI::Authentication
75
75
  input do
76
76
  string :user, label: 'User', required: true
77
77
  password :password, label: 'Password', required: true
78
+ string :scope, label: 'Scope', default: 'all', fill: true
78
79
  end
79
80
 
80
81
  handle do
@@ -1,20 +1,27 @@
1
1
  module HaveAPI
2
2
  class Authorization
3
3
  def initialize(&block)
4
- @block = block
4
+ @blocks = [block]
5
5
  end
6
6
 
7
7
  # Returns true if user is authorized.
8
8
  # Block must call allow to authorize user, default rule is deny.
9
- def authorized?(user)
9
+ def authorized?(user, path_params)
10
10
  @restrict = []
11
11
 
12
12
  catch(:rule) do
13
- instance_exec(user, &@block) if @block
14
- deny # will not be called if block throws allow
13
+ @blocks.each do |block|
14
+ instance_exec(user, path_params, &block)
15
+ end
16
+
17
+ deny # will not be called if some block throws allow
15
18
  end
16
19
  end
17
20
 
21
+ def prepend_block(block)
22
+ @blocks.insert(0, block)
23
+ end
24
+
18
25
  # Apply restrictions on query which selects objects from database.
19
26
  # Most common usage is restrict user to access only objects he owns.
20
27
  def restrict(**kwargs)
@@ -22,22 +29,22 @@ module HaveAPI
22
29
  end
23
30
 
24
31
  # Restrict parameters client can set/change.
25
- # [whitelist] allow only listed parameters
26
- # [blacklist] allow all parameters except listed ones
32
+ # @param whitelist [Array<Symbol>] allow only listed parameters
33
+ # @param blacklist [Array<Symbol>] allow all parameters except listed ones
27
34
  def input(whitelist: nil, blacklist: nil)
28
35
  @input = {
29
- whitelist: whitelist,
30
- blacklist: blacklist,
36
+ whitelist: whitelist,
37
+ blacklist: blacklist,
31
38
  }
32
39
  end
33
40
 
34
41
  # Restrict parameters client can retrieve.
35
- # [whitelist] allow only listed parameters
36
- # [blacklist] allow all parameters except listed ones
42
+ # @param whitelist [Array<Symbol>] allow only listed parameters
43
+ # @param blacklist [Array<Symbol>] allow all parameters except listed ones
37
44
  def output(whitelist: nil, blacklist: nil)
38
45
  @output = {
39
- whitelist: whitelist,
40
- blacklist: blacklist,
46
+ whitelist: whitelist,
47
+ blacklist: blacklist,
41
48
  }
42
49
  end
43
50