haveapi 0.18.1 → 0.19.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 (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 +62 -15
  8. data/lib/haveapi/authentication/oauth2/provider.rb +111 -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: 7a503deac250571b471c672f113a2470e24ebf575ec19b09d44d1d48c210bf50
4
- data.tar.gz: 300d414e67b3d2083c0c6d352e1817274e7ed729d099b6f814aa5fd03bd81ce8
3
+ metadata.gz: b96b662c6e3b7319b0f2eb1c2e75ad7272f5727f5763840d9db8eaa62b83472c
4
+ data.tar.gz: 7c1865a1dd86d6b0a64b3856ef846f66abb10919d153ff985f020dc31270ca5b
5
5
  SHA512:
6
- metadata.gz: 3e7d0ecf8a43a49e8bf7dca76286f6dc43cb22c65794d080e2d74b7f5ba1e39ccb6d1d1800229f925b26df6dcbad8f8bb5eb74ac70c50741450c95a5ad9f893d
7
- data.tar.gz: 10581d63837d9a8a2fb19625dc8b61b1217fe0c4fe55dac6376190e45e482d5786ca0b32cc7d5bcd6c100f16871a5f27c789bfcdd5ea4edc5c63cadec5ad03bb
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.1'
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
@@ -130,13 +168,22 @@ module HaveAPI::Authentication
130
168
  #
131
169
  # @return [Hash<String, String>]
132
170
  def oauth2_params(req)
133
- {
171
+ ret = {
134
172
  client_id: req.client_id,
135
173
  response_type: req.response_type,
136
174
  redirect_uri: req.redirect_uri,
137
175
  scope: req.scope.join(' '),
138
176
  state: req.state,
139
177
  }
178
+
179
+ if req.code_challenge.present? && req.code_challenge_method.present?
180
+ ret.update(
181
+ code_challenge: req.code_challenge,
182
+ code_challenge_method: req.code_challenge_method,
183
+ )
184
+ end
185
+
186
+ ret
140
187
  end
141
188
  end
142
189
  end
@@ -35,6 +35,12 @@ module HaveAPI::Authentication
35
35
 
36
36
  # Abstract class describing ongoing authorization and what methods it must respond to
37
37
  class Authorization
38
+ # @return [String, nil]
39
+ attr_reader :code_challenge
40
+
41
+ # @return [String, nil]
42
+ attr_reader :code_challenge_method
43
+
38
44
  # @return [String]
39
45
  attr_reader :redirect_uri
40
46
 
@@ -72,9 +78,21 @@ module HaveAPI::Authentication
72
78
  super(server, v)
73
79
  end
74
80
 
81
+ def setup
82
+ @server.allow_header(config.http_header)
83
+ end
84
+
75
85
  def register_routes(sinatra, prefix)
76
86
  @authorize_path = File.join(prefix, 'authorize')
77
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
+
78
96
  that = self
79
97
 
80
98
  sinatra.get @authorize_path do
@@ -88,12 +106,17 @@ module HaveAPI::Authentication
88
106
  sinatra.post @token_path do
89
107
  that.token_endpoint(self).call(request.env)
90
108
  end
109
+
110
+ sinatra.post @revoke_path do
111
+ that.revoke_endpoint(self).call(request.env)
112
+ end
91
113
  end
92
114
 
93
115
  def authenticate(request)
94
116
  tokens = [
95
117
  request['access_token'],
96
- token_from_header(request)
118
+ token_from_authorization_header(request),
119
+ token_from_haveapi_header(request),
97
120
  ].compact
98
121
 
99
122
  token =
@@ -109,7 +132,7 @@ module HaveAPI::Authentication
109
132
  token && config.find_user_by_access_token(request, token)
110
133
  end
111
134
 
112
- def token_from_header(request)
135
+ def token_from_authorization_header(request)
113
136
  auth_header = Rack::Auth::AbstractRequest.new(request.env)
114
137
 
115
138
  if auth_header.provided? && !auth_header.parts.first.nil? && auth_header.scheme.to_s == 'bearer'
@@ -119,22 +142,36 @@ module HaveAPI::Authentication
119
142
  end
120
143
  end
121
144
 
145
+ def token_from_haveapi_header(request)
146
+ request.env[header_to_env(config.http_header)]
147
+ end
148
+
122
149
  def describe
123
150
  desc = <<-END
124
- OAuth2 authorization provider. While OAuth2 is not supported by HaveAPI
125
- 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.
126
154
 
127
155
  HaveAPI partially implements RFC 6749: authorization response type "code"
128
156
  and token grant types "authorization_code" and "refresh_token". Other
129
157
  response and grant types are not supported at this time.
130
158
 
131
- 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.
132
164
  END
133
165
 
134
166
  {
135
167
  description: desc,
168
+ http_header: config.http_header,
169
+ authorize_url: @authorize_url,
136
170
  authorize_path: @authorize_path,
171
+ token_url: @token_url,
137
172
  token_path: @token_path,
173
+ revoke_url: @revoke_url,
174
+ revoke_path: @revoke_path,
138
175
  }
139
176
  end
140
177
 
@@ -146,7 +183,14 @@ module HaveAPI::Authentication
146
183
  res.redirect_uri = req.verify_redirect_uri!(client.redirect_uri)
147
184
 
148
185
  if req.post?
149
- 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
+ )
150
194
 
151
195
  if auth_res.nil?
152
196
  # Authentication failed
@@ -164,18 +208,33 @@ module HaveAPI::Authentication
164
208
  end
165
209
 
166
210
  res.approve!
167
- elsif auth_res.authenticated && !auth_res.complete
168
- # Continue with another authentication step
169
- res.content_type = 'text/html'
170
- res.write(config.render_authorize_page(req, handler.params, client, auth_result: auth_res))
171
- else
172
- # Authentication failed, report errors and let the user retry
173
- res.content_type = 'text/html'
174
- res.write(config.render_authorize_page(req, handler.params, client, auth_result: auth_res))
175
211
  end
176
212
  else
177
- res.content_type = 'text/html'
178
- 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
179
238
  end
180
239
  end
181
240
  end
@@ -194,6 +253,13 @@ module HaveAPI::Authentication
194
253
  req.invalid_grant!
195
254
  end
196
255
 
256
+ if authorization.code_challenge && authorization.code_challenge_method
257
+ req.verify_code_verifier!(
258
+ authorization.code_challenge,
259
+ authorization.code_challenge_method.to_sym,
260
+ )
261
+ end
262
+
197
263
  access_token, expires_at, refresh_token = config.get_tokens(authorization, handler.request)
198
264
 
199
265
  bearer_token = Rack::OAuth2::AccessToken::Bearer.new(
@@ -210,7 +276,11 @@ module HaveAPI::Authentication
210
276
  req.unsupported_grant_type!
211
277
 
212
278
  when :refresh_token
213
- 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
214
284
 
215
285
  access_token, expires_at, refresh_token = config.refresh_tokens(authorization, handler.request)
216
286
 
@@ -226,6 +296,30 @@ module HaveAPI::Authentication
226
296
  end
227
297
  end
228
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
229
323
  end
230
324
  end
231
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