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.
@@ -1,4 +1,7 @@
1
1
  # Input validation utilities for omniauth_openid_federation
2
+ require_relative "constants"
3
+ require_relative "configuration"
4
+
2
5
  module OmniauthOpenidFederation
3
6
  module Validators
4
7
  # Validate that a private key is present and valid
@@ -71,7 +74,8 @@ module OmniauthOpenidFederation
71
74
  true
72
75
  end
73
76
 
74
- # Validate client options hash
77
+ # Validate client options hash (for configuration validation only)
78
+ # Note: This validates configuration structure, not security (config is trusted)
75
79
  #
76
80
  # @param client_options [Hash] The client options to validate
77
81
  # @raise [ConfigurationError] If required options are missing
@@ -81,7 +85,7 @@ module OmniauthOpenidFederation
81
85
  # Normalize hash keys to symbols
82
86
  normalized = normalize_hash(client_options)
83
87
 
84
- # Validate required fields
88
+ # Validate required fields (structure validation, not security)
85
89
  if StringHelpers.blank?(normalized[:identifier])
86
90
  raise ConfigurationError, "Client identifier is required"
87
91
  end
@@ -90,13 +94,21 @@ module OmniauthOpenidFederation
90
94
  raise ConfigurationError, "Redirect URI is required"
91
95
  end
92
96
 
93
- # Validate redirect URI format
94
- validate_uri!(normalized[:redirect_uri], required: true)
97
+ # Basic format check for redirect URI (config validation, not security)
98
+ # Note: Config values are trusted, we only check format to catch config errors
99
+ begin
100
+ parsed = URI.parse(normalized[:redirect_uri].to_s)
101
+ unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
102
+ raise ConfigurationError, "Redirect URI must be HTTP or HTTPS: #{normalized[:redirect_uri]}"
103
+ end
104
+ rescue URI::InvalidURIError => e
105
+ raise ConfigurationError, "Invalid redirect URI format: #{e.message}"
106
+ end
95
107
 
96
- # Validate private key
108
+ # Validate private key (required for security)
97
109
  validate_private_key!(normalized[:private_key])
98
110
 
99
- # Validate endpoints if provided
111
+ # Basic format check for endpoints (config validation, not security)
100
112
  %i[authorization_endpoint token_endpoint jwks_uri].each do |endpoint|
101
113
  if normalized.key?(endpoint) && !StringHelpers.blank?(normalized[endpoint])
102
114
  # Endpoints can be paths or full URLs
@@ -122,5 +134,303 @@ module OmniauthOpenidFederation
122
134
  result[key] = v
123
135
  end
124
136
  end
