rodauth-oauth 0.7.4 → 0.8.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/CHANGELOG.md +1 -424
- data/README.md +26 -389
- data/doc/release_notes/0_0_1.md +3 -0
- data/doc/release_notes/0_0_2.md +15 -0
- data/doc/release_notes/0_0_3.md +31 -0
- data/doc/release_notes/0_0_4.md +36 -0
- data/doc/release_notes/0_0_5.md +36 -0
- data/doc/release_notes/0_0_6.md +21 -0
- data/doc/release_notes/0_1_0.md +44 -0
- data/doc/release_notes/0_2_0.md +43 -0
- data/doc/release_notes/0_3_0.md +28 -0
- data/doc/release_notes/0_4_0.md +18 -0
- data/doc/release_notes/0_4_1.md +9 -0
- data/doc/release_notes/0_4_2.md +5 -0
- data/doc/release_notes/0_4_3.md +3 -0
- data/doc/release_notes/0_5_0.md +11 -0
- data/doc/release_notes/0_5_1.md +13 -0
- data/doc/release_notes/0_6_0.md +9 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_7_0.md +20 -0
- data/doc/release_notes/0_7_1.md +10 -0
- data/doc/release_notes/0_7_2.md +21 -0
- data/doc/release_notes/0_7_3.md +10 -0
- data/doc/release_notes/0_7_4.md +5 -0
- data/doc/release_notes/0_8_0.md +37 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -3
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +22 -10
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +11 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +5 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +11 -15
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
- data/lib/rodauth/features/oauth.rb +3 -1418
- data/lib/rodauth/features/oauth_application_management.rb +209 -0
- data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
- data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
- data/lib/rodauth/features/oauth_base.rb +735 -0
- data/lib/rodauth/features/oauth_device_grant.rb +221 -0
- data/lib/rodauth/features/oauth_http_mac.rb +3 -21
- data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
- data/lib/rodauth/features/oauth_jwt.rb +37 -60
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
- data/lib/rodauth/features/oauth_pkce.rb +98 -0
- data/lib/rodauth/features/oauth_resource_server.rb +21 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
- data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
- data/lib/rodauth/features/oauth_token_management.rb +77 -0
- data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
- data/lib/rodauth/features/oidc.rb +4 -3
- data/lib/rodauth/oauth/database_extensions.rb +15 -2
- data/lib/rodauth/oauth/refinements.rb +48 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +28 -12
- data/templates/authorize.str +7 -7
- data/templates/client_secret_field.str +2 -2
- data/templates/description_field.str +1 -1
- data/templates/device_search.str +11 -0
- data/templates/device_verification.str +24 -0
- data/templates/homepage_url_field.str +2 -2
- data/templates/jws_jwk_field.str +4 -0
- data/templates/jwt_public_key_field.str +4 -0
- data/templates/name_field.str +1 -1
- data/templates/new_oauth_application.str +9 -0
- data/templates/oauth_application.str +7 -3
- data/templates/oauth_application_oauth_tokens.str +51 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_tokens.str +9 -11
- data/templates/redirect_uri_field.str +2 -2
- metadata +71 -3
- data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -1,1424 +1,9 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
-
require "time"
|
4
|
-
require "base64"
|
5
|
-
require "securerandom"
|
6
|
-
require "net/http"
|
7
|
-
|
8
|
-
require "rodauth/oauth/ttl_store"
|
9
|
-
require "rodauth/oauth/database_extensions"
|
10
|
-
|
11
3
|
module Rodauth
|
12
4
|
Feature.define(:oauth, :Oauth) do
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# Regexp class locally with #match? , but this is never tested, because ActiveSupport
|
17
|
-
# monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
|
18
|
-
# :nocov:
|
19
|
-
module RegexpExtensions
|
20
|
-
refine(Regexp) do
|
21
|
-
def match?(*args)
|
22
|
-
!match(*args).nil?
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
using(RegexpExtensions)
|
27
|
-
# :nocov:
|
28
|
-
end
|
29
|
-
|
30
|
-
unless String.method_defined?(:delete_suffix!)
|
31
|
-
module SuffixExtensions
|
32
|
-
refine(String) do
|
33
|
-
def delete_suffix!(suffix)
|
34
|
-
suffix = suffix.to_s
|
35
|
-
chomp! if frozen?
|
36
|
-
len = suffix.length
|
37
|
-
return unless len.positive? && index(suffix, -len)
|
38
|
-
|
39
|
-
self[-len..-1] = ""
|
40
|
-
self
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
using(SuffixExtensions)
|
45
|
-
end
|
46
|
-
|
47
|
-
SCOPES = %w[profile.read].freeze
|
48
|
-
|
49
|
-
SERVER_METADATA = OAuth::TtlStore.new
|
50
|
-
|
51
|
-
before "authorize"
|
52
|
-
after "authorize"
|
53
|
-
|
54
|
-
before "token"
|
55
|
-
|
56
|
-
before "revoke"
|
57
|
-
after "revoke"
|
58
|
-
|
59
|
-
before "introspect"
|
60
|
-
|
61
|
-
before "create_oauth_application"
|
62
|
-
after "create_oauth_application"
|
63
|
-
|
64
|
-
error_flash "Please authorize to continue", "require_authorization"
|
65
|
-
error_flash "There was an error registering your oauth application", "create_oauth_application"
|
66
|
-
notice_flash "Your oauth application has been registered", "create_oauth_application"
|
67
|
-
|
68
|
-
notice_flash "The oauth token has been revoked", "revoke_oauth_token"
|
69
|
-
error_flash "You are not authorized to revoke this token", "revoke_unauthorized_account"
|
70
|
-
|
71
|
-
view "authorize", "Authorize", "authorize"
|
72
|
-
view "oauth_applications", "Oauth Applications", "oauth_applications"
|
73
|
-
view "oauth_application", "Oauth Application", "oauth_application"
|
74
|
-
view "new_oauth_application", "New Oauth Application", "new_oauth_application"
|
75
|
-
view "oauth_tokens", "Oauth Tokens", "oauth_tokens"
|
76
|
-
|
77
|
-
auth_value_method :json_response_content_type, "application/json"
|
78
|
-
|
79
|
-
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
80
|
-
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
81
|
-
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
|
82
|
-
auth_value_method :use_oauth_implicit_grant_type?, false
|
83
|
-
auth_value_method :use_oauth_pkce?, true
|
84
|
-
auth_value_method :use_oauth_access_type?, true
|
85
|
-
|
86
|
-
auth_value_method :oauth_require_pkce, false
|
87
|
-
auth_value_method :oauth_pkce_challenge_method, "S256"
|
88
|
-
auth_value_method :oauth_response_mode, "query"
|
89
|
-
|
90
|
-
auth_value_method :oauth_valid_uri_schemes, %w[https]
|
91
|
-
|
92
|
-
auth_value_method :oauth_scope_separator, " "
|
93
|
-
|
94
|
-
# Application
|
95
|
-
APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
|
96
|
-
auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
|
97
|
-
|
98
|
-
(APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
|
99
|
-
auth_value_method :"oauth_application_#{param}_param", param
|
100
|
-
translatable_method :"#{param}_label", param.gsub("_", " ").capitalize
|
101
|
-
end
|
102
|
-
button "Register", "oauth_application"
|
103
|
-
button "Authorize", "oauth_authorize"
|
104
|
-
button "Revoke", "oauth_token_revoke"
|
105
|
-
button "Back to Client Application", "oauth_authorize_post"
|
106
|
-
|
107
|
-
# OAuth Token
|
108
|
-
auth_value_method :oauth_tokens_path, "oauth-tokens"
|
109
|
-
auth_value_method :oauth_tokens_table, :oauth_tokens
|
110
|
-
auth_value_method :oauth_tokens_id_column, :id
|
111
|
-
|
112
|
-
%i[
|
113
|
-
oauth_application_id oauth_token_id oauth_grant_id account_id
|
114
|
-
token refresh_token scopes
|
115
|
-
expires_in revoked_at
|
116
|
-
].each do |column|
|
117
|
-
auth_value_method :"oauth_tokens_#{column}_column", column
|
118
|
-
end
|
119
|
-
|
120
|
-
# Oauth Token Hash
|
121
|
-
auth_value_method :oauth_tokens_token_hash_column, nil
|
122
|
-
auth_value_method :oauth_tokens_refresh_token_hash_column, nil
|
123
|
-
|
124
|
-
# Access Token reuse
|
125
|
-
auth_value_method :oauth_reuse_access_token, false
|
126
|
-
# OAuth Grants
|
127
|
-
auth_value_method :oauth_grants_table, :oauth_grants
|
128
|
-
auth_value_method :oauth_grants_id_column, :id
|
129
|
-
%i[
|
130
|
-
account_id oauth_application_id
|
131
|
-
redirect_uri code scopes access_type
|
132
|
-
expires_in revoked_at
|
133
|
-
code_challenge code_challenge_method
|
134
|
-
].each do |column|
|
135
|
-
auth_value_method :"oauth_grants_#{column}_column", column
|
136
|
-
end
|
137
|
-
|
138
|
-
auth_value_method :authorization_required_error_status, 401
|
139
|
-
auth_value_method :invalid_oauth_response_status, 400
|
140
|
-
auth_value_method :already_in_use_response_status, 409
|
141
|
-
|
142
|
-
# OAuth Applications
|
143
|
-
auth_value_method :oauth_applications_route, "oauth-applications"
|
144
|
-
def oauth_applications_path(opts = {})
|
145
|
-
route_path(oauth_applications_route, opts)
|
146
|
-
end
|
147
|
-
|
148
|
-
def oauth_applications_url(opts = {})
|
149
|
-
route_url(oauth_applications_route, opts)
|
150
|
-
end
|
151
|
-
|
152
|
-
auth_value_method :oauth_applications_table, :oauth_applications
|
153
|
-
|
154
|
-
auth_value_method :oauth_applications_id_column, :id
|
155
|
-
auth_value_method :oauth_applications_id_pattern, Integer
|
156
|
-
|
157
|
-
%i[
|
158
|
-
account_id
|
159
|
-
name description scopes
|
160
|
-
client_id client_secret
|
161
|
-
homepage_url redirect_uri
|
162
|
-
].each do |column|
|
163
|
-
auth_value_method :"oauth_applications_#{column}_column", column
|
164
|
-
end
|
165
|
-
|
166
|
-
# Feature options
|
167
|
-
auth_value_method :oauth_application_default_scope, SCOPES.first
|
168
|
-
auth_value_method :oauth_application_scopes, SCOPES
|
169
|
-
auth_value_method :oauth_token_type, "bearer"
|
170
|
-
auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
|
171
|
-
|
172
|
-
translatable_method :invalid_client_message, "Invalid client"
|
173
|
-
translatable_method :invalid_grant_type_message, "Invalid grant type"
|
174
|
-
translatable_method :invalid_grant_message, "Invalid grant"
|
175
|
-
translatable_method :invalid_scope_message, "Invalid scope"
|
176
|
-
|
177
|
-
translatable_method :invalid_url_message, "Invalid URL"
|
178
|
-
translatable_method :unsupported_token_type_message, "Invalid token type hint"
|
179
|
-
|
180
|
-
translatable_method :unique_error_message, "is already in use"
|
181
|
-
translatable_method :null_error_message, "is not filled"
|
182
|
-
translatable_method :already_in_use_message, "error generating unique token"
|
183
|
-
auth_value_method :already_in_use_error_code, "invalid_request"
|
184
|
-
|
185
|
-
# PKCE
|
186
|
-
auth_value_method :code_challenge_required_error_code, "invalid_request"
|
187
|
-
translatable_method :code_challenge_required_message, "code challenge required"
|
188
|
-
auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
|
189
|
-
translatable_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
|
190
|
-
|
191
|
-
# METADATA
|
192
|
-
auth_value_method :oauth_metadata_service_documentation, nil
|
193
|
-
auth_value_method :oauth_metadata_ui_locales_supported, nil
|
194
|
-
auth_value_method :oauth_metadata_op_policy_uri, nil
|
195
|
-
auth_value_method :oauth_metadata_op_tos_uri, nil
|
196
|
-
|
197
|
-
# Resource Server params
|
198
|
-
# Only required to use if the plugin is to be used in a resource server
|
199
|
-
auth_value_method :is_authorization_server?, true
|
200
|
-
|
201
|
-
auth_value_method :oauth_unique_id_generation_retries, 3
|
202
|
-
|
203
|
-
auth_value_methods(
|
204
|
-
:oauth_application_path,
|
205
|
-
:fetch_access_token,
|
206
|
-
:oauth_unique_id_generator,
|
207
|
-
:secret_matches?,
|
208
|
-
:secret_hash,
|
209
|
-
:generate_token_hash,
|
210
|
-
:authorization_server_url,
|
211
|
-
:before_introspection_request,
|
212
|
-
:require_authorizable_account,
|
213
|
-
:oauth_tokens_unique_columns
|
214
|
-
)
|
215
|
-
|
216
|
-
auth_value_methods(:only_json?)
|
217
|
-
|
218
|
-
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
219
|
-
|
220
|
-
# /token
|
221
|
-
route(:token) do |r|
|
222
|
-
next unless is_authorization_server?
|
223
|
-
|
224
|
-
before_token_route
|
225
|
-
require_oauth_application
|
226
|
-
|
227
|
-
r.post do
|
228
|
-
catch_error do
|
229
|
-
validate_oauth_token_params
|
230
|
-
|
231
|
-
oauth_token = nil
|
232
|
-
transaction do
|
233
|
-
before_token
|
234
|
-
oauth_token = create_oauth_token
|
235
|
-
end
|
236
|
-
|
237
|
-
json_response_success(json_access_token_payload(oauth_token))
|
238
|
-
end
|
239
|
-
|
240
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
# /introspect
|
245
|
-
route(:introspect) do |r|
|
246
|
-
next unless is_authorization_server?
|
247
|
-
|
248
|
-
before_introspect_route
|
249
|
-
|
250
|
-
r.post do
|
251
|
-
catch_error do
|
252
|
-
validate_oauth_introspect_params
|
253
|
-
|
254
|
-
before_introspect
|
255
|
-
oauth_token = case param("token_type_hint")
|
256
|
-
when "access_token"
|
257
|
-
oauth_token_by_token(param("token"))
|
258
|
-
when "refresh_token"
|
259
|
-
oauth_token_by_refresh_token(param("token"))
|
260
|
-
else
|
261
|
-
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
262
|
-
end
|
263
|
-
|
264
|
-
if oauth_application
|
265
|
-
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
266
|
-
elsif oauth_token
|
267
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
268
|
-
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
269
|
-
end
|
270
|
-
|
271
|
-
json_response_success(json_token_introspect_payload(oauth_token))
|
272
|
-
end
|
273
|
-
|
274
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
# /revoke
|
279
|
-
route(:revoke) do |r|
|
280
|
-
next unless is_authorization_server?
|
281
|
-
|
282
|
-
before_revoke_route
|
283
|
-
|
284
|
-
if logged_in?
|
285
|
-
require_account
|
286
|
-
require_oauth_application_from_account
|
287
|
-
else
|
288
|
-
require_oauth_application
|
289
|
-
end
|
290
|
-
|
291
|
-
r.post do
|
292
|
-
catch_error do
|
293
|
-
validate_oauth_revoke_params
|
294
|
-
|
295
|
-
oauth_token = nil
|
296
|
-
transaction do
|
297
|
-
before_revoke
|
298
|
-
oauth_token = revoke_oauth_token
|
299
|
-
after_revoke
|
300
|
-
end
|
301
|
-
|
302
|
-
if accepts_json?
|
303
|
-
json_response_success \
|
304
|
-
"token" => oauth_token[oauth_tokens_token_column],
|
305
|
-
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
306
|
-
"revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
|
307
|
-
else
|
308
|
-
set_notice_flash revoke_oauth_token_notice_flash
|
309
|
-
redirect request.referer || "/"
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
redirect_response_error("invalid_request", request.referer || "/")
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
# /authorize
|
318
|
-
route(:authorize) do |r|
|
319
|
-
next unless is_authorization_server?
|
320
|
-
|
321
|
-
before_authorize_route
|
322
|
-
require_authorizable_account
|
323
|
-
|
324
|
-
validate_oauth_grant_params
|
325
|
-
try_approval_prompt if use_oauth_access_type? && request.get?
|
326
|
-
|
327
|
-
r.get do
|
328
|
-
authorize_view
|
329
|
-
end
|
330
|
-
|
331
|
-
r.post do
|
332
|
-
redirect_url = URI.parse(redirect_uri)
|
333
|
-
|
334
|
-
params, mode = transaction do
|
335
|
-
before_authorize
|
336
|
-
do_authorize
|
337
|
-
end
|
338
|
-
|
339
|
-
case mode
|
340
|
-
when "query"
|
341
|
-
params = params.map { |k, v| "#{k}=#{v}" }
|
342
|
-
params << redirect_url.query if redirect_url.query
|
343
|
-
redirect_url.query = params.join("&")
|
344
|
-
redirect(redirect_url.to_s)
|
345
|
-
when "fragment"
|
346
|
-
params = params.map { |k, v| "#{k}=#{v}" }
|
347
|
-
params << redirect_url.query if redirect_url.query
|
348
|
-
redirect_url.fragment = params.join("&")
|
349
|
-
redirect(redirect_url.to_s)
|
350
|
-
when "form_post"
|
351
|
-
scope.view layout: false, inline: <<-FORM
|
352
|
-
<html>
|
353
|
-
<head><title>Authorized</title></head>
|
354
|
-
<body onload="javascript:document.forms[0].submit()">
|
355
|
-
<form method="post" action="#{redirect_uri}">
|
356
|
-
#{
|
357
|
-
params.map do |name, value|
|
358
|
-
"<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
|
359
|
-
end.join
|
360
|
-
}
|
361
|
-
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
|
362
|
-
</form>
|
363
|
-
</body>
|
364
|
-
</html>
|
365
|
-
FORM
|
366
|
-
when "none"
|
367
|
-
redirect(redirect_url.to_s)
|
368
|
-
end
|
369
|
-
end
|
370
|
-
end
|
371
|
-
|
372
|
-
def oauth_server_metadata(issuer = nil)
|
373
|
-
request.on(".well-known") do
|
374
|
-
request.on("oauth-authorization-server") do
|
375
|
-
request.get do
|
376
|
-
json_response_success(oauth_server_metadata_body(issuer), true)
|
377
|
-
end
|
378
|
-
end
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
def oauth_application_path(id)
|
383
|
-
"#{oauth_applications_path}/#{id}"
|
384
|
-
end
|
385
|
-
|
386
|
-
# /oauth-applications routes
|
387
|
-
def oauth_applications
|
388
|
-
request.on(oauth_applications_route) do
|
389
|
-
require_account
|
390
|
-
|
391
|
-
request.get "new" do
|
392
|
-
new_oauth_application_view
|
393
|
-
end
|
394
|
-
|
395
|
-
request.on(oauth_applications_id_pattern) do |id|
|
396
|
-
oauth_application = db[oauth_applications_table]
|
397
|
-
.where(oauth_applications_id_column => id)
|
398
|
-
.where(oauth_applications_account_id_column => account_id)
|
399
|
-
.first
|
400
|
-
next unless oauth_application
|
401
|
-
|
402
|
-
scope.instance_variable_set(:@oauth_application, oauth_application)
|
403
|
-
|
404
|
-
request.is do
|
405
|
-
request.get do
|
406
|
-
oauth_application_view
|
407
|
-
end
|
408
|
-
end
|
409
|
-
|
410
|
-
request.on(oauth_tokens_path) do
|
411
|
-
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
412
|
-
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
413
|
-
request.get do
|
414
|
-
oauth_tokens_view
|
415
|
-
end
|
416
|
-
end
|
417
|
-
end
|
418
|
-
|
419
|
-
request.get do
|
420
|
-
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table]
|
421
|
-
.where(oauth_applications_account_id_column => account_id))
|
422
|
-
oauth_applications_view
|
423
|
-
end
|
424
|
-
|
425
|
-
request.post do
|
426
|
-
catch_error do
|
427
|
-
validate_oauth_application_params
|
428
|
-
|
429
|
-
transaction do
|
430
|
-
before_create_oauth_application
|
431
|
-
id = create_oauth_application
|
432
|
-
after_create_oauth_application
|
433
|
-
set_notice_flash create_oauth_application_notice_flash
|
434
|
-
redirect "#{request.path}/#{id}"
|
435
|
-
end
|
436
|
-
end
|
437
|
-
set_error_flash create_oauth_application_error_flash
|
438
|
-
new_oauth_application_view
|
439
|
-
end
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
|
-
def check_csrf?
|
444
|
-
case request.path
|
445
|
-
when token_path, introspect_path
|
446
|
-
false
|
447
|
-
when revoke_path
|
448
|
-
!json_request?
|
449
|
-
when authorize_path, oauth_applications_path
|
450
|
-
only_json? ? false : super
|
451
|
-
else
|
452
|
-
super
|
453
|
-
end
|
454
|
-
end
|
455
|
-
|
456
|
-
# Overrides session_value, so that a valid authorization token also authenticates a request
|
457
|
-
def session_value
|
458
|
-
super || begin
|
459
|
-
return unless authorization_token
|
460
|
-
|
461
|
-
authorization_token[oauth_tokens_account_id_column]
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
def accepts_json?
|
466
|
-
return true if only_json?
|
467
|
-
|
468
|
-
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
|
469
|
-
end
|
470
|
-
|
471
|
-
unless method_defined?(:json_request?)
|
472
|
-
# copied from the jwt feature
|
473
|
-
def json_request?
|
474
|
-
return @json_request if defined?(@json_request)
|
475
|
-
|
476
|
-
@json_request = request.content_type =~ json_request_regexp
|
477
|
-
end
|
478
|
-
end
|
479
|
-
|
480
|
-
def scopes
|
481
|
-
scope = request.params["scope"]
|
482
|
-
case scope
|
483
|
-
when Array
|
484
|
-
scope
|
485
|
-
when String
|
486
|
-
scope.split(" ")
|
487
|
-
when nil
|
488
|
-
Array(oauth_application_default_scope)
|
489
|
-
end
|
490
|
-
end
|
491
|
-
|
492
|
-
def redirect_uri
|
493
|
-
param_or_nil("redirect_uri") || begin
|
494
|
-
return unless oauth_application
|
495
|
-
|
496
|
-
redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
|
497
|
-
redirect_uris.size == 1 ? redirect_uris.first : nil
|
498
|
-
end
|
499
|
-
end
|
500
|
-
|
501
|
-
def oauth_application
|
502
|
-
return @oauth_application if defined?(@oauth_application)
|
503
|
-
|
504
|
-
@oauth_application = begin
|
505
|
-
client_id = param_or_nil("client_id")
|
506
|
-
|
507
|
-
return unless client_id
|
508
|
-
|
509
|
-
db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
|
510
|
-
end
|
511
|
-
end
|
512
|
-
|
513
|
-
def fetch_access_token
|
514
|
-
value = request.env["HTTP_AUTHORIZATION"]
|
515
|
-
|
516
|
-
return unless value && !value.empty?
|
517
|
-
|
518
|
-
scheme, token = value.split(" ", 2)
|
519
|
-
|
520
|
-
return unless scheme.downcase == oauth_token_type
|
521
|
-
|
522
|
-
return if token.nil? || token.empty?
|
523
|
-
|
524
|
-
token
|
525
|
-
end
|
526
|
-
|
527
|
-
def authorization_token
|
528
|
-
return @authorization_token if defined?(@authorization_token)
|
529
|
-
|
530
|
-
# check if there is a token
|
531
|
-
bearer_token = fetch_access_token
|
532
|
-
|
533
|
-
return unless bearer_token
|
534
|
-
|
535
|
-
@authorization_token = if is_authorization_server?
|
536
|
-
# check if token has not expired
|
537
|
-
# check if token has been revoked
|
538
|
-
oauth_token_by_token(bearer_token)
|
539
|
-
else
|
540
|
-
# where in resource server, NOT the authorization server.
|
541
|
-
payload = introspection_request("access_token", bearer_token)
|
542
|
-
|
543
|
-
return unless payload["active"]
|
544
|
-
|
545
|
-
payload
|
546
|
-
end
|
547
|
-
end
|
548
|
-
|
549
|
-
def require_oauth_authorization(*scopes)
|
550
|
-
authorization_required unless authorization_token
|
551
|
-
|
552
|
-
scopes << oauth_application_default_scope if scopes.empty?
|
553
|
-
|
554
|
-
token_scopes = if is_authorization_server?
|
555
|
-
authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
|
556
|
-
else
|
557
|
-
aux_scopes = authorization_token["scope"]
|
558
|
-
if aux_scopes
|
559
|
-
aux_scopes.split(oauth_scope_separator)
|
560
|
-
else
|
561
|
-
[]
|
562
|
-
end
|
563
|
-
end
|
564
|
-
|
565
|
-
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
566
|
-
end
|
567
|
-
|
568
|
-
def post_configure
|
569
|
-
super
|
570
|
-
|
571
|
-
# all of the extensions below involve DB changes. Resource server mode doesn't use
|
572
|
-
# database functions for OAuth though.
|
573
|
-
return unless is_authorization_server?
|
574
|
-
|
575
|
-
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
576
|
-
|
577
|
-
# Check whether we can reutilize db entries for the same account / application pair
|
578
|
-
one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
|
579
|
-
definition[:unique] &&
|
580
|
-
definition[:columns] == oauth_tokens_unique_columns
|
581
|
-
end
|
582
|
-
|
583
|
-
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
|
584
|
-
|
585
|
-
i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
|
586
|
-
end
|
587
|
-
|
588
|
-
def use_date_arithmetic?
|
589
|
-
true
|
590
|
-
end
|
591
|
-
|
592
|
-
private
|
593
|
-
|
594
|
-
def rescue_from_uniqueness_error(&block)
|
595
|
-
retries = oauth_unique_id_generation_retries
|
596
|
-
begin
|
597
|
-
transaction(savepoint: :only, &block)
|
598
|
-
rescue Sequel::UniqueConstraintViolation
|
599
|
-
redirect_response_error("already_in_use") if retries.zero?
|
600
|
-
retries -= 1
|
601
|
-
retry
|
602
|
-
end
|
603
|
-
end
|
604
|
-
|
605
|
-
# OAuth Token Unique/Reuse
|
606
|
-
def oauth_tokens_unique_columns
|
607
|
-
[
|
608
|
-
oauth_tokens_oauth_application_id_column,
|
609
|
-
oauth_tokens_account_id_column,
|
610
|
-
oauth_tokens_scopes_column
|
611
|
-
]
|
612
|
-
end
|
613
|
-
|
614
|
-
def authorization_server_url
|
615
|
-
base_url
|
616
|
-
end
|
617
|
-
|
618
|
-
def authorization_server_metadata
|
619
|
-
auth_url = URI(authorization_server_url)
|
620
|
-
|
621
|
-
server_metadata = SERVER_METADATA[auth_url]
|
622
|
-
|
623
|
-
return server_metadata if server_metadata
|
624
|
-
|
625
|
-
SERVER_METADATA.set(auth_url) do
|
626
|
-
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
627
|
-
http.use_ssl = auth_url.scheme == "https"
|
628
|
-
|
629
|
-
request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
|
630
|
-
request["accept"] = json_response_content_type
|
631
|
-
response = http.request(request)
|
632
|
-
authorization_required unless response.code.to_i == 200
|
633
|
-
|
634
|
-
# time-to-live
|
635
|
-
ttl = if response.key?("cache-control")
|
636
|
-
cache_control = response["cache-control"]
|
637
|
-
cache_control[/max-age=(\d+)/, 1].to_i
|
638
|
-
elsif response.key?("expires")
|
639
|
-
Time.parse(response["expires"]).to_i - Time.now.to_i
|
640
|
-
end
|
641
|
-
|
642
|
-
[JSON.parse(response.body, symbolize_names: true), ttl]
|
643
|
-
end
|
644
|
-
end
|
645
|
-
|
646
|
-
def introspection_request(token_type_hint, token)
|
647
|
-
auth_url = URI(authorization_server_url)
|
648
|
-
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
649
|
-
http.use_ssl = auth_url.scheme == "https"
|
650
|
-
|
651
|
-
request = Net::HTTP::Post.new(introspect_path)
|
652
|
-
request["content-type"] = "application/x-www-form-urlencoded"
|
653
|
-
request["accept"] = json_response_content_type
|
654
|
-
request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token })
|
655
|
-
|
656
|
-
before_introspection_request(request)
|
657
|
-
response = http.request(request)
|
658
|
-
authorization_required unless response.code.to_i == 200
|
659
|
-
|
660
|
-
JSON.parse(response.body)
|
661
|
-
end
|
662
|
-
|
663
|
-
def before_introspection_request(request); end
|
664
|
-
|
665
|
-
def template_path(page)
|
666
|
-
path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
|
667
|
-
return super unless File.exist?(path)
|
668
|
-
|
669
|
-
path
|
670
|
-
end
|
671
|
-
|
672
|
-
# to be used internally. Same semantics as require account, must:
|
673
|
-
# fetch an authorization basic header
|
674
|
-
# parse client id and secret
|
675
|
-
#
|
676
|
-
def require_oauth_application
|
677
|
-
# get client credenntials
|
678
|
-
client_id = client_secret = nil
|
679
|
-
|
680
|
-
# client_secret_basic
|
681
|
-
if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
|
682
|
-
client_id, client_secret = Base64.decode64(token).split(/:/, 2)
|
683
|
-
else
|
684
|
-
client_id = param_or_nil("client_id")
|
685
|
-
client_secret = param_or_nil("client_secret")
|
686
|
-
end
|
687
|
-
|
688
|
-
authorization_required unless client_id
|
689
|
-
|
690
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
691
|
-
|
692
|
-
# skip if using pkce
|
693
|
-
return if @oauth_application && use_oauth_pkce? && param_or_nil("code_verifier")
|
694
|
-
|
695
|
-
authorization_required unless @oauth_application && secret_matches?(@oauth_application, client_secret)
|
696
|
-
end
|
697
|
-
|
698
|
-
def require_oauth_application_from_account
|
699
|
-
ds = db[oauth_applications_table]
|
700
|
-
.join(oauth_tokens_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
|
701
|
-
Sequel[oauth_applications_table][oauth_applications_id_column])
|
702
|
-
.where(oauth_token_by_token_ds(param("token")).opts.fetch(:where, true))
|
703
|
-
.where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
|
704
|
-
|
705
|
-
@oauth_application = ds.qualify.first
|
706
|
-
return if @oauth_application
|
707
|
-
|
708
|
-
set_redirect_error_flash revoke_unauthorized_account_error_flash
|
709
|
-
redirect request.referer || "/"
|
710
|
-
end
|
711
|
-
|
712
|
-
def secret_matches?(oauth_application, secret)
|
713
|
-
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
|
714
|
-
end
|
715
|
-
|
716
|
-
def secret_hash(secret)
|
717
|
-
password_hash(secret)
|
718
|
-
end
|
719
|
-
|
720
|
-
def oauth_unique_id_generator
|
721
|
-
SecureRandom.urlsafe_base64(32)
|
722
|
-
end
|
723
|
-
|
724
|
-
def generate_token_hash(token)
|
725
|
-
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
|
726
|
-
end
|
727
|
-
|
728
|
-
def token_from_application?(oauth_token, oauth_application)
|
729
|
-
oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
|
730
|
-
end
|
731
|
-
|
732
|
-
unless method_defined?(:password_hash)
|
733
|
-
# From login_requirements_base feature
|
734
|
-
|
735
|
-
def password_hash(password)
|
736
|
-
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
|
737
|
-
end
|
738
|
-
end
|
739
|
-
|
740
|
-
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
741
|
-
create_params = {
|
742
|
-
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
743
|
-
}.merge(params)
|
744
|
-
|
745
|
-
rescue_from_uniqueness_error do
|
746
|
-
token = oauth_unique_id_generator
|
747
|
-
|
748
|
-
if oauth_tokens_token_hash_column
|
749
|
-
create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
750
|
-
else
|
751
|
-
create_params[oauth_tokens_token_column] = token
|
752
|
-
end
|
753
|
-
|
754
|
-
refresh_token = nil
|
755
|
-
if should_generate_refresh_token
|
756
|
-
refresh_token = oauth_unique_id_generator
|
757
|
-
|
758
|
-
if oauth_tokens_refresh_token_hash_column
|
759
|
-
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
760
|
-
else
|
761
|
-
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
762
|
-
end
|
763
|
-
end
|
764
|
-
oauth_token = _generate_oauth_token(create_params)
|
765
|
-
oauth_token[oauth_tokens_token_column] = token
|
766
|
-
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
767
|
-
oauth_token
|
768
|
-
end
|
769
|
-
end
|
770
|
-
|
771
|
-
def _generate_oauth_token(params = {})
|
772
|
-
ds = db[oauth_tokens_table]
|
773
|
-
|
774
|
-
if __one_oauth_token_per_account
|
775
|
-
|
776
|
-
token = __insert_or_update_and_return__(
|
777
|
-
ds,
|
778
|
-
oauth_tokens_id_column,
|
779
|
-
oauth_tokens_unique_columns,
|
780
|
-
params,
|
781
|
-
Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
|
782
|
-
([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
|
783
|
-
)
|
784
|
-
|
785
|
-
# if the previous operation didn't return a row, it means that the conditions
|
786
|
-
# invalidated the update, and the existing token is still valid.
|
787
|
-
token || ds.where(
|
788
|
-
oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
|
789
|
-
oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
|
790
|
-
).first
|
791
|
-
else
|
792
|
-
if oauth_reuse_access_token
|
793
|
-
unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
|
794
|
-
valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
|
795
|
-
.where(unique_conds).first
|
796
|
-
return valid_token if valid_token
|
797
|
-
end
|
798
|
-
__insert_and_return__(ds, oauth_tokens_id_column, params)
|
799
|
-
end
|
800
|
-
end
|
801
|
-
|
802
|
-
def oauth_token_by_token_ds(token)
|
803
|
-
ds = db[oauth_tokens_table]
|
804
|
-
|
805
|
-
ds = if oauth_tokens_token_hash_column
|
806
|
-
ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_hash_column] => generate_token_hash(token))
|
807
|
-
else
|
808
|
-
ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_column] => token)
|
809
|
-
end
|
810
|
-
|
811
|
-
ds.where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
812
|
-
.where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil)
|
813
|
-
end
|
814
|
-
|
815
|
-
def oauth_token_by_token(token)
|
816
|
-
oauth_token_by_token_ds(token).first
|
817
|
-
end
|
818
|
-
|
819
|
-
def oauth_token_by_refresh_token(token, revoked: false)
|
820
|
-
ds = db[oauth_tokens_table]
|
821
|
-
#
|
822
|
-
# filter expired refresh tokens out.
|
823
|
-
# an expired refresh token is a token whose access token expired for a period longer than the
|
824
|
-
# refresh token expiration period.
|
825
|
-
#
|
826
|
-
ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
|
827
|
-
|
828
|
-
ds = if oauth_tokens_refresh_token_hash_column
|
829
|
-
ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
|
830
|
-
else
|
831
|
-
ds.where(oauth_tokens_refresh_token_column => token)
|
832
|
-
end
|
833
|
-
|
834
|
-
ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
|
835
|
-
|
836
|
-
ds.first
|
837
|
-
end
|
838
|
-
|
839
|
-
def json_access_token_payload(oauth_token)
|
840
|
-
payload = {
|
841
|
-
"access_token" => oauth_token[oauth_tokens_token_column],
|
842
|
-
"token_type" => oauth_token_type,
|
843
|
-
"expires_in" => oauth_token_expires_in
|
844
|
-
}
|
845
|
-
payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
|
846
|
-
payload
|
847
|
-
end
|
848
|
-
|
849
|
-
# Oauth Application
|
850
|
-
|
851
|
-
def oauth_application_params
|
852
|
-
@oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
|
853
|
-
value = request.params[__send__(:"oauth_application_#{param}_param")]
|
854
|
-
if value && !value.empty?
|
855
|
-
params[param] = value
|
856
|
-
else
|
857
|
-
set_field_error(param, null_error_message)
|
858
|
-
end
|
859
|
-
end
|
860
|
-
end
|
861
|
-
|
862
|
-
def validate_oauth_application_params
|
863
|
-
oauth_application_params.each do |key, value|
|
864
|
-
if key == oauth_application_homepage_url_param
|
865
|
-
|
866
|
-
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
|
867
|
-
|
868
|
-
elsif key == oauth_application_redirect_uri_param
|
869
|
-
|
870
|
-
if value.respond_to?(:each)
|
871
|
-
value.each do |uri|
|
872
|
-
next if uri.empty?
|
873
|
-
|
874
|
-
set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
|
875
|
-
end
|
876
|
-
else
|
877
|
-
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
|
878
|
-
end
|
879
|
-
elsif key == oauth_application_scopes_param
|
880
|
-
|
881
|
-
value.each do |scope|
|
882
|
-
set_field_error(key, invalid_scope_message) unless oauth_application_scopes.include?(scope)
|
883
|
-
end
|
884
|
-
end
|
885
|
-
end
|
886
|
-
|
887
|
-
throw :rodauth_error if @field_errors && !@field_errors.empty?
|
888
|
-
end
|
889
|
-
|
890
|
-
def create_oauth_application
|
891
|
-
create_params = {
|
892
|
-
oauth_applications_account_id_column => account_id,
|
893
|
-
oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
|
894
|
-
oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
|
895
|
-
oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
|
896
|
-
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
|
897
|
-
}
|
898
|
-
|
899
|
-
redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
|
900
|
-
redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
|
901
|
-
create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
|
902
|
-
# set client ID/secret pairs
|
903
|
-
|
904
|
-
create_params.merge! \
|
905
|
-
oauth_applications_client_secret_column => \
|
906
|
-
secret_hash(oauth_application_params[oauth_application_client_secret_param])
|
907
|
-
|
908
|
-
create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
|
909
|
-
create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
|
910
|
-
else
|
911
|
-
oauth_application_default_scope
|
912
|
-
end
|
913
|
-
|
914
|
-
rescue_from_uniqueness_error do
|
915
|
-
create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
|
916
|
-
db[oauth_applications_table].insert(create_params)
|
917
|
-
end
|
918
|
-
end
|
919
|
-
|
920
|
-
# Authorize
|
921
|
-
def require_authorizable_account
|
922
|
-
require_account
|
923
|
-
end
|
924
|
-
|
925
|
-
def validate_oauth_grant_params
|
926
|
-
redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
|
927
|
-
|
928
|
-
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
|
929
|
-
check_valid_approval_prompt? && check_valid_response_type?
|
930
|
-
redirect_response_error("invalid_request")
|
931
|
-
end
|
932
|
-
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
933
|
-
|
934
|
-
if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
|
935
|
-
redirect_response_error("invalid_request")
|
936
|
-
end
|
937
|
-
validate_pkce_challenge_params if use_oauth_pkce?
|
938
|
-
end
|
939
|
-
|
940
|
-
def try_approval_prompt
|
941
|
-
approval_prompt = param_or_nil("approval_prompt")
|
942
|
-
|
943
|
-
return unless approval_prompt && approval_prompt == "auto"
|
944
|
-
|
945
|
-
return if db[oauth_grants_table].where(
|
946
|
-
oauth_grants_account_id_column => account_id,
|
947
|
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
948
|
-
oauth_grants_redirect_uri_column => redirect_uri,
|
949
|
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
950
|
-
oauth_grants_access_type_column => "online"
|
951
|
-
).count.zero?
|
952
|
-
|
953
|
-
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
|
954
|
-
request.env["REQUEST_METHOD"] = "POST"
|
955
|
-
end
|
956
|
-
|
957
|
-
def create_oauth_grant(create_params = {})
|
958
|
-
create_params.merge!(
|
959
|
-
oauth_grants_account_id_column => account_id,
|
960
|
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
961
|
-
oauth_grants_redirect_uri_column => redirect_uri,
|
962
|
-
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
|
963
|
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
964
|
-
)
|
965
|
-
|
966
|
-
# Access Type flow
|
967
|
-
if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
|
968
|
-
create_params[oauth_grants_access_type_column] = access_type
|
969
|
-
end
|
970
|
-
|
971
|
-
# PKCE flow
|
972
|
-
if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
|
973
|
-
code_challenge_method = param_or_nil("code_challenge_method")
|
974
|
-
|
975
|
-
create_params[oauth_grants_code_challenge_column] = code_challenge
|
976
|
-
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
977
|
-
end
|
978
|
-
|
979
|
-
ds = db[oauth_grants_table]
|
980
|
-
|
981
|
-
rescue_from_uniqueness_error do
|
982
|
-
create_params[oauth_grants_code_column] = oauth_unique_id_generator
|
983
|
-
__insert_and_return__(ds, oauth_grants_id_column, create_params)
|
984
|
-
end
|
985
|
-
create_params[oauth_grants_code_column]
|
986
|
-
end
|
987
|
-
|
988
|
-
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
989
|
-
case param("response_type")
|
990
|
-
when "token"
|
991
|
-
redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
|
992
|
-
|
993
|
-
response_mode ||= "fragment"
|
994
|
-
response_params.replace(_do_authorize_token)
|
995
|
-
when "code"
|
996
|
-
response_mode ||= "query"
|
997
|
-
response_params.replace(_do_authorize_code)
|
998
|
-
when "none"
|
999
|
-
response_mode ||= "none"
|
1000
|
-
when "", nil
|
1001
|
-
response_mode ||= oauth_response_mode
|
1002
|
-
response_params.replace(_do_authorize_code)
|
1003
|
-
end
|
1004
|
-
|
1005
|
-
response_params["state"] = param("state") if param_or_nil("state")
|
1006
|
-
|
1007
|
-
[response_params, response_mode]
|
1008
|
-
end
|
1009
|
-
|
1010
|
-
def _do_authorize_code
|
1011
|
-
{ "code" => create_oauth_grant }
|
1012
|
-
end
|
1013
|
-
|
1014
|
-
def _do_authorize_token
|
1015
|
-
create_params = {
|
1016
|
-
oauth_tokens_account_id_column => account_id,
|
1017
|
-
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
1018
|
-
oauth_tokens_scopes_column => scopes
|
1019
|
-
}
|
1020
|
-
oauth_token = generate_oauth_token(create_params, false)
|
1021
|
-
|
1022
|
-
json_access_token_payload(oauth_token)
|
1023
|
-
end
|
1024
|
-
|
1025
|
-
# Access Tokens
|
1026
|
-
|
1027
|
-
def validate_oauth_token_params
|
1028
|
-
unless (grant_type = param_or_nil("grant_type"))
|
1029
|
-
redirect_response_error("invalid_request")
|
1030
|
-
end
|
1031
|
-
|
1032
|
-
case grant_type
|
1033
|
-
when "authorization_code"
|
1034
|
-
redirect_response_error("invalid_request") unless param_or_nil("code")
|
1035
|
-
|
1036
|
-
when "refresh_token"
|
1037
|
-
redirect_response_error("invalid_request") unless param_or_nil("refresh_token")
|
1038
|
-
else
|
1039
|
-
redirect_response_error("invalid_request")
|
1040
|
-
end
|
1041
|
-
end
|
1042
|
-
|
1043
|
-
def create_oauth_token
|
1044
|
-
case param("grant_type")
|
1045
|
-
when "authorization_code"
|
1046
|
-
# fetch oauth grant
|
1047
|
-
oauth_grant = db[oauth_grants_table].where(
|
1048
|
-
oauth_grants_code_column => param("code"),
|
1049
|
-
oauth_grants_redirect_uri_column => param("redirect_uri"),
|
1050
|
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
1051
|
-
oauth_grants_revoked_at_column => nil
|
1052
|
-
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
1053
|
-
.for_update
|
1054
|
-
.first
|
1055
|
-
|
1056
|
-
redirect_response_error("invalid_grant") unless oauth_grant
|
1057
|
-
|
1058
|
-
create_params = {
|
1059
|
-
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
1060
|
-
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
1061
|
-
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
1062
|
-
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
1063
|
-
}
|
1064
|
-
create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
1065
|
-
when "refresh_token"
|
1066
|
-
# fetch potentially revoked oauth token
|
1067
|
-
oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
|
1068
|
-
|
1069
|
-
if !oauth_token
|
1070
|
-
redirect_response_error("invalid_grant")
|
1071
|
-
elsif oauth_token[oauth_tokens_revoked_at_column]
|
1072
|
-
if oauth_refresh_token_protection_policy == "rotation"
|
1073
|
-
# https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
|
1074
|
-
#
|
1075
|
-
# If a refresh token is compromised and subsequently used by both the attacker and the legitimate
|
1076
|
-
# client, one of them will present an invalidated refresh token, which will inform the authorization
|
1077
|
-
# server of the breach. The authorization server cannot determine which party submitted the invalid
|
1078
|
-
# refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
|
1079
|
-
# forcing the legitimate client to obtain a fresh authorization grant.
|
1080
|
-
|
1081
|
-
db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
|
1082
|
-
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1083
|
-
end
|
1084
|
-
redirect_response_error("invalid_grant")
|
1085
|
-
end
|
1086
|
-
|
1087
|
-
update_params = {
|
1088
|
-
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
1089
|
-
oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
1090
|
-
}
|
1091
|
-
create_oauth_token_from_token(oauth_token, update_params)
|
1092
|
-
end
|
1093
|
-
end
|
1094
|
-
|
1095
|
-
def create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
1096
|
-
# PKCE
|
1097
|
-
if use_oauth_pkce?
|
1098
|
-
if oauth_grant[oauth_grants_code_challenge_column]
|
1099
|
-
code_verifier = param_or_nil("code_verifier")
|
1100
|
-
|
1101
|
-
redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
|
1102
|
-
elsif oauth_require_pkce
|
1103
|
-
redirect_response_error("code_challenge_required")
|
1104
|
-
end
|
1105
|
-
end
|
1106
|
-
|
1107
|
-
# revoke oauth grant
|
1108
|
-
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
1109
|
-
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1110
|
-
|
1111
|
-
should_generate_refresh_token = !use_oauth_access_type? ||
|
1112
|
-
oauth_grant[oauth_grants_access_type_column] == "offline"
|
1113
|
-
|
1114
|
-
generate_oauth_token(create_params, should_generate_refresh_token)
|
1115
|
-
end
|
1116
|
-
|
1117
|
-
def create_oauth_token_from_token(oauth_token, update_params)
|
1118
|
-
redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
|
1119
|
-
|
1120
|
-
rescue_from_uniqueness_error do
|
1121
|
-
oauth_tokens_ds = db[oauth_tokens_table]
|
1122
|
-
token = oauth_unique_id_generator
|
1123
|
-
|
1124
|
-
if oauth_tokens_token_hash_column
|
1125
|
-
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
1126
|
-
else
|
1127
|
-
update_params[oauth_tokens_token_column] = token
|
1128
|
-
end
|
1129
|
-
|
1130
|
-
oauth_token = if oauth_refresh_token_protection_policy == "rotation"
|
1131
|
-
insert_params = {
|
1132
|
-
**update_params,
|
1133
|
-
oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
|
1134
|
-
oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
|
1135
|
-
}
|
1136
|
-
|
1137
|
-
refresh_token = oauth_unique_id_generator
|
1138
|
-
|
1139
|
-
if oauth_tokens_refresh_token_hash_column
|
1140
|
-
insert_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
1141
|
-
else
|
1142
|
-
insert_params[oauth_tokens_refresh_token_column] = refresh_token
|
1143
|
-
end
|
1144
|
-
|
1145
|
-
# revoke the refresh token
|
1146
|
-
oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1147
|
-
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1148
|
-
|
1149
|
-
insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
|
1150
|
-
__insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
|
1151
|
-
else
|
1152
|
-
# includes none
|
1153
|
-
ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1154
|
-
__update_and_return__(ds, update_params)
|
1155
|
-
end
|
1156
|
-
|
1157
|
-
oauth_token[oauth_tokens_token_column] = token
|
1158
|
-
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
1159
|
-
oauth_token
|
1160
|
-
end
|
1161
|
-
end
|
1162
|
-
|
1163
|
-
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
1164
|
-
|
1165
|
-
# Token introspect
|
1166
|
-
|
1167
|
-
def validate_oauth_introspect_params
|
1168
|
-
# check if valid token hint type
|
1169
|
-
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
1170
|
-
redirect_response_error("unsupported_token_type")
|
1171
|
-
end
|
1172
|
-
|
1173
|
-
redirect_response_error("invalid_request") unless param_or_nil("token")
|
1174
|
-
end
|
1175
|
-
|
1176
|
-
def json_token_introspect_payload(token)
|
1177
|
-
return { active: false } unless token
|
1178
|
-
|
1179
|
-
{
|
1180
|
-
active: true,
|
1181
|
-
scope: token[oauth_tokens_scopes_column],
|
1182
|
-
client_id: oauth_application[oauth_applications_client_id_column],
|
1183
|
-
# username
|
1184
|
-
token_type: oauth_token_type,
|
1185
|
-
exp: token[oauth_tokens_expires_in_column].to_i
|
1186
|
-
}
|
1187
|
-
end
|
1188
|
-
|
1189
|
-
# Token revocation
|
1190
|
-
|
1191
|
-
def validate_oauth_revoke_params
|
1192
|
-
# check if valid token hint type
|
1193
|
-
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
1194
|
-
redirect_response_error("unsupported_token_type")
|
1195
|
-
end
|
1196
|
-
|
1197
|
-
redirect_response_error("invalid_request") unless param_or_nil("token")
|
1198
|
-
end
|
1199
|
-
|
1200
|
-
def revoke_oauth_token
|
1201
|
-
token = param("token")
|
1202
|
-
|
1203
|
-
oauth_token = if param("token_type_hint") == "refresh_token"
|
1204
|
-
oauth_token_by_refresh_token(token)
|
1205
|
-
else
|
1206
|
-
oauth_token_by_token(token)
|
1207
|
-
end
|
1208
|
-
|
1209
|
-
redirect_response_error("invalid_request") unless oauth_token
|
1210
|
-
|
1211
|
-
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
1212
|
-
|
1213
|
-
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
1214
|
-
|
1215
|
-
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1216
|
-
|
1217
|
-
oauth_token = __update_and_return__(ds, update_params)
|
1218
|
-
|
1219
|
-
oauth_token[oauth_tokens_token_column] = token
|
1220
|
-
oauth_token
|
1221
|
-
|
1222
|
-
# If the particular
|
1223
|
-
# token is a refresh token and the authorization server supports the
|
1224
|
-
# revocation of access tokens, then the authorization server SHOULD
|
1225
|
-
# also invalidate all access tokens based on the same authorization
|
1226
|
-
# grant
|
1227
|
-
#
|
1228
|
-
# we don't need to do anything here, as we revalidate existing tokens
|
1229
|
-
end
|
1230
|
-
|
1231
|
-
# Response helpers
|
1232
|
-
|
1233
|
-
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
|
1234
|
-
if accepts_json?
|
1235
|
-
status_code = if respond_to?(:"#{error_code}_response_status")
|
1236
|
-
send(:"#{error_code}_response_status")
|
1237
|
-
else
|
1238
|
-
invalid_oauth_response_status
|
1239
|
-
end
|
1240
|
-
|
1241
|
-
throw_json_response_error(status_code, error_code)
|
1242
|
-
else
|
1243
|
-
redirect_url = URI.parse(redirect_url)
|
1244
|
-
query_params = []
|
1245
|
-
|
1246
|
-
query_params << if respond_to?(:"#{error_code}_error_code")
|
1247
|
-
"error=#{send(:"#{error_code}_error_code")}"
|
1248
|
-
else
|
1249
|
-
"error=#{error_code}"
|
1250
|
-
end
|
1251
|
-
|
1252
|
-
if respond_to?(:"#{error_code}_message")
|
1253
|
-
message = send(:"#{error_code}_message")
|
1254
|
-
query_params << ["error_description=#{CGI.escape(message)}"]
|
1255
|
-
end
|
1256
|
-
|
1257
|
-
query_params << redirect_url.query if redirect_url.query
|
1258
|
-
redirect_url.query = query_params.join("&")
|
1259
|
-
redirect(redirect_url.to_s)
|
1260
|
-
end
|
1261
|
-
end
|
1262
|
-
|
1263
|
-
def json_response_success(body, cache = false)
|
1264
|
-
response.status = 200
|
1265
|
-
response["Content-Type"] ||= json_response_content_type
|
1266
|
-
if cache
|
1267
|
-
# defaulting to 1-day for everyone, for now at least
|
1268
|
-
max_age = 60 * 60 * 24
|
1269
|
-
response["Cache-Control"] = "private, max-age=#{max_age}"
|
1270
|
-
else
|
1271
|
-
response["Cache-Control"] = "no-store"
|
1272
|
-
response["Pragma"] = "no-cache"
|
1273
|
-
end
|
1274
|
-
json_payload = _json_response_body(body)
|
1275
|
-
response.write(json_payload)
|
1276
|
-
request.halt
|
1277
|
-
end
|
1278
|
-
|
1279
|
-
def throw_json_response_error(status, error_code)
|
1280
|
-
set_response_error_status(status)
|
1281
|
-
code = if respond_to?(:"#{error_code}_error_code")
|
1282
|
-
send(:"#{error_code}_error_code")
|
1283
|
-
else
|
1284
|
-
error_code
|
1285
|
-
end
|
1286
|
-
payload = { "error" => code }
|
1287
|
-
payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
|
1288
|
-
json_payload = _json_response_body(payload)
|
1289
|
-
response["Content-Type"] ||= json_response_content_type
|
1290
|
-
response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
|
1291
|
-
response.write(json_payload)
|
1292
|
-
request.halt
|
1293
|
-
end
|
1294
|
-
|
1295
|
-
unless method_defined?(:_json_response_body)
|
1296
|
-
def _json_response_body(hash)
|
1297
|
-
if request.respond_to?(:convert_to_json)
|
1298
|
-
request.send(:convert_to_json, hash)
|
1299
|
-
else
|
1300
|
-
JSON.dump(hash)
|
1301
|
-
end
|
1302
|
-
end
|
1303
|
-
end
|
1304
|
-
|
1305
|
-
def authorization_required
|
1306
|
-
if accepts_json?
|
1307
|
-
throw_json_response_error(authorization_required_error_status, "invalid_client")
|
1308
|
-
else
|
1309
|
-
set_redirect_error_flash(require_authorization_error_flash)
|
1310
|
-
redirect(authorize_path)
|
1311
|
-
end
|
1312
|
-
end
|
1313
|
-
|
1314
|
-
def check_valid_uri?(uri)
|
1315
|
-
URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
|
1316
|
-
end
|
1317
|
-
|
1318
|
-
def check_valid_scopes?
|
1319
|
-
return false unless scopes
|
1320
|
-
|
1321
|
-
(scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
|
1322
|
-
end
|
1323
|
-
|
1324
|
-
def check_valid_redirect_uri?
|
1325
|
-
oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
|
1326
|
-
end
|
1327
|
-
|
1328
|
-
ACCESS_TYPES = %w[offline online].freeze
|
1329
|
-
|
1330
|
-
def check_valid_access_type?
|
1331
|
-
return true unless use_oauth_access_type?
|
1332
|
-
|
1333
|
-
access_type = param_or_nil("access_type")
|
1334
|
-
!access_type || ACCESS_TYPES.include?(access_type)
|
1335
|
-
end
|
1336
|
-
|
1337
|
-
APPROVAL_PROMPTS = %w[force auto].freeze
|
1338
|
-
|
1339
|
-
def check_valid_approval_prompt?
|
1340
|
-
return true unless use_oauth_access_type?
|
1341
|
-
|
1342
|
-
approval_prompt = param_or_nil("approval_prompt")
|
1343
|
-
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
|
1344
|
-
end
|
1345
|
-
|
1346
|
-
def check_valid_response_type?
|
1347
|
-
response_type = param_or_nil("response_type")
|
1348
|
-
|
1349
|
-
return true if response_type.nil? || response_type == "code"
|
1350
|
-
|
1351
|
-
return use_oauth_implicit_grant_type? if response_type == "token"
|
1352
|
-
|
1353
|
-
false
|
1354
|
-
end
|
1355
|
-
|
1356
|
-
# PKCE
|
1357
|
-
|
1358
|
-
def validate_pkce_challenge_params
|
1359
|
-
if param_or_nil("code_challenge")
|
1360
|
-
|
1361
|
-
challenge_method = param_or_nil("code_challenge_method")
|
1362
|
-
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
|
1363
|
-
else
|
1364
|
-
return unless oauth_require_pkce
|
1365
|
-
|
1366
|
-
redirect_response_error("code_challenge_required")
|
1367
|
-
end
|
1368
|
-
end
|
1369
|
-
|
1370
|
-
def check_valid_grant_challenge?(grant, verifier)
|
1371
|
-
challenge = grant[oauth_grants_code_challenge_column]
|
1372
|
-
|
1373
|
-
case grant[oauth_grants_code_challenge_method_column]
|
1374
|
-
when "plain"
|
1375
|
-
challenge == verifier
|
1376
|
-
when "S256"
|
1377
|
-
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
|
1378
|
-
generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
|
1379
|
-
|
1380
|
-
challenge == generated_challenge
|
1381
|
-
else
|
1382
|
-
redirect_response_error("unsupported_transform_algorithm")
|
1383
|
-
end
|
1384
|
-
end
|
1385
|
-
|
1386
|
-
# Server metadata
|
1387
|
-
|
1388
|
-
def oauth_server_metadata_body(path)
|
1389
|
-
issuer = base_url
|
1390
|
-
issuer += "/#{path}" if path
|
1391
|
-
|
1392
|
-
responses_supported = %w[code]
|
1393
|
-
response_modes_supported = %w[query form_post]
|
1394
|
-
grant_types_supported = %w[authorization_code]
|
1395
|
-
|
1396
|
-
if use_oauth_implicit_grant_type?
|
1397
|
-
responses_supported << "token"
|
1398
|
-
response_modes_supported << "fragment"
|
1399
|
-
grant_types_supported << "implicit"
|
1400
|
-
end
|
1401
|
-
|
1402
|
-
{
|
1403
|
-
issuer: issuer,
|
1404
|
-
authorization_endpoint: authorize_url,
|
1405
|
-
token_endpoint: token_url,
|
1406
|
-
registration_endpoint: oauth_applications_url,
|
1407
|
-
scopes_supported: oauth_application_scopes,
|
1408
|
-
response_types_supported: responses_supported,
|
1409
|
-
response_modes_supported: response_modes_supported,
|
1410
|
-
grant_types_supported: grant_types_supported,
|
1411
|
-
token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
|
1412
|
-
service_documentation: oauth_metadata_service_documentation,
|
1413
|
-
ui_locales_supported: oauth_metadata_ui_locales_supported,
|
1414
|
-
op_policy_uri: oauth_metadata_op_policy_uri,
|
1415
|
-
op_tos_uri: oauth_metadata_op_tos_uri,
|
1416
|
-
revocation_endpoint: revoke_url,
|
1417
|
-
revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
|
1418
|
-
introspection_endpoint: introspect_url,
|
1419
|
-
introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
|
1420
|
-
code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
|
1421
|
-
}
|
1422
|
-
end
|
5
|
+
depends :oauth_base, :oauth_authorization_code_grant, :oauth_pkce, :oauth_implicit_grant,
|
6
|
+
:oauth_device_grant, :oauth_token_introspection, :oauth_token_revocation,
|
7
|
+
:oauth_application_management, :oauth_token_management
|
1423
8
|
end
|
1424
9
|
end
|