omniauth_openid_federation 1.2.2 → 1.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.
@@ -10,6 +10,8 @@ require "digest"
10
10
  require_relative "string_helpers"
11
11
  require_relative "logger"
12
12
  require_relative "errors"
13
+ require_relative "constants"
14
+ require_relative "configuration"
13
15
  require_relative "validators"
14
16
  require_relative "http_client"
15
17
  require_relative "jws"
@@ -38,8 +40,8 @@ require_relative "federation/metadata_policy_merger"
38
40
  # - Metadata Policy Merging (Section 5.1) - Applies metadata policies from trust chain
39
41
  # - Automatic Client Registration (Section 11.1) - Uses Entity ID as client_id
40
42
  #
41
- # Features NOT implemented (provider-specific or optional):
42
- # - Trust marks (Section 7) - Provider-specific feature (parsed but not validated)
43
+ # Features NOT implemented (optional):
44
+ # - Trust marks (Section 7) - Optional feature (parsed but not validated)
43
45
  # - Federation endpoints (Section 8) - Server-side feature (Fetch Endpoint implemented separately)
44
46
  #
45
47
  # This strategy uses the openid_connect gem and extends it with federation-specific features.
@@ -65,7 +67,6 @@ module OmniAuth
65
67
  option :client_auth_method, :jwt_bearer
66
68
  option :client_signing_alg, :RS256
67
69
  option :audience, nil # Audience for JWT request objects (defaults to token_endpoint)
68
- option :acr_values, nil # Authentication Context Class Reference values (space-separated string or array)
69
70
  option :fetch_userinfo, true # Whether to fetch userinfo endpoint (default: true for backward compatibility, set to false if ID token contains all needed data)
70
71
  option :key_source, :local # Key source: :local (use local static private_key) or :federation (use federation/JWKS) - used as default for both signing and decryption
71
72
  option :signing_key_source, nil # Signing key source: :local, :federation, or nil (uses key_source)
@@ -82,6 +83,8 @@ module OmniAuth
82
83
  option :client_jwk_signing_key, nil # Client JWKS for token endpoint authentication (auto-extracted from client entity statement if available)
83
84
  option :trust_anchors, [] # Array of Trust Anchor configurations for trust chain resolution: [{entity_id: "...", jwks: {...}}]
84
85
  option :enable_trust_chain_resolution, true # Enable trust chain resolution when issuer/client_id is an Entity ID
86
+ option :request_object_params, nil # Array of parameter names to include in signed request object from request.params (allow-list)
87
+ option :prepare_request_object_params, nil # Proc to modify params before adding to signed request object: proc { |params| modified_params }
85
88
 
86
89
  # Override client_jwk_signing_key to automatically extract from client entity statement
87
90
  # This automates client JWKS extraction according to OpenID Federation spec
@@ -263,23 +266,42 @@ module OmniAuth
263
266
  super
264
267
  end
265
268
 
266
- # Override request_phase to use our custom authorize_uri instead of client.auth_code
267
- # The base OAuth2 strategy calls client.auth_code.authorize_url, but OpenIDConnect::Client
268
- # doesn't have an auth_code method - it uses authorization_uri directly
269
- #
270
- # ENFORCEMENT: This method ALWAYS uses signed request objects (required for security)
271
- # The authorize_uri method enforces this requirement - unsigned requests are NOT allowed
269
+ # Override request_phase to use signed request objects (RFC 9101)
272
270
  def request_phase
273
271
  redirect authorize_uri
274
272
  end
275
273
 
276
- # Override callback_phase to bypass base OAuth2 strategy's auth_code call
277
- # The base OAuth2 strategy tries to call client.auth_code.get_token, but OpenIDConnect::Client
278
- # doesn't have an auth_code method - we handle token exchange using oidc_client.access_token!
274
+ # Override callback_phase to handle token exchange with OpenIDConnect::Client
279
275
  def callback_phase