137
+
138
+ # Validate and sanitize user input from HTTP requests only (not config values)
139
+ # Prevents URI exploitation, ReDoS, string overflow, and control character attacks
140
+ # Default max_length uses Configuration.config.max_string_length (8KB default) - large enough for legitimate use, prevents DoS attacks
141
+ def self.sanitize_request_param(value, max_length: nil, allow_control_chars: false)
142
+ max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
143
+ return nil if value.nil?
144
+
145
+ str = value.to_s.strip
146
+ return nil if str.length > max_length
147
+
148
+ # Only allow printable ASCII (whitelist approach)
149
+ unless allow_control_chars
150
+ str = str.gsub(/[^\x20-\x7E]/, "")
151
+ end
152
+
153
+ str.empty? ? nil : str
154
+ end
155
+
156
+ # Validate URI for user input only (not config values)
157
+ # Prevents URI gem exploitation and validates scheme/length
158
+ def self.validate_uri_safe!(uri_str, max_length: nil, allowed_schemes: ["http", "https"])
159
+ max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
160
+ raise SecurityError, "URI cannot be nil" if uri_str.nil?
161
+
162
+ str = uri_str.to_s.strip
163
+ raise SecurityError, "URI cannot be empty" if str.empty?
164
+ raise SecurityError, "URI exceeds maximum length of #{max_length} characters" if str.length > max_length
165
+
166
+ sanitized = str.gsub(/[^\x20-\x7E]/, "")
167
+ raise SecurityError, "URI contains invalid characters (only printable ASCII allowed)" if sanitized != str
168
+
169
+ begin
170
+ parsed = URI.parse(sanitized)
171
+ rescue URI::InvalidURIError => e
172
+ raise SecurityError, "Invalid URI format: #{e.message}"
173
+ end
174
+
175
+ unless parsed.scheme && allowed_schemes.include?(parsed.scheme.downcase)
176
+ raise SecurityError, "URI scheme must be one of: #{allowed_schemes.join(", ")}"
177
+ end
178
+
179
+ unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
180
+ raise SecurityError, "URI must be HTTP or HTTPS"
181
+ end
182
+
183
+ raise SecurityError, "URI host cannot be empty" if parsed.host.nil? || parsed.host.empty?
184
+
185
+ parsed
186
+ end
187
+
188
+ # Validate and normalize acr_values parameter per OIDC Core 1.0 spec
189
+ # acr_values is a space-separated string of ACR values
190
+ # Security: Uses allowed characters approach - only allows printable ASCII characters
191
+ #
192
+ # @param acr_values [String, Array, nil] ACR values in any format
193
+ # @param max_length [Integer] Maximum total length (default: Configuration.config.max_string_length)
194
+ # @param skip_sanitization [Boolean] Skip sanitization if values are already sanitized (default: false)
195
+ # @return [String, nil] Normalized space-separated string or nil
196
+ def self.normalize_acr_values(acr_values, max_length: nil, skip_sanitization: false)
197
+ max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
198
+ return nil if StringHelpers.blank?(acr_values)
199
+
200
+ case acr_values
201
+ when Array
202
+ # Filter out blanks (arrays may already be sanitized)
203
+ values = acr_values.map(&:to_s).map(&:strip).reject { |v| StringHelpers.blank?(v) }
204
+ when String
205
+ # Trim and split by whitespace and validate each value using allowed characters
206
+ # Security: Use simple space split (no regexp to avoid ReDoS)
207
+ trimmed = acr_values.strip
208
+ values = trimmed.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
209
+ else
210
+ # Convert to string, trim and split
211
+ str = acr_values.to_s.strip
212
+ return nil if str.length > max_length
213
+ # Security: Use simple space split (no regexp to avoid ReDoS)
214
+ values = str.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
215
+ end
216
+
217
+ # Sanitize each value unless already sanitized
218
+ unless skip_sanitization
219
+ values = values.map { |v| sanitize_request_param(v) }.compact
220
+ end
221
+
222
+ return nil if values.empty?
223
+
224
+ result = values.join(" ")
225
+ return nil if result.length > max_length
226
+
227
+ result
228
+ end
229
+
230
+ # Validate and sanitize client_id per OIDC Core 1.0 spec
231
+ # client_id is REQUIRED and must be a valid string
232
+ #
233
+ # @param client_id [String, nil] Client identifier
234
+ # @return [String] Sanitized client_id
235
+ # @raise [ConfigurationError] If client_id is invalid
236
+ def self.validate_client_id!(client_id)
237
+ if StringHelpers.blank?(client_id)
238
+ raise ConfigurationError, "client_id is REQUIRED per OIDC Core 1.0 spec"
239
+ end
240
+
241
+ str = client_id.to_s.strip
242
+ if str.empty?
243
+ raise ConfigurationError, "client_id cannot be empty after trimming"
244
+ end
245
+
246
+ # Sanitize using allowed characters (printable ASCII only)
247
+ sanitized = sanitize_request_param(str)
248
+ if sanitized.nil? || sanitized.empty?
249
+ raise ConfigurationError, "client_id contains invalid characters"
250
+ end
251
+
252
+ sanitized
253
+ end
254
+
255
+ # Validate and sanitize redirect_uri per OIDC Core 1.0 spec
256
+ # redirect_uri is REQUIRED and must be a valid absolute URI
257
+ #
258
+ # @param redirect_uri [String, nil] Redirect URI
259
+ # @return [String] Validated redirect_uri
260
+ # @raise [ConfigurationError] If redirect_uri is invalid
261
+ def self.validate_redirect_uri!(redirect_uri)
262
+ if StringHelpers.blank?(redirect_uri)
263
+ raise ConfigurationError, "redirect_uri is REQUIRED per OIDC Core 1.0 spec"
264
+ end
265
+
266
+ str = redirect_uri.to_s.strip
267
+ if str.empty?
268
+ raise ConfigurationError, "redirect_uri cannot be empty after trimming"
269
+ end
270
+
271
+ # Validate as URI (includes allowed characters validation)
272
+ validated = validate_uri_safe!(str, allowed_schemes: ["http", "https"])
273
+ validated.to_s
274
+ end
275
+
276
+ # Validate and sanitize scope per OIDC Core 1.0 spec
277
+ # scope is space-delimited, case-sensitive list of ASCII string values
278
+ # MUST include "openid" scope value
279
+ #
280
+ # @param scope [String, Array, nil] Scope value(s)
281
+ # @param require_openid [Boolean] Whether to require "openid" scope (default: true)
282
+ # @return [String] Normalized space-separated scope string
283
+ # @raise [ConfigurationError] If scope is invalid
284
+ def self.validate_scope!(scope, require_openid: true)
285
+ if StringHelpers.blank?(scope)
286
+ if require_openid
287
+ raise ConfigurationError, "scope is REQUIRED and MUST include 'openid' per OIDC Core 1.0 spec"
288
+ end
289
+ return nil
290
+ end
291
+
292
+ # Normalize to array
293
+ scopes = case scope
294
+ when Array
295
+ scope.map(&:to_s).map(&:strip).reject { |s| StringHelpers.blank?(s) }
296
+ when String
297
+ scope.strip.split(" ").map(&:strip).reject { |s| StringHelpers.blank?(s) }
298
+ else
299
+ scope.to_s.strip.split(" ").map(&:strip).reject { |s| StringHelpers.blank?(s) }
300
+ end
301
+
302
+ # Validate each scope value (allowed: printable ASCII)
303
+ scopes = scopes.map { |s| sanitize_request_param(s) }.compact
304
+
305
+ if scopes.empty?
306
+ raise ConfigurationError, "scope cannot be empty after validation"
307
+ end
308
+
309
+ # Check for "openid" scope if required
310
+ if require_openid && !scopes.include?("openid")
311
+ raise ConfigurationError, "scope MUST include 'openid' per OIDC Core 1.0 spec"
312
+ end
313
+
314
+ result = scopes.join(" ")
315
+ max_length = ::OmniauthOpenidFederation::Configuration.config.max_string_length
316
+ if result.length > max_length
317
+ raise ConfigurationError, "scope exceeds maximum length of #{max_length} characters"
318
+ end
319
+
320
+ result
321
+ end
322
+
323
+ # Validate and sanitize state parameter
324
+ # state is REQUIRED for CSRF protection
325
+ #
326
+ # @param state [String, nil] State value
327
+ # @return [String] Sanitized state value
328
+ # @raise [ConfigurationError] If state is invalid
329
+ def self.validate_state!(state)
330
+ if StringHelpers.blank?(state)
331
+ raise ConfigurationError, "state is REQUIRED for CSRF protection"
332
+ end
333
+
334
+ str = state.to_s.strip
335
+ if str.empty?
336
+ raise ConfigurationError, "state cannot be empty after trimming"
337
+ end
338
+
339
+ # Sanitize using allowed characters (printable ASCII only)
340
+ sanitized = sanitize_request_param(str)
341
+ if sanitized.nil? || sanitized.empty?
342
+ raise ConfigurationError, "state contains invalid characters"
343
+ end
344
+
345
+ sanitized
346
+ end
347
+
348
+ # Validate and sanitize nonce parameter
349
+ # nonce is REQUIRED for Implicit and Hybrid flows, RECOMMENDED for Authorization Code flow
350
+ #
351
+ # @param nonce [String, nil] Nonce value
352
+ # @param required [Boolean] Whether nonce is required (default: false)
353
+ # @return [String, nil] Sanitized nonce value or nil
354
+ # @raise [ConfigurationError] If nonce is required but invalid
355
+ def self.validate_nonce(nonce, required: false)
356
+ return nil unless nonce
357
+
358
+ str = nonce.to_s.strip
359
+ if str.empty?
360
+ if required
361
+ raise ConfigurationError, "nonce is REQUIRED but is empty after trimming"
362
+ end
363
+ return nil
364
+ end
365
+
366
+ # Sanitize using allowed characters (printable ASCII only)
367
+ # OIDC Core 1.0: nonce value is a case-sensitive string
368
+ sanitized = sanitize_request_param(str)
369
+ if sanitized.nil? || sanitized.empty?
370
+ if required
371
+ raise ConfigurationError, "nonce contains invalid characters"
372
+ end
373
+ return nil
374
+ end
375
+
376
+ sanitized
377
+ end
378
+
379
+ # Validate and sanitize response_type per OIDC Core 1.0 spec
380
+ # response_type is REQUIRED and must be a valid value
381
+ #
382
+ # @param response_type [String, nil] Response type
383
+ # @return [String] Validated response_type
384
+ # @raise [ConfigurationError] If response_type is invalid
385
+ def self.validate_response_type!(response_type)
386
+ if StringHelpers.blank?(response_type)
387
+ raise ConfigurationError, "response_type is REQUIRED per OIDC Core 1.0 spec"
388
+ end
389
+
390
+ str = response_type.to_s.strip
391
+ if str.empty?
392
+ raise ConfigurationError, "response_type cannot be empty after trimming"
393
+ end
394
+
395
+ # Sanitize using allowed characters (printable ASCII only)
396
+ sanitized = sanitize_request_param(str)
397
+ if sanitized.nil? || sanitized.empty?
398
+ raise ConfigurationError, "response_type contains invalid characters"
399
+ end
400
+
401
+ # Validate it's a known response type (space-separated list)
402
+ valid_types = ["code", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"]
403
+ types = sanitized.split(" ").map(&:strip)
404
+ unless types.all? { |t| valid_types.include?(t) || t.match?(/^[a-z_]+$/) }
405
+ raise ConfigurationError, "response_type contains invalid value: #{sanitized}"
406
+ end
407
+
408
+ sanitized
409
+ end
410
+
411
+ # Validate entity identifier per OpenID Federation 1.0 spec
412
+ # Entity identifiers are URIs that identify entities in the federation
413
+ #
414
+ # @param entity_id [String, nil] Entity identifier
415
+ # @param max_length [Integer] Maximum length (default: Configuration.config.max_string_length)
416
+ # @return [String] Validated and trimmed entity identifier
417
+ # @raise [SecurityError] If entity identifier is invalid
418
+ def self.validate_entity_identifier!(entity_id, max_length: nil)
419
+ max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
420
+ if StringHelpers.blank?(entity_id)
421
+ raise SecurityError, "Entity identifier cannot be nil or empty"
422
+ end
423
+
424
+ str = entity_id.to_s.strip
425
+ if str.empty?
426
+ raise SecurityError, "Entity identifier cannot be empty after trimming"
427
+ end
428
+
429
+ # Validate as URI (includes allowed characters and length validation)
430
+ validate_uri_safe!(str, max_length: max_length, allowed_schemes: ["http", "https"])
431
+
432
+ # Return trimmed and validated value
433
+ str
434
+ end
125
435
  end
126
436
  end
@@ -1,3 +1,3 @@
1
1
  module OmniauthOpenidFederation
2
- VERSION = "1.2.2".freeze
2
+ VERSION = "1.3.0".freeze
3
3
  end
@@ -373,4 +373,302 @@ namespace :openid_federation do
373
373
  exit 1
374
374
  end
375
375
  end
376
+
377
+ desc "Test full OpenID Federation authentication flow with a real provider"
378
+ task :test_authentication_flow, [:login_page_url, :base_url, :provider_acr] => :environment do |_t, args|
379
+ require "omniauth_openid_federation"
380
+ require "cgi"
381
+ require "base64"
382
+ require "openssl"
383
+ require "uri"
384
+ require "time"
385
+
386
+ login_page_url = args[:login_page_url] || ENV["LOGIN_PAGE_URL"]
387
+ base_url = args[:base_url] || ENV["BASE_URL"] || (login_page_url ? URI.parse(login_page_url).tap { |u|
388
+ u.path = ""
389
+ u.query = nil
390
+ u.fragment = nil
391
+ }.to_s : "http://localhost:3000")
392
+ provider_acr = args[:provider_acr] || ENV["PROVIDER_ACR"]
393
+
394
+ # Required configuration from environment
395
+ client_id = ENV["CLIENT_ID"]
396
+ redirect_uri = ENV["REDIRECT_URI"] || "#{base_url}/users/auth/openid_federation/callback"
397
+ private_key_pem = ENV["PRIVATE_KEY"] || (ENV["PRIVATE_KEY_BASE64"] ? Base64.decode64(ENV["PRIVATE_KEY_BASE64"]) : nil)
398
+ private_key_path = ENV["PRIVATE_KEY_PATH"]
399
+
400
+ # Entity statement configuration
401
+ entity_statement_url = ENV["ENTITY_STATEMENT_URL"]
402
+ entity_statement_path = ENV["ENTITY_STATEMENT_PATH"]
403
+ client_entity_statement_url = ENV["CLIENT_ENTITY_STATEMENT_URL"]
404
+ client_entity_statement_path = ENV["CLIENT_ENTITY_STATEMENT_PATH"]
405
+
406
+ puts "=" * 80
407
+ puts "OpenID Federation Authentication Flow Test"
408
+ puts "=" * 80
409
+ puts
410
+ puts "Login Page URL: #{login_page_url || "Not provided"}"
411
+ puts "Base URL: #{base_url}"
412
+ puts "Provider ACR: #{provider_acr || "Not specified (will use default)"}"
413
+ puts
414
+
415
+ # Validate required parameters
416
+ unless login_page_url
417
+ puts "❌ LOGIN_PAGE_URL is required"
418
+ puts " Set it as an environment variable or pass as first argument:"
419
+ puts " rake openid_federation:test_authentication_flow[https://example.com/login]"
420
+ exit 1
421
+ end
422
+
423
+ # Load private key
424
+ private_key = nil
425
+ if private_key_pem
426
+ begin
427
+ private_key = OpenSSL::PKey::RSA.new(private_key_pem)
428
+ rescue => e
429
+ puts "❌ Failed to parse private key from PRIVATE_KEY or PRIVATE_KEY_BASE64: #{e.message}"
430
+ exit 1
431
+ end
432
+ elsif private_key_path
433
+ begin
434
+ private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
435
+ rescue => e
436
+ puts "❌ Failed to load private key from #{private_key_path}: #{e.message}"
437
+ exit 1
438
+ end
439
+ end
440
+
441
+ unless private_key
442
+ puts "❌ Private key is required"
443
+ puts " Set one of: PRIVATE_KEY, PRIVATE_KEY_BASE64, or PRIVATE_KEY_PATH"
444
+ exit 1
445
+ end
446
+
447
+ unless client_id
448
+ puts "❌ CLIENT_ID is required"
449
+ puts " Set it as an environment variable: CLIENT_ID=your_client_id"
450
+ exit 1
451
+ end
452
+
453
+ # Try to resolve entity statement if not provided
454
+ unless entity_statement_url || entity_statement_path
455
+ # Try to fetch from well-known endpoint
456
+ begin
457
+ well_known_url = "#{base_url}/.well-known/openid-federation"
458
+ puts "📥 Attempting to fetch entity statement from: #{well_known_url}"
459
+ require "http"
460
+ response = HTTP.timeout(connect: 5, read: 5).get(well_known_url)
461
+ if response.status.success?
462
+ entity_statement_path = "/tmp/entity_statement_#{Time.now.to_i}.json"
463
+ File.write(entity_statement_path, response.body.to_s)
464
+ puts " ✅ Entity statement cached to: #{entity_statement_path}"
465
+ end
466
+ rescue => e
467
+ puts " ⚠️ Could not fetch entity statement: #{e.message}"
468
+ puts " Set ENTITY_STATEMENT_URL or ENTITY_STATEMENT_PATH manually"
469
+ end
470
+ end
471
+
472
+ # Display configuration status
473
+ puts "📋 Configuration Status:"
474
+ puts " ✅ Client ID: #{client_id}"
475
+ puts " ✅ Redirect URI: #{redirect_uri}"
476
+ puts " ✅ Private Key: Loaded"
477
+ if entity_statement_url
478
+ puts " ✅ Provider Entity Statement URL: #{entity_statement_url}"
479
+ elsif entity_statement_path
480
+ puts " ✅ Provider Entity Statement Path: #{entity_statement_path}"
481
+ else
482
+ puts " ⚠️ Provider Entity Statement: Not configured"
483
+ end
484
+ if client_entity_statement_url
485
+ puts " ✅ Client Entity Statement URL: #{client_entity_statement_url}"
486
+ elsif client_entity_statement_path
487
+ puts " ✅ Client Entity Statement Path: #{client_entity_statement_path}"
488
+ end
489
+ puts
490
+
491
+ begin
492
+ # Step 1: Request authorization URL
493
+ puts "=" * 80
494
+ puts "📋 Step 1: Requesting Authorization URL"
495
+ puts "-" * 80
496
+ puts
497
+
498
+ result = OmniauthOpenidFederation::TasksHelper.test_authentication_flow(
499
+ login_page_url: login_page_url,
500
+ base_url: base_url,
501
+ provider_acr: provider_acr
502
+ )
503
+
504
+ if result[:errors].any?
505
+ puts "❌ Errors occurred:"
506
+ result[:errors].each { |error| puts " - #{error}" }
507
+ exit 1
508
+ end
509
+
510
+ puts "✅ CSRF token extracted: #{result[:csrf_token][0..20]}..." if result[:csrf_token]
511
+ puts "✅ Cookies received: #{result[:cookies].length} cookie(s)"
512
+ puts
513
+ puts "✅ Authorization URL received"
514
+ puts
515
+ puts "🔗 Authorization URL:"
516
+ puts result[:authorization_url]
517
+ puts
518
+ puts "📋 Instructions:"
519
+ result[:instructions].each { |instruction| puts " #{instruction}" }
520
+ puts
521
+
522
+ # Step 2: Get callback URL from user
523
+ puts "=" * 80
524
+ puts "📥 Step 2: Waiting for Callback URL"
525
+ puts "-" * 80
526
+ puts
527
+ print "Paste the callback URL here (or press Enter to skip to manual code entry): "
528
+ callback_url = $stdin.gets.chomp
529
+
530
+ if callback_url.empty?
531
+ puts
532
+ print "Enter authorization code manually: "
533
+ auth_code = $stdin.gets.chomp
534
+
535
+ if auth_code.empty?
536
+ puts "❌ No authorization code provided"
537
+ exit 1
538
+ end
539
+
540
+ # Build callback URL with code
541
+ callback_url = "#{base_url}/users/auth/openid_federation/callback?code=#{CGI.escape(auth_code)}"
542
+ end
543
+
544
+ # Step 3: Process callback and validate
545
+ puts
546
+ puts "=" * 80
547
+ puts "🔄 Step 3: Processing Callback and Validating"
548
+ puts "-" * 80
549
+ puts
550
+
551
+ callback_result = OmniauthOpenidFederation::TasksHelper.process_callback_and_validate(
552
+ callback_url: callback_url,
553
+ base_url: base_url,
554
+ entity_statement_url: entity_statement_url,
555
+ entity_statement_path: entity_statement_path,
556
+ client_id: client_id,
557
+ redirect_uri: redirect_uri,
558
+ private_key: private_key,
559
+ provider_acr: provider_acr,
560
+ client_entity_statement_url: client_entity_statement_url,
561
+ client_entity_statement_path: client_entity_statement_path
562
+ )
563
+
564
+ if callback_result[:errors].any?
565
+ puts "❌ Errors occurred:"
566
+ callback_result[:errors].each { |error| puts " - #{error}" }
567
+ exit 1
568
+ end
569
+
570
+ puts "✅ Authorization code extracted"
571
+ puts "✅ Strategy initialized"
572
+ puts "✅ Tokens received"
573
+ puts
574
+ puts "📋 Token Information:"
575
+ callback_result[:token_info].each do |key, value|
576
+ if value
577
+ label = key.to_s.split("_").map(&:capitalize).join(" ")
578
+ puts " #{label}: #{value}"
579
+ end
580
+ end
581
+ puts
582
+
583
+ # Step 4: ID Token validation
584
+ puts "=" * 80
585
+ puts "🔐 Step 4: ID Token Validation"
586
+ puts "-" * 80
587
+ puts
588
+
589
+ if callback_result[:id_token_valid]
590
+ puts "✅ ID token decrypted and validated"
591
+ puts
592
+ puts "📋 ID Token Claims:"
593
+ callback_result[:id_token_claims].each do |key, value|
594
+ if value
595
+ if [:exp, :iat, :auth_time].include?(key)
596
+ time_value = begin
597
+ Time.at(value)
598
+ rescue
599
+ value
600
+ end
601
+ puts " #{key}: #{value} (#{time_value})"
602
+ else
603
+ puts " #{key}: #{value}"
604
+ end
605
+ end
606
+ end
607
+ puts
608
+ puts "✅ All required claims present"
609
+ else
610
+ puts "❌ ID token validation failed"
611
+ end
612
+ puts
613
+
614
+ # Step 5: OpenID Federation Compliance
615
+ puts "=" * 80
616
+ puts "✅ Step 5: OpenID Federation Compliance Check"
617
+ puts "-" * 80
618
+ puts
619
+
620
+ callback_result[:compliance_checks].each do |check_name, check_data|
621
+ status_icon = check_data[:verified] ? "✅" : "❌"
622
+ puts "#{status_icon} #{check_name}"
623
+ puts " Status: #{check_data[:status]}"
624
+ puts " Description: #{check_data[:description]}"
625
+ puts
626
+ end
627
+
628
+ all_verified = callback_result[:all_compliance_verified]
629
+
630
+ if all_verified
631
+ puts "✅ All OpenID Federation requirements verified"
632
+ else
633
+ puts "⚠️ Some requirements not verified"
634
+ end
635
+ puts
636
+
637
+ # Step 6: Summary
638
+ puts "=" * 80
639
+ puts "📊 Test Summary"
640
+ puts "=" * 80
641
+ puts
642
+ puts "Provider ACR: #{provider_acr || "Default"}"
643
+ if callback_result[:id_token_claims][:sub]
644
+ puts "Subject (sub): #{callback_result[:id_token_claims][:sub]}"
645
+ end
646
+ if callback_result[:id_token_claims][:iss]
647
+ puts "Issuer (iss): #{callback_result[:id_token_claims][:iss]}"
648
+ end
649
+ if callback_result[:id_token_claims][:aud]
650
+ puts "Audience (aud): #{callback_result[:id_token_claims][:aud]}"
651
+ end
652
+ if callback_result[:id_token_claims][:acr]
653
+ puts "Authentication Context (acr): #{callback_result[:id_token_claims][:acr]}"
654
+ end
655
+ puts
656
+ puts "OpenID Federation Compliance: #{all_verified ? "✅ PASS" : "❌ FAIL"}"
657
+ puts
658
+ puts "=" * 80
659
+
660
+ if all_verified
661
+ puts "✅ All tests passed! Implementation is compliant."
662
+ exit 0
663
+ else
664
+ puts "⚠️ Some checks failed. Review the output above."
665
+ exit 1
666
+ end
667
+ rescue => e
668
+ puts "❌ Test failed: #{e.message}"
669
+ puts " #{e.class}"
670
+ puts " #{e.backtrace.first(10).join("\n ")}"
671
+ exit 1
672
+ end
673
+ end
376
674
  end
data/sig/federation.rbs CHANGED
@@ -152,14 +152,6 @@ module OmniauthOpenidFederation
152
152
  def self.generate_signed_jwks: () -> String
153
153
  def self.current_jwks: () -> Hash[String, untyped]
154
154
  def self.rack_app: () -> RackEndpoint
155
- def self.mount_routes: (
156
- untyped router,
157
- ?entity_statement_path: String,
158
- ?fetch_path: String,
159
- ?jwks_path: String,
160
- ?signed_jwks_path: String,
161
- ?as: Symbol
162
- ) -> void
163
155
 
164
156
  def self.generate_fresh_keys: (
165
157
  entity_statement_path: String,
data/sig/jwks.rbs CHANGED
@@ -35,12 +35,6 @@ module OmniauthOpenidFederation
35
35
  ?entity_statement_keys: untyped
36
36
  ) -> Array[Hash[String, untyped]]
37
37
 
38
- def self.json_jwt: (
39
- String encoded_jwt,
40
- String jwks_uri,
41
- ?retried: bool,
42
- ?entity_statement_keys: untyped
43
- ) -> untyped
44
38
  end
45
39
 
46
40
  class Selector