haveapi 0.18.1 → 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 +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