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.
- checksums.yaml +4 -4
- data/haveapi.gemspec +2 -1
- data/lib/haveapi/action.rb +72 -31
- data/lib/haveapi/authentication/base.rb +1 -1
- data/lib/haveapi/authentication/basic/provider.rb +2 -2
- data/lib/haveapi/authentication/chain.rb +4 -4
- data/lib/haveapi/authentication/oauth2/config.rb +62 -15
- data/lib/haveapi/authentication/oauth2/provider.rb +111 -17
- data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +36 -0
- data/lib/haveapi/authentication/token/config.rb +1 -0
- data/lib/haveapi/authorization.rb +19 -12
- data/lib/haveapi/client_examples/js_client.rb +11 -1
- data/lib/haveapi/client_examples/php_client.rb +43 -1
- data/lib/haveapi/context.rb +21 -2
- data/lib/haveapi/example.rb +9 -9
- data/lib/haveapi/hooks.rb +23 -23
- data/lib/haveapi/metadata.rb +1 -1
- data/lib/haveapi/model_adapter.rb +14 -14
- data/lib/haveapi/model_adapters/active_record.rb +20 -20
- data/lib/haveapi/output_formatter.rb +4 -4
- data/lib/haveapi/output_formatters/base.rb +1 -1
- data/lib/haveapi/parameters/resource.rb +22 -22
- data/lib/haveapi/parameters/typed.rb +7 -7
- data/lib/haveapi/params.rb +24 -22
- data/lib/haveapi/resource.rb +9 -3
- data/lib/haveapi/resources/action_state.rb +16 -16
- data/lib/haveapi/route.rb +3 -2
- data/lib/haveapi/server.rb +113 -98
- data/lib/haveapi/spec/mock_action.rb +7 -7
- data/lib/haveapi/spec/spec_methods.rb +8 -8
- data/lib/haveapi/tasks/yard.rb +2 -2
- data/lib/haveapi/validator.rb +13 -13
- data/lib/haveapi/validator_chain.rb +6 -6
- data/lib/haveapi/validators/acceptance.rb +2 -2
- data/lib/haveapi/validators/confirmation.rb +4 -4
- data/lib/haveapi/validators/exclusion.rb +4 -4
- data/lib/haveapi/validators/format.rb +4 -4
- data/lib/haveapi/validators/inclusion.rb +3 -3
- data/lib/haveapi/validators/length.rb +1 -1
- data/lib/haveapi/validators/numericality.rb +3 -3
- data/lib/haveapi/validators/presence.rb +2 -2
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page/auth_body.erb +6 -4
- data/lib/haveapi/views/version_page/resource_body.erb +2 -0
- data/lib/haveapi.rb +1 -0
- data/spec/authorization_spec.rb +28 -28
- data/spec/envelope_spec.rb +4 -4
- data/spec/parameters/typed_spec.rb +3 -3
- data/spec/params_spec.rb +2 -2
- data/spec/validators/acceptance_spec.rb +2 -2
- data/spec/validators/confirmation_spec.rb +4 -4
- data/spec/validators/exclusion_spec.rb +2 -2
- data/spec/validators/format_spec.rb +5 -5
- data/spec/validators/inclusion_spec.rb +8 -8
- data/spec/validators/presence_spec.rb +1 -1
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b96b662c6e3b7319b0f2eb1c2e75ad7272f5727f5763840d9db8eaa62b83472c
|
4
|
+
data.tar.gz: 7c1865a1dd86d6b0a64b3856ef846f66abb10919d153ff985f020dc31270ca5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
data/lib/haveapi/action.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
416
|
-
|
417
|
-
|
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
|
-
|
429
|
-
|
430
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
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
|
-
|
447
|
-
|
448
|
-
|
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
|
-
|
578
|
-
|
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
|
-
|
584
|
-
|
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
|
-
|
609
|
-
|
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
|
@@ -36,8 +36,8 @@ module HaveAPI::Authentication
|
|
36
36
|
|
37
37
|
def describe
|
38
38
|
{
|
39
|
-
|
40
|
-
|
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
|
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
|
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
|
80
|
-
#
|
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 {#
|
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
|
-
#
|
19
|
+
# Handle GET authorize requests
|
20
20
|
#
|
21
|
-
# This method
|
22
|
-
#
|
23
|
-
# steps.
|
21
|
+
# This method usually writes HTML to `oauth2_response`, you must also set
|
22
|
+
# content type.
|
24
23
|
#
|
25
|
-
#
|
26
|
-
#
|
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
|
-
# @
|
32
|
-
|
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
|
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
|
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
|
-
|
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
|
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
|
125
|
-
|
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(
|
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
|
-
|
178
|
-
|
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
|