rodauth-oauth 1.6.5 → 1.6.7
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/README.md +9 -1
- data/doc/release_notes/1_6_6.md +25 -0
- data/doc/release_notes/1_6_7.md +17 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +6 -48
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +6 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +3 -1
- data/lib/rodauth/features/oauth_application_management.rb +22 -2
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +2 -2
- data/lib/rodauth/features/oauth_authorize_base.rb +27 -1
- data/lib/rodauth/features/oauth_base.rb +64 -17
- data/lib/rodauth/features/oauth_device_code_grant.rb +5 -0
- data/lib/rodauth/features/oauth_dpop.rb +16 -0
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +71 -33
- data/lib/rodauth/features/oauth_grant_management.rb +6 -0
- data/lib/rodauth/features/oauth_jwt.rb +1 -1
- data/lib/rodauth/features/oauth_jwt_base.rb +56 -13
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +8 -0
- data/lib/rodauth/features/oauth_jwt_jwks.rb +6 -0
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +7 -1
- data/lib/rodauth/features/oauth_pkce.rb +35 -4
- data/lib/rodauth/features/oauth_pushed_authorization_request.rb +6 -0
- data/lib/rodauth/features/oauth_resource_indicators.rb +16 -1
- data/lib/rodauth/features/oauth_resource_server.rb +1 -1
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +8 -3
- data/lib/rodauth/features/oauth_tls_client_auth.rb +14 -2
- data/lib/rodauth/features/oauth_token_revocation.rb +2 -1
- data/lib/rodauth/features/oidc.rb +35 -19
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +1 -1
- data/lib/rodauth/features/oidc_frontchannel_logout.rb +12 -4
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +8 -2
- data/lib/rodauth/features/oidc_self_issued.rb +9 -1
- data/lib/rodauth/oauth/database_extensions.rb +1 -2
- data/lib/rodauth/oauth/ttl_store.rb +3 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +4 -0
- data/locales/pt.yml +4 -0
- data/templates/authorize.str +8 -26
- data/templates/device_verification.str +3 -3
- data/templates/oauth_application.str +4 -1
- metadata +6 -2
|
@@ -12,7 +12,23 @@ module Rodauth
|
|
|
12
12
|
auth_value_method :oauth_applications_registration_access_token_column, :registration_access_token
|
|
13
13
|
auth_value_method :registration_client_uri_route, "register"
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
translatable_method :register_no_client_secret_if_public, "Client secret is not allowed for a public client"
|
|
16
|
+
|
|
17
|
+
if respond_to?(:uses_instance_variables)
|
|
18
|
+
uses_instance_variables(
|
|
19
|
+
:@oauth_application_params,
|
|
20
|
+
:@oauth_application_unrecognized_params
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
PROTECTED_APPLICATION_CREATE_ATTRIBUTES = %w[account_id client_id].freeze
|
|
25
|
+
|
|
26
|
+
PROTECTED_APPLICATION_UPDATE_ATTRIBUTES = %w[
|
|
27
|
+
account_id client_id registration_access_token
|
|
28
|
+
registration_client_uri client_secret_expires_at client_id_issued_at
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
HIDDEN_APPLICATION_ATTRIBUTES = %w[id account_id client_id client_secret client_secret_hash].freeze
|
|
16
32
|
|
|
17
33
|
def load_registration_client_uri_routes
|
|
18
34
|
request.on(registration_client_uri_route) do
|
|
@@ -34,19 +50,23 @@ module Rodauth
|
|
|
34
50
|
json_response_oauth_application(oauth_application)
|
|
35
51
|
end
|
|
36
52
|
request.on method: :put do
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
params = request.params
|
|
54
|
+
|
|
55
|
+
validate_client_registration_params(params, PROTECTED_APPLICATION_UPDATE_ATTRIBUTES)
|
|
56
|
+
|
|
57
|
+
if params.key?("client_secret") && !secret_matches?(oauth_application, params["client_secret"])
|
|
58
|
+
# if the client includes the "client_secret" field in the request, the value of this field MUST match the currently
|
|
59
|
+
# issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with
|
|
60
|
+
# its own chosen value.
|
|
61
|
+
authorization_required
|
|
42
62
|
end
|
|
43
|
-
validate_client_registration_params
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
if params.key?("client_secret")
|
|
65
|
+
next_oauth_application = oauth_application.merge(@oauth_application_params)
|
|
66
|
+
unless confidential?(next_oauth_application)
|
|
67
|
+
register_throw_json_response_error("invalid_client_metadata", register_no_client_secret_if_public)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
50
70
|
|
|
51
71
|
oauth_application = transaction do
|
|
52
72
|
applications_ds = db[oauth_applications_table]
|
|
@@ -80,7 +100,7 @@ module Rodauth
|
|
|
80
100
|
end
|
|
81
101
|
end
|
|
82
102
|
|
|
83
|
-
validate_client_registration_params
|
|
103
|
+
validate_client_registration_params(request.params, PROTECTED_APPLICATION_CREATE_ATTRIBUTES)
|
|
84
104
|
|
|
85
105
|
response_params = transaction do
|
|
86
106
|
before_register
|
|
@@ -108,7 +128,7 @@ module Rodauth
|
|
|
108
128
|
|
|
109
129
|
def _before_register
|
|
110
130
|
raise %{dynamic client registration requires authentication.
|
|
111
|
-
Override
|
|
131
|
+
Override `before_register` to perform it.
|
|
112
132
|
example:
|
|
113
133
|
|
|
114
134
|
before_register do
|
|
@@ -119,8 +139,14 @@ module Rodauth
|
|
|
119
139
|
}
|
|
120
140
|
end
|
|
121
141
|
|
|
122
|
-
def validate_client_registration_params(request_params =
|
|
142
|
+
def validate_client_registration_params(request_params, protected_attributes = PROTECTED_APPLICATION_CREATE_ATTRIBUTES)
|
|
143
|
+
@oauth_application_unrecognized_params = []
|
|
123
144
|
@oauth_application_params = request_params.each_with_object({}) do |(key, value), params|
|
|
145
|
+
if protected_attributes.include?(key)
|
|
146
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
|
|
147
|
+
next
|
|
148
|
+
end
|
|
149
|
+
|
|
124
150
|
case key
|
|
125
151
|
when "redirect_uris"
|
|
126
152
|
if value.is_a?(Array)
|
|
@@ -179,6 +205,10 @@ module Rodauth
|
|
|
179
205
|
end
|
|
180
206
|
end
|
|
181
207
|
key = __send__(:"oauth_applications_#{key}_column")
|
|
208
|
+
when "client_secret"
|
|
209
|
+
# use internal routine which may hash the secret
|
|
210
|
+
set_client_secret(params, value)
|
|
211
|
+
next
|
|
182
212
|
when "jwks"
|
|
183
213
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
|
|
184
214
|
if request_params.key?("jwks_uri")
|
|
@@ -248,13 +278,14 @@ module Rodauth
|
|
|
248
278
|
key = __send__(:"oauth_applications_#{key}_column")
|
|
249
279
|
else
|
|
250
280
|
if respond_to?(:"oauth_applications_#{key}_column")
|
|
251
|
-
if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
|
|
252
|
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
|
|
253
|
-
end
|
|
254
281
|
property = :"oauth_applications_#{key}_column"
|
|
255
282
|
key = __send__(property)
|
|
256
283
|
elsif !db[oauth_applications_table].columns.include?(key.to_sym)
|
|
257
|
-
|
|
284
|
+
# https://datatracker.ietf.org/doc/html/rfc7591#section-3.1
|
|
285
|
+
# "The authorization server MUST ignore any client metadata sent by the client that it
|
|
286
|
+
# does not understand".
|
|
287
|
+
@oauth_application_unrecognized_params << key
|
|
288
|
+
next
|
|
258
289
|
end
|
|
259
290
|
end
|
|
260
291
|
params[key] = value
|
|
@@ -285,6 +316,9 @@ module Rodauth
|
|
|
285
316
|
applications_ds = db[oauth_applications_table]
|
|
286
317
|
application_columns = applications_ds.columns
|
|
287
318
|
|
|
319
|
+
# ignored metadata is not part of the registration, so it does not belong in the response either
|
|
320
|
+
Array(@oauth_application_unrecognized_params).each { |key| return_params.delete(key) }
|
|
321
|
+
|
|
288
322
|
# set defaults
|
|
289
323
|
create_params = @oauth_application_params
|
|
290
324
|
|
|
@@ -310,8 +344,9 @@ module Rodauth
|
|
|
310
344
|
return_params["response_types"] = %w[code]
|
|
311
345
|
"code"
|
|
312
346
|
end
|
|
347
|
+
|
|
313
348
|
rescue_from_uniqueness_error do
|
|
314
|
-
|
|
349
|
+
initialize_create_params(create_params, return_params)
|
|
315
350
|
create_params.delete_if { |k, _| !application_columns.include?(k) }
|
|
316
351
|
applications_ds.insert(create_params)
|
|
317
352
|
end
|
|
@@ -319,27 +354,30 @@ module Rodauth
|
|
|
319
354
|
return_params
|
|
320
355
|
end
|
|
321
356
|
|
|
322
|
-
def
|
|
357
|
+
def initialize_create_params(create_params, return_params)
|
|
323
358
|
client_id = oauth_unique_id_generator
|
|
324
359
|
create_params[oauth_applications_client_id_column] = client_id
|
|
325
360
|
return_params["client_id"] = client_id
|
|
326
|
-
|
|
361
|
+
# https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1
|
|
362
|
+
# "client_id_issued_at" is a number, expressed as seconds since 1970-01-01T00:00:00Z.
|
|
363
|
+
return_params["client_id_issued_at"] = Time.now.to_i
|
|
327
364
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
365
|
+
if create_params.key?(oauth_applications_client_secret_column) && !confidential?(create_params)
|
|
366
|
+
register_throw_json_response_error("invalid_client_metadata", register_no_client_secret_if_public)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
return_params.delete("client_secret")
|
|
370
|
+
|
|
371
|
+
unless (client_secret = create_params.key?(oauth_applications_client_secret_column))
|
|
372
|
+
# do not generate a secret for a confidential client
|
|
373
|
+
return unless confidential?(create_params)
|
|
332
374
|
|
|
333
|
-
if create_params.key?(oauth_applications_client_secret_column)
|
|
334
|
-
set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
|
|
335
|
-
return_params.delete("client_secret")
|
|
336
|
-
else
|
|
337
375
|
client_secret = oauth_unique_id_generator
|
|
338
|
-
set_client_secret(create_params, client_secret)
|
|
339
376
|
return_params["client_secret"] = client_secret
|
|
340
377
|
return_params["client_secret_expires_at"] = 0
|
|
341
|
-
|
|
342
378
|
end
|
|
379
|
+
|
|
380
|
+
set_client_secret(create_params, client_secret)
|
|
343
381
|
end
|
|
344
382
|
|
|
345
383
|
def register_throw_json_response_error(code, message)
|
|
@@ -404,7 +442,7 @@ module Rodauth
|
|
|
404
442
|
params = methods.map { |k| k.to_s[/\Aoauth_applications_(\w+)_column\z/, 1] }.compact
|
|
405
443
|
|
|
406
444
|
body = params.each_with_object({}) do |k, hash|
|
|
407
|
-
next if
|
|
445
|
+
next if HIDDEN_APPLICATION_ATTRIBUTES.include?(k)
|
|
408
446
|
|
|
409
447
|
value = oauth_application[__send__(:"oauth_applications_#{k}_column")]
|
|
410
448
|
|
|
@@ -51,7 +51,7 @@ module Rodauth
|
|
|
51
51
|
def authorization_token
|
|
52
52
|
return super unless oauth_jwt_access_tokens
|
|
53
53
|
|
|
54
|
-
return @authorization_token if
|
|
54
|
+
return @authorization_token if @authorization_token
|
|
55
55
|
|
|
56
56
|
@authorization_token = decode_access_token
|
|
57
57
|
end
|
|
@@ -18,6 +18,11 @@ module Rodauth
|
|
|
18
18
|
|
|
19
19
|
auth_value_method :oauth_jwt_jwe_copyright, nil
|
|
20
20
|
|
|
21
|
+
# Clock-skew leeway, in seconds, tolerated when rejecting a JWT whose iat (issued-at) is in the
|
|
22
|
+
# future. iat is informational per RFC 7519 section 4.1.6, so this only guards against tokens
|
|
23
|
+
# issued well ahead of now. Applies to the json-jwt verification path (see jwt_decode).
|
|
24
|
+
auth_value_method :oauth_jwt_iat_leeway, 30
|
|
25
|
+
|
|
21
26
|
auth_methods(
|
|
22
27
|
:jwt_encode,
|
|
23
28
|
:jwt_decode,
|
|
@@ -28,6 +33,14 @@ module Rodauth
|
|
|
28
33
|
:resource_owner_params_from_jwt_claims
|
|
29
34
|
)
|
|
30
35
|
|
|
36
|
+
if respond_to?(:uses_instance_variables)
|
|
37
|
+
uses_instance_variables(
|
|
38
|
+
:@oauth_jwt_issuer,
|
|
39
|
+
:@oauth_jwt_audience,
|
|
40
|
+
:@_jwt_key
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
31
44
|
private
|
|
32
45
|
|
|
33
46
|
def oauth_jwt_issuer
|
|
@@ -266,15 +279,28 @@ module Rodauth
|
|
|
266
279
|
end
|
|
267
280
|
|
|
268
281
|
now = Time.now
|
|
269
|
-
if verify_claims
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
if verify_claims
|
|
283
|
+
# Each line asserts that a claim is acceptable: absent / not checked, or valid. The token
|
|
284
|
+
# is rejected unless all of them hold. These are deliberately PASS conditions joined with
|
|
285
|
+
# `&&` -- the inverse (failure conditions joined with `&&`) only rejects when every check
|
|
286
|
+
# fails at once, which was the original verification bug. exp/nbf/iat are optional and
|
|
287
|
+
# only enforced when present (e.g. DPoP proofs carry no exp).
|
|
288
|
+
#
|
|
289
|
+
# iat is informational per RFC 7519 section 4.1.6 (RFC 9068 section 4 requires it be present,
|
|
290
|
+
# not verified), so we only reject tokens issued in the future, tolerating
|
|
291
|
+
# oauth_jwt_iat_leeway seconds of clock skew. Both backends now honor oauth_jwt_iat_leeway:
|
|
292
|
+
# the ruby-jwt path disables verify_iat (which removed iat_leeway in 2.2.0, jwt/ruby-jwt#319)
|
|
293
|
+
# and performs the same leeway'd future-iat check manually, so behavior is unified.
|
|
294
|
+
# A non-numeric iat is malformed (NumericDate per RFC 7519); reject it rather than letting
|
|
295
|
+
# Time.at raise, mirroring the ruby-jwt path so both backends reject malformed iat alike.
|
|
296
|
+
claims_valid = (!claims[:exp] || Time.at(claims[:exp]) >= now) &&
|
|
297
|
+
(!claims[:nbf] || Time.at(claims[:nbf]) <= now) &&
|
|
298
|
+
(!claims[:iat] || (claims[:iat].is_a?(Numeric) && Time.at(claims[:iat]) <= now + oauth_jwt_iat_leeway)) &&
|
|
299
|
+
(!verify_iss || claims[:iss] == oauth_jwt_issuer) &&
|
|
300
|
+
(!verify_aud || verify_aud(claims[:aud], claims[:client_id])) &&
|
|
301
|
+
(!verify_jti || verify_jti(claims[:jti], claims))
|
|
302
|
+
|
|
303
|
+
return unless claims_valid
|
|
278
304
|
end
|
|
279
305
|
|
|
280
306
|
return if verify_headers && !verify_headers.call(claims.header)
|
|
@@ -413,7 +439,12 @@ module Rodauth
|
|
|
413
439
|
#
|
|
414
440
|
# issuer: check that server generated the token
|
|
415
441
|
# aud: check the audience field (client is who he says he is)
|
|
416
|
-
#
|
|
442
|
+
# exp: check that the token hasn't expired
|
|
443
|
+
# iat: informational per RFC 7519 section 4.1.6, so we only reject a future iat, tolerating
|
|
444
|
+
# oauth_jwt_iat_leeway seconds of clock skew. ruby-jwt's verify_iat offers no leeway knob
|
|
445
|
+
# (iat_leeway was removed in 2.2.0, jwt/ruby-jwt#319), so verify_iat is disabled below and
|
|
446
|
+
# iat is checked manually -- unifying behavior with the json-jwt path, which honors the
|
|
447
|
+
# same oauth_jwt_iat_leeway.
|
|
417
448
|
#
|
|
418
449
|
# subject can't be verified automatically without having access to the account id,
|
|
419
450
|
# which we don't because that's the whole point.
|
|
@@ -425,7 +456,9 @@ module Rodauth
|
|
|
425
456
|
# can't use stock aud verification, as it's dependent on the client application id
|
|
426
457
|
verify_aud: false,
|
|
427
458
|
verify_jti: (verify_jti ? method(:verify_jti) : false),
|
|
428
|
-
|
|
459
|
+
# iat is verified manually below (with leeway) rather than via
|
|
460
|
+
# ruby-jwt's verify_iat, which rejects a future iat with no leeway.
|
|
461
|
+
verify_iat: false
|
|
429
462
|
}
|
|
430
463
|
else
|
|
431
464
|
{}
|
|
@@ -456,6 +489,16 @@ module Rodauth
|
|
|
456
489
|
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params)
|
|
457
490
|
end
|
|
458
491
|
|
|
492
|
+
# iat is only rejected when issued in the future, tolerating oauth_jwt_iat_leeway seconds of
|
|
493
|
+
# clock skew -- mirroring the json-jwt path so both backends behave identically. ruby-jwt
|
|
494
|
+
# claims are string-keyed. Since verify_iat is disabled above, ruby-jwt no longer validates
|
|
495
|
+
# that iat is a NumericDate, so reject a non-numeric iat here rather than letting Time.at
|
|
496
|
+
# raise an error the JWT::DecodeError handler would not catch.
|
|
497
|
+
if verify_claims && (iat = claims["iat"])
|
|
498
|
+
return unless iat.is_a?(Numeric)
|
|
499
|
+
return if Time.at(iat) > Time.now + oauth_jwt_iat_leeway
|
|
500
|
+
end
|
|
501
|
+
|
|
459
502
|
return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
|
|
460
503
|
|
|
461
504
|
return if verify_headers && !verify_headers.call(headers)
|
|
@@ -496,7 +539,7 @@ module Rodauth
|
|
|
496
539
|
JWT.decode(token, nil, false)
|
|
497
540
|
end
|
|
498
541
|
else
|
|
499
|
-
# :
|
|
542
|
+
# simplecov:enable
|
|
500
543
|
def jwk_export(_key)
|
|
501
544
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
|
502
545
|
end
|
|
@@ -520,7 +563,7 @@ module Rodauth
|
|
|
520
563
|
def private_jwk?(_jwk)
|
|
521
564
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
|
522
565
|
end
|
|
523
|
-
# :
|
|
566
|
+
# simplecov:disable
|
|
524
567
|
end
|
|
525
568
|
end
|
|
526
569
|
end
|
|
@@ -28,6 +28,14 @@ module Rodauth
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
+
def oauth_confidential_token_endpoint_auth_methods
|
|
32
|
+
if oauth_applications_client_secret_hash_column.nil?
|
|
33
|
+
super | %w[client_secret_jwt private_key_jwt urn:ietf:params:oauth:client-assertion-type:jwt-bearer]
|
|
34
|
+
else
|
|
35
|
+
super | %w[private_key_jwt]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
|
|
32
40
|
claims = jwt_assertion(assertion)
|
|
33
41
|
|
|
@@ -23,6 +23,12 @@ module Rodauth
|
|
|
23
23
|
|
|
24
24
|
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
|
|
25
25
|
|
|
26
|
+
if respond_to?(:uses_instance_variables)
|
|
27
|
+
uses_instance_variables(
|
|
28
|
+
:@require_signed_request_object
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
26
32
|
private
|
|
27
33
|
|
|
28
34
|
# /authorize
|
|
@@ -93,7 +99,7 @@ module Rodauth
|
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
def require_signed_request_object?
|
|
96
|
-
return @require_signed_request_object if
|
|
102
|
+
return @require_signed_request_object if @require_signed_request_object
|
|
97
103
|
|
|
98
104
|
@require_signed_request_object = (oauth_application[oauth_applications_require_signed_request_object_column] if oauth_application)
|
|
99
105
|
@require_signed_request_object = oauth_require_signed_request_object if @require_signed_request_object.nil?
|
|
@@ -9,6 +9,11 @@ module Rodauth
|
|
|
9
9
|
auth_value_method :oauth_require_pkce, true
|
|
10
10
|
auth_value_method :oauth_pkce_challenge_method, "S256"
|
|
11
11
|
|
|
12
|
+
# TODO: change to false in a major version bump
|
|
13
|
+
# RFC 7636 §4.2 discourages the "plain" transform: it transmits the raw code_verifier,
|
|
14
|
+
# so an intercepted authorization request exposes it and defeats PKCE.
|
|
15
|
+
auth_value_method :oauth_pkce_allow_plain_method, true
|
|
16
|
+
|
|
12
17
|
auth_value_method :oauth_grants_code_challenge_column, :code_challenge
|
|
13
18
|
auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
|
|
14
19
|
|
|
@@ -17,6 +22,16 @@ module Rodauth
|
|
|
17
22
|
auth_value_method :oauth_unsupported_transform_algorithm_error_code, "invalid_request"
|
|
18
23
|
translatable_method :oauth_unsupported_transform_algorithm_message, "transform algorithm not supported"
|
|
19
24
|
|
|
25
|
+
def authorize_form_params
|
|
26
|
+
super.tap do |params|
|
|
27
|
+
%w[code_challenge code_challenge_method].each do |param_name|
|
|
28
|
+
if (param_value = param_or_nil(param_name))
|
|
29
|
+
params << { "name" => param_name, "value" => param_value, "type" => "hidden" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
20
35
|
private
|
|
21
36
|
|
|
22
37
|
def supports_auth_method?(oauth_application, auth_method)
|
|
@@ -61,7 +76,13 @@ module Rodauth
|
|
|
61
76
|
if param_or_nil("code_challenge")
|
|
62
77
|
|
|
63
78
|
challenge_method = param_or_nil("code_challenge_method")
|
|
64
|
-
|
|
79
|
+
|
|
80
|
+
# Reject the weak "plain" transform unless it has been explicitly opted into. This runs
|
|
81
|
+
# before the supported-method check so the disabled method surfaces a meaningful error
|
|
82
|
+
# instead of the generic "code challenge required".
|
|
83
|
+
redirect_response_error("unsupported_transform_algorithm") if challenge_method == "plain" && !oauth_pkce_allow_plain_method
|
|
84
|
+
|
|
85
|
+
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_methods.include?(challenge_method)
|
|
65
86
|
else
|
|
66
87
|
return unless oauth_require_pkce
|
|
67
88
|
|
|
@@ -74,19 +95,29 @@ module Rodauth
|
|
|
74
95
|
|
|
75
96
|
case grant[oauth_grants_code_challenge_method_column]
|
|
76
97
|
when "plain"
|
|
77
|
-
|
|
98
|
+
# A grant stored with the weak "plain" method must not be redeemable unless the
|
|
99
|
+
# method has been explicitly enabled, otherwise PKCE is silently downgraded.
|
|
100
|
+
redirect_response_error("unsupported_transform_algorithm") unless oauth_pkce_allow_plain_method
|
|
101
|
+
timing_safe_eql?(verifier.to_s, challenge.to_s)
|
|
78
102
|
when "S256"
|
|
79
103
|
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
|
80
104
|
|
|
81
|
-
challenge
|
|
105
|
+
timing_safe_eql?(generated_challenge, challenge.to_s)
|
|
82
106
|
else
|
|
83
107
|
redirect_response_error("unsupported_transform_algorithm")
|
|
84
108
|
end
|
|
85
109
|
end
|
|
86
110
|
|
|
111
|
+
# The PKCE transform methods the server accepts. "S256" is always supported; the weak "plain"
|
|
112
|
+
# transform is only included when explicitly opted into via oauth_pkce_allow_plain_method.
|
|
113
|
+
def oauth_pkce_challenge_methods
|
|
114
|
+
oauth_pkce_allow_plain_method ? %w[S256 plain] : %w[S256]
|
|
115
|
+
end
|
|
116
|
+
|
|
87
117
|
def oauth_server_metadata_body(*)
|
|
88
118
|
super.tap do |data|
|
|
89
|
-
|
|
119
|
+
# RFC 8414: code_challenge_methods_supported is a JSON array of the supported methods.
|
|
120
|
+
data[:code_challenge_methods_supported] = oauth_pkce_challenge_methods
|
|
90
121
|
end
|
|
91
122
|
end
|
|
92
123
|
end
|
|
@@ -19,6 +19,12 @@ module Rodauth
|
|
|
19
19
|
auth_value_method :"oauth_pushed_authorization_requests_#{column}_column", column
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
if respond_to?(:uses_instance_variables)
|
|
23
|
+
uses_instance_variables(
|
|
24
|
+
:@require_signed_request_object
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
# /par
|
|
23
29
|
auth_server_route(:par) do |r|
|
|
24
30
|
require_oauth_application
|
|
@@ -8,8 +8,14 @@ module Rodauth
|
|
|
8
8
|
|
|
9
9
|
auth_value_method :oauth_grants_resource_column, :resource
|
|
10
10
|
|
|
11
|
+
if respond_to?(:uses_instance_variables)
|
|
12
|
+
uses_instance_variables(
|
|
13
|
+
:@resource_indicators
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
def resource_indicators
|
|
12
|
-
return @resource_indicators if
|
|
18
|
+
return @resource_indicators if @resource_indicators
|
|
13
19
|
|
|
14
20
|
resources = param_or_nil("resource")
|
|
15
21
|
|
|
@@ -46,6 +52,15 @@ module Rodauth
|
|
|
46
52
|
authorization_required unless token_indicators.any? { |resource| base_url.start_with?(resource) }
|
|
47
53
|
end
|
|
48
54
|
|
|
55
|
+
def authorize_form_params
|
|
56
|
+
super.tap do |params|
|
|
57
|
+
# the "resource" param may be repeated, so it carries one entry per resource indicator
|
|
58
|
+
Array(resource_indicators).each do |resource|
|
|
59
|
+
params << { "name" => "resource", "value" => resource, "type" => "hidden" }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
49
64
|
private
|
|
50
65
|
|
|
51
66
|
def validate_token_params
|
|
@@ -34,6 +34,13 @@ module Rodauth
|
|
|
34
34
|
:account_from_saml2_bearer_assertion
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
if respond_to?(:uses_instance_variables)
|
|
38
|
+
uses_instance_variables(
|
|
39
|
+
:@saml_settings,
|
|
40
|
+
:@assertion
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
def oauth_grant_types_supported
|
|
38
45
|
super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
|
|
39
46
|
end
|
|
@@ -98,9 +105,8 @@ module Rodauth
|
|
|
98
105
|
settings
|
|
99
106
|
end
|
|
100
107
|
|
|
101
|
-
# rubocop:disable Naming/MemoizedInstanceVariableName
|
|
102
108
|
def parse_saml_assertion(assertion)
|
|
103
|
-
return @assertion if
|
|
109
|
+
return @assertion if @assertion
|
|
104
110
|
|
|
105
111
|
response = OneLogin::RubySaml::Response.new(assertion)
|
|
106
112
|
|
|
@@ -129,7 +135,6 @@ module Rodauth
|
|
|
129
135
|
|
|
130
136
|
@assertion = response
|
|
131
137
|
end
|
|
132
|
-
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
133
138
|
|
|
134
139
|
def oauth_server_metadata_body(*)
|
|
135
140
|
super.tap do |data|
|
|
@@ -21,12 +21,24 @@ module Rodauth
|
|
|
21
21
|
|
|
22
22
|
auth_value_method :oauth_grants_certificate_thumbprint_column, :certificate_thumbprint
|
|
23
23
|
|
|
24
|
+
if respond_to?(:uses_instance_variables)
|
|
25
|
+
uses_instance_variables(
|
|
26
|
+
:@client_certificate,
|
|
27
|
+
:@certificate,
|
|
28
|
+
:@client_certificate_sans
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
24
32
|
def oauth_token_endpoint_auth_methods_supported
|
|
25
33
|
super | %w[tls_client_auth self_signed_tls_client_auth]
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
private
|
|
29
37
|
|
|
38
|
+
def oauth_confidential_token_endpoint_auth_methods
|
|
39
|
+
super | %w[tls_client_auth self_signed_tls_client_auth]
|
|
40
|
+
end
|
|
41
|
+
|
|
30
42
|
def validate_token_params
|
|
31
43
|
# For all requests to the authorization server utilizing mutual-TLS client authentication,
|
|
32
44
|
# the client MUST include the client_id parameter
|
|
@@ -124,7 +136,7 @@ module Rodauth
|
|
|
124
136
|
end
|
|
125
137
|
|
|
126
138
|
def client_certificate
|
|
127
|
-
return @client_certificate if
|
|
139
|
+
return @client_certificate if @client_certificate
|
|
128
140
|
|
|
129
141
|
unless (pem_cert = request.env["SSL_CLIENT_CERT"] || request.env["HTTP_SSL_CLIENT_CERT"] || request.env["HTTP_X_SSL_CLIENT_CERT"])
|
|
130
142
|
return
|
|
@@ -136,7 +148,7 @@ module Rodauth
|
|
|
136
148
|
end
|
|
137
149
|
|
|
138
150
|
def client_certificate_sans
|
|
139
|
-
return @client_certificate_sans if
|
|
151
|
+
return @client_certificate_sans if @client_certificate_sans
|
|
140
152
|
|
|
141
153
|
@client_certificate_sans =
|
|
142
154
|
if client_certificate
|
|
@@ -117,7 +117,8 @@ module Rodauth
|
|
|
117
117
|
def oauth_server_metadata_body(*)
|
|
118
118
|
super.tap do |data|
|
|
119
119
|
data[:revocation_endpoint] = revoke_url
|
|
120
|
-
|
|
120
|
+
# revocation_endpoint_auth_methods_supported must be absent: it defaults to client_secret_basic
|
|
121
|
+
data.delete(:revocation_endpoint_auth_methods_supported)
|
|
121
122
|
end
|
|
122
123
|
end
|
|
123
124
|
end
|