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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -1
  3. data/doc/release_notes/1_6_6.md +25 -0
  4. data/doc/release_notes/1_6_7.md +17 -0
  5. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +6 -48
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +6 -0
  7. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +3 -1
  8. data/lib/rodauth/features/oauth_application_management.rb +22 -2
  9. data/lib/rodauth/features/oauth_authorization_code_grant.rb +2 -2
  10. data/lib/rodauth/features/oauth_authorize_base.rb +27 -1
  11. data/lib/rodauth/features/oauth_base.rb +64 -17
  12. data/lib/rodauth/features/oauth_device_code_grant.rb +5 -0
  13. data/lib/rodauth/features/oauth_dpop.rb +16 -0
  14. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +71 -33
  15. data/lib/rodauth/features/oauth_grant_management.rb +6 -0
  16. data/lib/rodauth/features/oauth_jwt.rb +1 -1
  17. data/lib/rodauth/features/oauth_jwt_base.rb +56 -13
  18. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +8 -0
  19. data/lib/rodauth/features/oauth_jwt_jwks.rb +6 -0
  20. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +7 -1
  21. data/lib/rodauth/features/oauth_pkce.rb +35 -4
  22. data/lib/rodauth/features/oauth_pushed_authorization_request.rb +6 -0
  23. data/lib/rodauth/features/oauth_resource_indicators.rb +16 -1
  24. data/lib/rodauth/features/oauth_resource_server.rb +1 -1
  25. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +8 -3
  26. data/lib/rodauth/features/oauth_tls_client_auth.rb +14 -2
  27. data/lib/rodauth/features/oauth_token_revocation.rb +2 -1
  28. data/lib/rodauth/features/oidc.rb +35 -19
  29. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +1 -1
  30. data/lib/rodauth/features/oidc_frontchannel_logout.rb +12 -4
  31. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +8 -2
  32. data/lib/rodauth/features/oidc_self_issued.rb +9 -1
  33. data/lib/rodauth/oauth/database_extensions.rb +1 -2
  34. data/lib/rodauth/oauth/ttl_store.rb +3 -3
  35. data/lib/rodauth/oauth/version.rb +1 -1
  36. data/locales/en.yml +4 -0
  37. data/locales/pt.yml +4 -0
  38. data/templates/authorize.str +8 -26
  39. data/templates/device_verification.str +3 -3
  40. data/templates/oauth_application.str +4 -1
  41. 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
- PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
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
- %w[client_id registration_access_token registration_client_uri client_secret_expires_at
38
- client_id_issued_at].each do |prohibited_param|
39
- if request.params.key?(prohibited_param)
40
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(prohibited_param))
41
- end
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
- # if the client includes the "client_secret" field in the request, the value of this field MUST match the currently
46
- # issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with
47
- # its own chosen value.
48
- authorization_required if request.params.key?("client_secret") && secret_matches?(oauth_application,
49
- request.params["client_secret"])
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 ´before_register` to perform it.
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 = 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
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
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
- initialize_register_params(create_params, return_params)
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 initialize_register_params(create_params, return_params)
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
- return_params["client_id_issued_at"] = Time.now.utc.iso8601
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
- registration_access_token = oauth_unique_id_generator
329
- create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
330
- return_params["registration_access_token"] = registration_access_token
331
- return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
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 %w[id account_id client_id client_secret cliennt_secret_hash].include?(k)
445
+ next if HIDDEN_APPLICATION_ATTRIBUTES.include?(k)
408
446
 
409
447
  value = oauth_application[__send__(:"oauth_applications_#{k}_column")]
410
448
 
@@ -25,6 +25,12 @@ module Rodauth
25
25
  :oauth_grant_path
26
26
  )
27
27
 
28
+ if respond_to?(:uses_instance_variables)
29
+ uses_instance_variables(
30
+ :@oauth_grants
31
+ )
32
+ end
33
+
28
34
  def oauth_grants_path(opts = {})
29
35
  route_path(oauth_grants_route, opts)
30
36
  end
@@ -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 defined?(@authorization_token)
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
- (!claims[:exp] || Time.at(claims[:exp]) < now) &&
271
- claims[:nbf] && Time.at(claims[:nbf]) < now &&
272
- claims[:iat] && Time.at(claims[:iat]) < now &&
273
- verify_iss && claims[:iss] != oauth_jwt_issuer &&
274
- verify_aud && !verify_aud(claims[:aud], claims[:client_id]) &&
275
- verify_jti && !verify_jti(claims[:jti], claims)
276
-
277
- return
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
- # iat: check that the token didn't expire
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
- verify_iat: true
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
- # :nocov:
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
- # :nocov:
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
 
@@ -17,6 +17,12 @@ module Rodauth
17
17
  end
18
18
  end
19
19
 
20
+ if respond_to?(:uses_instance_variables)
21
+ uses_instance_variables(
22
+ :@jwks_set
23
+ )
24
+ end
25
+
20
26
  private
21
27
 
22
28
  def oauth_server_metadata_body(path = nil)
@@ -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 defined?(@require_signed_request_object)
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
- redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
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
- challenge == verifier
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 == generated_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
- data[:code_challenge_methods_supported] = oauth_pkce_challenge_method
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 defined?(@resource_indicators)
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
@@ -13,7 +13,7 @@ module Rodauth
13
13
  )
14
14
 
15
15
  def authorization_token
16
- return @authorization_token if defined?(@authorization_token)
16
+ return @authorization_token if @authorization_token
17
17
 
18
18
  # check if there is a token
19
19
  access_token = fetch_access_token
@@ -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 defined?(@assertion)
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 defined?(@client_certificate)
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 defined?(@client_certificate_sans)
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
- data[:revocation_endpoint_auth_methods_supported] = nil # because it's client_secret_basic
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