haveapi 0.18.2 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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: b96b662c6e3b7319b0f2eb1c2e75ad7272f5727f5763840d9db8eaa62b83472c
4
+ data.tar.gz: 7c1865a1dd86d6b0a64b3856ef846f66abb10919d153ff985f020dc31270ca5b
5
5
  SHA512:
6
- metadata.gz: '0876a3b9b10ad452c89a62e06fc56e29cfb6aff188e4943274adf4e4180464b90daa420e4ab0953faba1f1ae9a5df0a8a19ae0362acd7290043efefd050f8b19'
7
- data.tar.gz: cddc8f6c09dea69afc3a359ab18cd947a5a338ec9413d2f16eb600fa4893c88f0cbe3e3224b9ae58ac0d3de5707b96ec0e0891162d3d0ee9c97e906bff0c8e44
6
+ metadata.gz: 21ec1c16ae453630a70318ea92a80cdabf85b72e1d8eb53b6c074963c1c307dadd29b1af21607181b663e594481a2a1840399336d345dccb869dea9c61d92da9
7
+ data.tar.gz: af8c317576b3320bc3f184552de9cd67d96fd5c91f8c8395ec290f52fe8ac9f95f67ce04ddb06aeb2395feee011384561d61629d4db4834cefe6226e06270634
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.0'
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