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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -1
- data/README.md +210 -708
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +13 -0
- data/examples/config/initializers/devise.rb.example +44 -55
- data/examples/config/initializers/federation_endpoint.rb.example +2 -2
- data/examples/config/open_id_connect_config.rb.example +12 -15
- data/examples/config/routes.rb.example +9 -5
- data/lib/omniauth_openid_federation/configuration.rb +8 -0
- data/lib/omniauth_openid_federation/constants.rb +5 -0
- data/lib/omniauth_openid_federation/federation_endpoint.rb +0 -22
- data/lib/omniauth_openid_federation/jwks/decode.rb +0 -15
- data/lib/omniauth_openid_federation/jws.rb +21 -19
- data/lib/omniauth_openid_federation/rack_endpoint.rb +13 -0
- data/lib/omniauth_openid_federation/strategy.rb +143 -194
- data/lib/omniauth_openid_federation/tasks_helper.rb +482 -1
- data/lib/omniauth_openid_federation/validators.rb +316 -6
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/tasks/omniauth_openid_federation.rake +298 -0
- data/sig/federation.rbs +0 -8
- data/sig/jwks.rbs +0 -6
- data/sig/omniauth_openid_federation.rbs +0 -1
- data/sig/strategy.rbs +0 -2
- metadata +1 -1
|
@@ -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 (
|
|
42
|
-
# - Trust marks (Section 7) -
|
|
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
|
|
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
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
385
|
-
#
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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:
|
|
481
|
-
redirect_uri:
|
|
482
|
-
scope:
|
|
483
|
-
issuer:
|
|
484
|
-
audience:
|
|
485
|
-
state:
|
|
486
|
-
nonce:
|
|
487
|
-
response_type:
|
|
488
|
-
response_mode:
|
|
489
|
-
login_hint:
|
|
490
|
-
ui_locales:
|
|
491
|
-
claims_locales:
|
|
492
|
-
prompt:
|
|
493
|
-
hd:
|
|
494
|
-
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
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
jws_builder.
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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
|
-
|
|
714
|
+
begin
|
|
715
|
+
URI.parse(resolved_issuer)
|
|
716
|
+
rescue URI::InvalidURIError
|
|
717
|
+
nil
|
|
718
|
+
end
|
|
732
719
|
elsif options.issuer
|
|
733
|
-
|
|
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
|
-
#
|
|
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
|