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