280
- # Validate state parameter (CSRF protection)
281
- # Use constant-time comparison to prevent timing attacks
282
- state_param = request.params["state"]
276
+ # Security: Validate user input from HTTP request
277
+ state_param_raw = request.params["state"]
278
+ code_param_raw = request.params["code"]
279
+ error_param_raw = request.params["error"]
280
+ error_description_raw = request.params["error_description"]
281
+
282
+ state_param = state_param_raw ? OmniauthOpenidFederation::Validators.sanitize_request_param(state_param_raw) : nil
283
+ code_param = code_param_raw ? OmniauthOpenidFederation::Validators.sanitize_request_param(code_param_raw) : nil
284
+ error_param = error_param_raw ? OmniauthOpenidFederation::Validators.sanitize_request_param(error_param_raw) : nil
285
+ error_description_param = error_description_raw ? OmniauthOpenidFederation::Validators.sanitize_request_param(error_description_raw) : nil
286
+ if error_param
287
+ error_msg = "Authorization error: #{error_param}"
288
+ error_msg += " - #{error_description_param}" if error_description_param
289
+ OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
290
+ stage: "callback_phase",
291
+ error_message: error_msg,
292
+ error_class: "AuthorizationError",
293
+ request_info: {
294
+ remote_ip: request.env["REMOTE_ADDR"],
295
+ user_agent: request.env["HTTP_USER_AGENT"],
296
+ path: request.path
297
+ }
298
+ )
299
+ env["omniauth_openid_federation.instrumented"] = true
300
+ fail!(:authorization_error, OmniauthOpenidFederation::ValidationError.new(error_msg))
301
+ return
302
+ end
303
+
304
+ # CSRF protection: constant-time state comparison
283
305
  state_session = session["omniauth.state"]
284
306
 
285
307
  if OmniauthOpenidFederation::StringHelpers.blank?(state_param) ||
@@ -304,8 +326,7 @@ module OmniAuth
304
326
  # Clear state from session
305
327
  session.delete("omniauth.state")
306
328
 
307
- # Validate authorization code is present
308
- if OmniauthOpenidFederation::StringHelpers.blank?(request.params["code"])
329
+ if OmniauthOpenidFederation::StringHelpers.blank?(code_param)
309
330
  # Instrument unexpected authentication break
310
331
  OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
311
332
  stage: "callback_phase",
@@ -323,10 +344,8 @@ module OmniAuth
323
344
  return
324
345
  end
325
346
 
326
- # Exchange authorization code for access token using OpenID Connect client
327
- # This bypasses the base OAuth2 strategy's client.auth_code.get_token call
328
347
  begin
329
- @access_token = exchange_authorization_code(request.params["code"])
348
+ @access_token = exchange_authorization_code(code_param)
330
349
  rescue => e
331
350
  # Instrument unexpected authentication break
332
351
  OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
@@ -345,24 +364,11 @@ module OmniAuth
345
364
  return
346
365
  end
347
366
 
348
- # Build auth hash manually since we bypassed the base strategy's token handling
349
- # The base OAuth2 strategy's auth_hash expects @access_token.token, but OpenIDConnect::AccessToken
350
- # uses @access_token.access_token, so we need to build it ourselves
351
367
  env["omniauth.auth"] = auth_hash
352
-
353
- # Continue with OmniAuth flow
354
368
  call_app!
355
369
  end
356
370
 
357
- # Override auth_hash to work with OpenIDConnect::AccessToken
358
- # The base OAuth2 strategy expects @access_token.token, but OpenIDConnect::AccessToken uses access_token
359
- # We build the hash directly to avoid calling the base strategy's auth_hash which will fail
360
371
  def auth_hash
361
- # Ensure provider name is always "openid_federation"
362
- # The name option should be set, but fallback to "openid_federation" if not
363
- # Check both symbol and string keys, and also check the name method
364
- options[:name] || options["name"] || (respond_to?(:name) && name) || "openid_federation"
365
- # Always use "openid_federation" as the provider name for consistency
366
372
  OmniAuth::AuthHash.new(
367
373
  provider: "openid_federation",
368
374
  uid: uid,
@@ -378,39 +384,56 @@ module OmniAuth
378
384
  end
379
385
 
380
386
  def authorize_uri
381
- # In OmniAuth strategies, use request.params instead of params
382
387
  request_params = request.params
383
388
 
384
- # Combine configured ACR values with request ACR values
385
- # This allows flexibility: configure assurance level (e.g., level4) at gem level,
386
- # while allowing components to specify provider (e.g., oidc.provider.1)
387
- options.acr_values = combine_acr_values(
388
- configured_acr: options.acr_values,
389
- request_acr: request_params["acr_values"]
390
- )
389
+ # Security: Only validate user input from HTTP requests, not config values
390
+ # Note: Rack params can return arrays for multi-value parameters
391
+ sanitized_params = {}
392
+ request_params.each do |key, value|
393
+ next unless value
394
+ key_str = key.to_s
395
+ next if key_str.length > 256
396
+ # For arrays (multi-value params), sanitize each element and limit size
397
+ if value.is_a?(Array)
398
+ # Prevent DoS: limit array size
399
+ if value.length > 100
400
+ next
401
+ end
402
+ # Sanitize each element
403
+ sanitized_array = value.map { |v| OmniauthOpenidFederation::Validators.sanitize_request_param(v) }.compact
404
+ next if sanitized_array.empty?
405
+ # Keep as array for acr_values (handled by normalize_acr_values)
406
+ # Convert to space-separated string for other parameters (ui_locales, claims_locales)
407
+ sanitized_params[key_str] = if key_str == "acr_values"
408
+ sanitized_array
409
+ else
410
+ sanitized_array.join(" ")
411
+ end
412
+ else
413
+ sanitized = OmniauthOpenidFederation::Validators.sanitize_request_param(value)
414
+ sanitized_params[key_str] = sanitized if sanitized
415
+ end
416
+ end
417
+ request_params = sanitized_params
418
+
419
+ # Apply custom proc to modify params before adding to signed request object
420
+ if options.prepare_request_object_params.respond_to?(:call)
421
+ request_params = options.prepare_request_object_params.call(request_params.dup) || request_params
422
+ request_params = {} unless request_params.is_a?(Hash)
423
+ end
391
424
 
392
- # ENFORCE signed request objects - Required for secure authorization requests
393
- # All authentication requests MUST use signed request objects
394
- # This implementation enforces this requirement - unsigned requests are NOT allowed
425
+ # Enforce signed request objects (RFC 9101) - unsigned requests are not allowed
395
426
  client_options_hash = options.client_options || {}
396
427
  normalized_options = OmniauthOpenidFederation::Validators.normalize_hash(client_options_hash)
397
428
  private_key = normalized_options[:private_key]
398
-
399
- # Validate that private key is present (required for signing)
400
- # This ensures signed request objects are ALWAYS used - no bypass possible
401
429
  OmniauthOpenidFederation::Validators.validate_private_key!(private_key)
402
430
 
403
- # Resolve issuer from entity statement if not explicitly configured
404
- # This allows issuer to be automatically discovered from entity statement
405
431
  resolved_issuer = options.issuer
406
432
  unless OmniauthOpenidFederation::StringHelpers.present?(resolved_issuer)
407
433
  resolved_issuer = resolve_issuer_from_metadata
408
- # Update options.issuer if resolved (for use in JWS builder)
409
434
  options.issuer = resolved_issuer if resolved_issuer
410
435
  end
411
436
 
412
- # Resolve audience (required for signed request objects)
413
- # Priority: explicit config > entity statement > resolved issuer > token endpoint (from entity/resolved) > client token_endpoint > client_options issuer
414
437
  audience_value = resolve_audience(client_options_hash, resolved_issuer)
415
438
 
416
439
  unless OmniauthOpenidFederation::StringHelpers.present?(audience_value)
@@ -420,33 +443,20 @@ module OmniAuth
420
443
  raise OmniauthOpenidFederation::ConfigurationError, error_msg
421
444
  end
422
445
 
423
- # Use signed request object (required for secure authorization requests)
424
- # RFC 9101: All authorization parameters MUST be included in the signed JWT
425
446
  state_value = new_state
426
447
  nonce_value = options.send_nonce ? new_nonce : nil
427
-
428
- # Normalize client options hash keys
429
- normalized_options = OmniauthOpenidFederation::Validators.normalize_hash(client_options_hash)
430
-
431
- # Use configured redirect_uri from client_options to ensure it matches what's registered
432
- # OmniAuth's callback_url might generate a different URL, so we use the configured one
433
448
  configured_redirect_uri = normalized_options[:redirect_uri] || callback_url
434
449
 
435
- # Handle automatic client registration (OpenID Federation Section 12.1)
436
- # For automatic registration, client_id is the entity identifier and entity statement is included
450
+ # Automatic registration uses entity identifier as client_id (OpenID Federation Section 12.1)
437
451
  client_registration_type = options.client_registration_type || :explicit
438
452
  client_id_for_request = normalized_options[:identifier]
439
453
  client_entity_statement = nil
440
454
 
441
455
  if client_registration_type == :automatic
442
- # Load client entity statement for automatic registration
443
- # Entity statement is always available (either from file or generated dynamically)
444
456
  client_entity_statement = load_client_entity_statement(
445
457
  options.client_entity_statement_path,
446
458
  options.client_entity_statement_url
447
459
  )
448
-
449
- # Extract entity identifier from entity statement (use 'sub' claim)
450
460
  entity_identifier = extract_entity_identifier_from_statement(client_entity_statement, options.client_entity_identifier)
451
461
  unless OmniauthOpenidFederation::StringHelpers.present?(entity_identifier)
452
462
  error_msg = "Failed to extract entity identifier from client entity statement. " \
@@ -454,44 +464,53 @@ module OmniAuth
454
464
  OmniauthOpenidFederation::Logger.error("[Strategy] #{error_msg}")
455
465
  raise OmniauthOpenidFederation::ConfigurationError, error_msg
456
466
  end
457
-
458
- # Use entity identifier as client_id for automatic registration
459
467
  client_id_for_request = entity_identifier
460
-
461
- # Update the OpenID Connect client's identifier for client assertion
462
- # The client assertion at the token endpoint should also use the entity identifier
463
- # Note: The client is cached, so we update it here for this request
464
468
  if client.respond_to?(:identifier=)
465
469
  client.identifier = entity_identifier
466
470
  elsif client.respond_to?(:client_id=)
467
471
  client.client_id = entity_identifier
468
472
  end
469
-
470
473
  OmniauthOpenidFederation::Logger.debug("[Strategy] Using automatic registration with entity identifier: #{entity_identifier}")
471
474
  end
472
475
 
473
- # Build JWT request object with all authorization parameters
474
- # According to RFC 9101, when using request objects, all params should be in the JWT
475
- # Support separate signing/encryption keys per OpenID Federation spec
476
- # Signing key source determines whether to use local static private_key or federation/JWKS
477
476
  signing_key_source = options.signing_key_source || options.key_source || :local
478
477
  jwks = normalized_options[:jwks] || normalized_options["jwks"]
478
+
479
+ # Extract already-sanitized user input params (sanitized above)
480
+ validated_state = state_value.to_s.strip
481
+ validated_nonce = nonce_value&.to_s&.strip
482
+ validated_login_hint = request_params["login_hint"]
483
+ validated_ui_locales = request_params["ui_locales"]
484
+ validated_claims_locales = request_params["claims_locales"]
485
+
486
+ # Config values are trusted (no sanitization needed)
487
+ validated_client_id = client_id_for_request.to_s.strip
488
+ validated_redirect_uri = configured_redirect_uri.to_s.strip
489
+ validated_scope = Array(options.scope).join(" ").strip
490
+ validated_response_type = options.response_type.to_s.strip
491
+ validated_prompt = options.prompt&.to_s&.strip
492
+ validated_hd = options.hd&.to_s&.strip
493
+ validated_response_mode = options.response_mode&.to_s&.strip
494
+ validated_issuer = (resolved_issuer || options.issuer)&.to_s&.strip
495
+ validated_audience = audience_value&.to_s&.strip
496
+ normalized_acr_values = OmniauthOpenidFederation::Validators.normalize_acr_values(request_params["acr_values"], skip_sanitization: true) || nil
497
+
479
498
  jws_builder = OmniauthOpenidFederation::Jws.new(
480
- client_id: client_id_for_request,
481
- redirect_uri: configured_redirect_uri,
482
- scope: Array(options.scope).join(" "),
483
- issuer: resolved_issuer || options.issuer,
484
- audience: audience_value,
485
- state: state_value,
486
- nonce: nonce_value,
487
- response_type: options.response_type,
488
- response_mode: options.response_mode,
489
- login_hint: request_params["login_hint"],
490
- ui_locales: request_params["ui_locales"],
491
- claims_locales: request_params["claims_locales"],
492
- prompt: options.prompt,
493
- hd: options.hd,
494
- acr_values: options.acr_values,
499
+ client_id: validated_client_id,
500
+ redirect_uri: validated_redirect_uri,
501
+ scope: validated_scope,
502
+ issuer: validated_issuer,
503
+ audience: validated_audience,
504
+ state: validated_state,
505
+ nonce: validated_nonce,
506
+ response_type: validated_response_type,
507
+ response_mode: validated_response_mode,
508
+ login_hint: validated_login_hint,
509
+ ui_locales: validated_ui_locales,
510
+ claims_locales: validated_claims_locales,
511
+ prompt: validated_prompt,
512
+ hd: validated_hd,
513
+ acr_values: normalized_acr_values,
495
514
  extra_params: options.extra_authorize_params || {},
496
515
  private_key: normalized_options[:private_key],
497
516
  jwks: jwks,
@@ -500,29 +519,15 @@ module OmniAuth
500
519
  client_entity_statement: client_entity_statement
501
520
  )
502
521
 
503
- # Add provider-specific extension parameters if configured
504
- # Note: Some providers may require additional parameters outside the JWT
505
- # @deprecated ftn_spname option - Use request_object_params instead for adding params to the JWT request object
506
- if options.ftn_spname && !options.ftn_spname.to_s.empty?
507
- OmniauthOpenidFederation::Logger.warn("[Strategy] ftn_spname option is deprecated. Use request_object_params: ['ftn_spname'] instead.")
508
- jws_builder.ftn_spname = options.ftn_spname
522
+ # Add dynamic request object params from HTTP request (already sanitized above)
523
+ options.request_object_params&.each do |key|
524
+ key_str = key.to_s
525
+ next if key_str.length > 256
526
+ value = request_params[key_str]
527
+ jws_builder.add_claim(key_str.to_sym, value) if value
509
528
  end
510
529
 
511
- # Allow dynamic request object params from HTTP request if configured
512
- # These parameters are added as claims to the JWT request object (RFC 9101)
513
- options.request_object_params&.each do |key|
514
- value = request_params[key.to_s]
515
- jws_builder.add_claim(key.to_sym, value) if value && !value.to_s.empty?
516
- end
517
-
518
- # ENFORCE: When using signed request objects, ONLY pass the 'request' parameter
519
- # All other params MUST be inside the JWT (RFC 9101 requirement)
520
- # This ensures secure authorization requests - unsigned requests are NOT allowed
521
- #
522
- # Load provider metadata for optional request object encryption
523
- # According to OpenID Connect Core and RFC 9101, if provider specifies
524
- # request_object_encryption_alg, the client SHOULD encrypt request objects
525
- # The always_encrypt_request_object option can force encryption if encryption keys are available
530
+ # RFC 9101: Only 'request' parameter in query, all params in JWT
526
531
  provider_metadata = load_provider_metadata_for_encryption
527
532
  signed_request_object = jws_builder.sign(
528
533
  provider_metadata: provider_metadata,
@@ -534,12 +539,7 @@ module OmniAuth
534
539
  raise OmniauthOpenidFederation::SecurityError, error_msg
535
540
  end
536
541
 
537
- # Build authorization URL manually to ensure RFC 9101 compliance
538
- # When using signed request objects, ONLY the 'request' parameter should be in the query string
539
- # The OpenID Connect client's authorization_uri method may add extra parameters, which violates RFC 9101
540
- # So we build the URL manually to ensure compliance
541
-
542
- # Get authorization endpoint from client
542
+ # Build URL manually to ensure RFC 9101 compliance (only 'request' param in query)
543
543
  auth_endpoint = client.authorization_endpoint
544
544
  unless OmniauthOpenidFederation::StringHelpers.present?(auth_endpoint)
545
545
  error_msg = "Authorization endpoint not configured. Provide authorization_endpoint in client_options or entity statement"
@@ -547,22 +547,20 @@ module OmniAuth
547
547
  raise OmniauthOpenidFederation::ConfigurationError, error_msg
548
548
  end
549
549
 
550
- # Build query string with ONLY the request parameter (and provider-specific params if needed)
551
- # RFC 9101: All authorization parameters MUST be inside the JWT, only 'request' parameter in query
552
- query_params = {
553
- request: signed_request_object
554
- }
550
+ begin
551
+ uri = URI.parse(auth_endpoint)
552
+ rescue URI::InvalidURIError => e
553
+ error_msg = "Invalid authorization endpoint URI format: #{e.message}"
554
+ OmniauthOpenidFederation::Logger.error("[Strategy] #{error_msg}")
555
+ raise OmniauthOpenidFederation::ConfigurationError, error_msg
556
+ end
555
557
 
556
- # Add provider-specific extension parameters outside JWT if configured
557
- # These are allowed per provider requirements (some providers require additional parameters)
558
- # @deprecated ftn_spname option - Use request_object_params instead for adding params to the JWT request object
559
- if options.ftn_spname && !options.ftn_spname.to_s.empty?
560
- query_params[:ftn_spname] = options.ftn_spname
558
+ max_string_length = ::OmniauthOpenidFederation::Configuration.config.max_string_length
559
+ if signed_request_object.length > max_string_length
560
+ OmniauthOpenidFederation::Logger.warn("[Strategy] Request object exceeds maximum length")
561
561
  end
562
562
 
563
- # Build the full authorization URL manually
564
- uri = URI.parse(auth_endpoint)
565
- uri.query = URI.encode_www_form(query_params.reject { |_k, v| v.nil? })
563
+ uri.query = URI.encode_www_form(request: signed_request_object)
566
564
  uri.to_s
567
565
  end
568
566
 
@@ -594,31 +592,21 @@ module OmniAuth
594
592
  access_token = @access_token
595
593
  access_token ||= exchange_authorization_code(request.params["code"])
596
594
 
597
- # Decode and validate ID token
598
595
  id_token = decode_id_token(access_token.id_token)
599
596
  id_token_claims = id_token.raw_attributes || {}
600
597
 
601
- # Fetch userinfo if configured (default: true for backward compatibility)
602
- # According to OpenID Federation spec, ID token may contain all needed data
603
- # Developer can disable userinfo fetching if ID token is sufficient
604
598
  if options.fetch_userinfo
605
599
  begin
606
600
  userinfo = access_token.userinfo!
607
-
608
- # Decrypt userinfo if encrypted (JWE format)
609
601
  userinfo_hash = decode_userinfo(userinfo)
610
-
611
- # Combine ID token and userinfo (userinfo takes precedence for overlapping claims)
612
602
  id_token_claims.merge(userinfo_hash)
613
603
  rescue => e
614
604
  error_msg = "Failed to fetch or decode userinfo: #{e.class} - #{e.message}"
615
605
  OmniauthOpenidFederation::Logger.error("[Strategy] #{error_msg}")
616
- # If userinfo fetch fails, log warning but don't fail - ID token may be sufficient
617
606
  OmniauthOpenidFederation::Logger.warn("[Strategy] Falling back to ID token claims only")
618
607
  id_token_claims
619
608
  end
620
609
  else
621
- # Userinfo fetching disabled - use ID token claims only
622
610
  OmniauthOpenidFederation::Logger.debug("[Strategy] Userinfo fetching disabled, using ID token claims only")
623
611
  id_token_claims
624
612
  end
@@ -628,17 +616,11 @@ module OmniAuth
628
616
  private
629
617
 
630
618
  # Exchange authorization code for access token
631
- # This bypasses the base OAuth2 strategy's client.auth_code.get_token call
632
- # @param authorization_code [String] The authorization code from the callback
633
- # @return [OpenIDConnect::AccessToken] The access token
634
- # @raise [StandardError] If token exchange fails
635
619
  def exchange_authorization_code(authorization_code)
636
620
  client_options_hash = options.client_options || {}
637
621
  normalized_options = OmniauthOpenidFederation::Validators.normalize_hash(client_options_hash)
638
622
  configured_redirect_uri = normalized_options[:redirect_uri] || callback_url
639
623
 
640
- # Set the authorization code grant type (required for authorization_code flow)
641
- # This sets @grant = Grant::AuthorizationCode.new(...) instead of default Grant::ClientCredentials
642
624
  oidc_client.authorization_code = authorization_code
643
625
  oidc_client.redirect_uri = configured_redirect_uri
644
626
 
@@ -727,10 +709,19 @@ module OmniAuth
727
709
 
728
710
  # Build full URLs from paths if needed
729
711
  # Use resolved issuer if available, otherwise fall back to configured issuer
712
+ # Note: Config values are trusted, no security validation needed
730
713
  issuer_uri = if resolved_issuer
731
- URI.parse(resolved_issuer)
714
+ begin
715
+ URI.parse(resolved_issuer)
716
+ rescue URI::InvalidURIError
717
+ nil
718
+ end
732
719
  elsif options.issuer
733
- URI.parse(options.issuer.to_s)
720
+ begin
721
+ URI.parse(options.issuer.to_s)
722
+ rescue URI::InvalidURIError
723
+ nil
724
+ end
734
725
  end
735
726
 
736
727
  resolved_hash = {}
@@ -1610,7 +1601,8 @@ module OmniAuth
1610
1601
 
1611
1602
  # Priority 3: Fetch from issuer if provided (only if issuer is a valid URL)
1612
1603
  if OmniauthOpenidFederation::StringHelpers.present?(options.issuer)
1613
- # Validate that issuer is a valid URL before trying to fetch
1604
+ # Check that issuer is a valid URL format before trying to fetch
1605
+ # Note: Config values are trusted, only basic format check needed
1614
1606
  begin
1615
1607
  parsed_issuer = URI.parse(options.issuer)
1616
1608
  unless parsed_issuer.is_a?(URI::HTTP) || parsed_issuer.is_a?(URI::HTTPS)
@@ -2049,49 +2041,6 @@ module OmniAuth
2049
2041
  end
2050
2042
  end
2051
2043
 
2052
- # Combines configured ACR values with request ACR values
2053
- # ACR values are space-separated strings per OpenID Connect spec
2054
- # This allows:
2055
- # - Configure assurance level (e.g., "urn:example:oidc:acr:level4") at gem level
2056
- # - Specify provider (e.g., "oidc.provider.1") from component/request
2057
- # - Both are combined: "oidc.provider.1 urn:example:oidc:acr:level4"
2058
- #
2059
- # @param configured_acr [String, Array, nil] ACR values configured at gem level
2060
- # @param request_acr [String, nil] ACR values from request parameters
2061
- # @return [String, nil] Combined space-separated ACR values, or nil if both are empty
2062
- def combine_acr_values(configured_acr:, request_acr:)
2063
- # Normalize both to arrays of values
2064
- configured_values = normalize_acr_values(configured_acr)
2065
- request_values = normalize_acr_values(request_acr)
2066
-
2067
- # Combine and remove duplicates (preserving order)
2068
- combined = (request_values + configured_values).uniq
2069
-
2070
- # Return space-separated string or nil
2071
- combined.empty? ? nil : combined.join(" ")
2072
- end
2073
-
2074
- # Normalizes ACR values to an array
2075
- # Handles: nil, string (space-separated), array
2076
- #
2077
- # @param acr_values [String, Array, nil] ACR values in any format
2078
- # @return [Array<String>] Array of ACR value strings
2079
- def normalize_acr_values(acr_values)
2080
- return [] if OmniauthOpenidFederation::StringHelpers.blank?(acr_values)
2081
-
2082
- case acr_values
2083
- when Array
2084
- # Already an array, filter out blanks
2085
- acr_values.map(&:to_s).reject { |v| OmniauthOpenidFederation::StringHelpers.blank?(v) }
2086
- when String
2087
- # Space-separated string, split and filter
2088
- acr_values.split(/\s+/).reject { |v| OmniauthOpenidFederation::StringHelpers.blank?(v) }
2089
- else
2090
- # Convert to string and split
2091
- acr_values.to_s.split(/\s+/).reject { |v| OmniauthOpenidFederation::StringHelpers.blank?(v) }
2092
- end
2093
- end
2094
-
2095
2044
  def fetch_jwks(jwks_uri)
2096
2045
  # Use our JWKS fetching logic
2097
2046
  # Returns a hash with "keys" array that JWT.decode can use directly