omniauth_openid_federation 1.2.2 → 1.3.2

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -1
  3. data/README.md +210 -708
  4. data/app/controllers/omniauth_openid_federation/federation_controller.rb +14 -1
  5. data/config/routes.rb +20 -10
  6. data/examples/config/initializers/devise.rb.example +44 -55
  7. data/examples/config/initializers/federation_endpoint.rb.example +2 -2
  8. data/examples/config/open_id_connect_config.rb.example +12 -15
  9. data/examples/config/routes.rb.example +9 -5
  10. data/examples/integration_test_flow.rb +4 -4
  11. data/examples/mock_op_server.rb +3 -3
  12. data/examples/mock_rp_server.rb +3 -3
  13. data/lib/omniauth_openid_federation/configuration.rb +8 -0
  14. data/lib/omniauth_openid_federation/constants.rb +5 -0
  15. data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
  16. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +7 -14
  17. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +40 -11
  18. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +6 -87
  19. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +3 -15
  20. data/lib/omniauth_openid_federation/federation_endpoint.rb +39 -193
  21. data/lib/omniauth_openid_federation/jwks/decode.rb +0 -15
  22. data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
  23. data/lib/omniauth_openid_federation/jws.rb +23 -20
  24. data/lib/omniauth_openid_federation/rack_endpoint.rb +30 -5
  25. data/lib/omniauth_openid_federation/strategy.rb +143 -194
  26. data/lib/omniauth_openid_federation/tasks_helper.rb +501 -2
  27. data/lib/omniauth_openid_federation/time_helpers.rb +60 -0
  28. data/lib/omniauth_openid_federation/utils.rb +4 -7
  29. data/lib/omniauth_openid_federation/validators.rb +294 -8
  30. data/lib/omniauth_openid_federation/version.rb +1 -1
  31. data/lib/omniauth_openid_federation.rb +1 -0
  32. data/lib/tasks/omniauth_openid_federation.rake +301 -2
  33. data/sig/federation.rbs +0 -8
  34. data/sig/jwks.rbs +0 -6
  35. data/sig/omniauth_openid_federation.rbs +6 -1
  36. data/sig/strategy.rbs +0 -2
  37. metadata +100 -1
@@ -2,6 +2,8 @@ require_relative "federation/entity_statement_builder"
2
2
  require_relative "logger"
3
3
  require_relative "errors"
4
4
  require_relative "utils"
5
+ require_relative "string_helpers"
6
+ require_relative "time_helpers"
5
7
  require "jwt"
6
8
  require "base64"
7
9
  require "digest"
@@ -64,8 +66,6 @@ module OmniauthOpenidFederation
64
66
  # # In config/routes.rb (Rails)
65
67
  # get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show"
66
68
  #
67
- # # Or use the provided route helper
68
- # OmniauthOpenidFederation::FederationEndpoint.mount_routes
69
69
  class FederationEndpoint
70
70
  class << self
71
71
  # Configure the federation endpoint
@@ -125,16 +125,13 @@ module OmniauthOpenidFederation
125
125
  auto_provision_keys: true,
126
126
  key_rotation_period: nil
127
127
  )
128
- raise ConfigurationError, "Issuer is required" if issuer.nil? || issuer.empty?
128
+ raise ConfigurationError, "Issuer is required" if StringHelpers.blank?(issuer)
129
129
 
130
- # Priority 1: Validate key configuration - exception if all three are set
131
130
  if signing_key && encryption_key && private_key
132
131
  raise ConfigurationError, "Cannot specify signing_key, encryption_key, and private_key simultaneously. " \
133
132
  "Use either (signing_key + encryption_key) OR private_key, not both."
134
133
  end
135
134
 
136
- # If auto_provision_keys is enabled and no keys are provided, allow automatic generation
137
- # Keys will be generated in provision_jwks if needed
138
135
  unless auto_provision_keys
139
136
  if signing_key.nil? && encryption_key.nil? && private_key.nil? && jwks.nil?
140
137
  raise ConfigurationError, "At least one key source is required: signing_key, encryption_key, private_key, or jwks"
@@ -151,11 +148,9 @@ module OmniauthOpenidFederation
151
148
 
152
149
  config = configuration
153
150
 
154
- # Set issuer and subject
155
151
  config.issuer = issuer
156
152
  config.subject = subject || issuer
157
153
 
158
- # Automatic key provisioning
159
154
  if auto_provision_keys && jwks.nil?
160
155
  jwks = provision_jwks(
161
156
  signing_key: signing_key,
@@ -168,85 +163,60 @@ module OmniauthOpenidFederation
168
163
  entity_statement_path_provided: !entity_statement_path.nil?
169
164
  )
170
165
 
171
- # After provisioning, check if we have keys available
172
- # If provisioning failed and no keys were provided, raise appropriate error
173
166
  if jwks.nil? && signing_key.nil? && encryption_key.nil? && private_key.nil? && config.signing_key.nil?
174
167
  raise ConfigurationError, "Signing key is required. Provide signing_key, encryption_key, or private_key, or enable auto_provision_keys with entity_statement_path."
175
168
  end
176
169
  end
177
170
 
178
- # Use provided jwks if available, otherwise use provisioned jwks
179
171
  config.jwks = jwks || raise(ConfigurationError, "JWKS is required. Provide jwks parameter or enable auto_provision_keys.")
180
172
 
181
- # Set keys in configuration following priority:
182
- # 1. Use provided keys (signing_key + encryption_key, or private_key)
183
- # 2. Use keys loaded from disk (if entity statement was loaded)
184
- # 3. Use keys from generated keys (if auto-generated)
185
173
  if signing_key && encryption_key
186
- # Priority 1: Use provided separate keys
187
174
  config.signing_key = signing_key
188
175
  config.encryption_key = encryption_key
189
176
  config.private_key = signing_key
190
177
  elsif signing_key
191
- # Priority 1b: Use provided signing_key for both signing and encryption
192
178
  config.signing_key = signing_key
193
179
  config.encryption_key = signing_key
194
180
  config.private_key = signing_key
195
181
  elsif private_key
196
- # Priority 2: Use provided single private_key
197
182
  config.private_key = private_key
198
183
  config.signing_key = private_key
199
184
  config.encryption_key = private_key
200
185
  elsif config.signing_key && config.encryption_key
201
- # Priority 3: Keys were loaded from disk in provision_jwks
202
186
  config.private_key = config.signing_key
203
187
  elsif config.signing_key
204
- # Priority 4: Only signing_key was set (from auto-generation or fallback)
205
- # Use it for both signing and encryption
206
188
  config.private_key = config.signing_key
207
189
  config.encryption_key = config.signing_key
208
190
  else
209
191
  raise ConfigurationError, "Signing key is required. Provide signing_key, encryption_key, or private_key, or enable auto_provision_keys with entity_statement_path."
210
192
  end
211
193
 
212
- # Set kid from first signing key in JWKS
213
194
  keys = config.jwks[:keys] || config.jwks["keys"] || []
214
195
  signing_key_jwk = keys.find { |k| (k[:use] || k["use"]) == "sig" } || keys.first
215
196
  config.kid = signing_key_jwk&.dig(:kid) || signing_key_jwk&.dig("kid")
216
197
 
217
- # Set metadata
218
- # Detect entity type from metadata or default based on provided keys
219
198
  entity_type = detect_entity_type(metadata)
220
199
 
221
200
  if metadata
222
- # Automatically add required jwks_uri and signed_jwks_uri if not present
223
- # These are required by OpenID Federation 1.0 spec and the library provides these endpoints
224
201
  metadata = ensure_jwks_endpoints(metadata, issuer, entity_type)
225
202
  config.metadata = metadata
226
- # Ensure entity type is consistent
227
203
  entity_type = detect_entity_type(config.metadata)
228
204
  else
229
- # Auto-generate minimal metadata with only standard well-known endpoints
230
- # Default to openid_relying_party (RP) entity type for clients
231
205
  base_metadata = {
232
206
  issuer: issuer
233
207
  }
234
208
 
235
- # Only add federation_fetch_endpoint for openid_provider (OP) entities
236
- # RPs typically don't have subordinates, so they don't need fetch endpoint
237
209
  if entity_type == :openid_provider
238
210
  base_metadata[:federation_fetch_endpoint] = "#{issuer}/.well-known/openid-federation/fetch"
239
211
  config.metadata = {
240
212
  openid_provider: base_metadata
241
213
  }
242
214
  else
243
- # Default to openid_relying_party (RP)
244
215
  config.metadata = {
245
216
  openid_relying_party: base_metadata
246
217
  }
247
218
  end
248
219
 
249
- # Ensure jwks_uri and signed_jwks_uri are added (same as when metadata is provided)
250
220
  config.metadata = ensure_jwks_endpoints(config.metadata, issuer, entity_type)
251
221
 
252
222
  OmniauthOpenidFederation::Logger.warn(
@@ -257,25 +227,19 @@ module OmniauthOpenidFederation
257
227
  )
258
228
  end
259
229
 
260
- # Store entity type for later use
261
230
  config.entity_type = entity_type
262
231
 
263
- # Set optional configuration
264
232
  config.expiration_seconds = expiration_seconds if expiration_seconds
265
233
  config.jwks_cache_ttl = jwks_cache_ttl if jwks_cache_ttl
266
234
  config.key_rotation_period = key_rotation_period if key_rotation_period
267
235
  config.entity_statement_path = entity_statement_path if entity_statement_path
268
236
 
269
- # If keys were provided in config, regenerate entity statement and save keys to disk
270
- # This ensures the entity statement signature matches the provided keys
271
237
  if entity_statement_path && (signing_key || private_key)
272
238
  begin
273
- # Save provided keys to disk for persistence
274
239
  keys_dir = File.dirname(entity_statement_path)
275
240
  FileUtils.mkdir_p(keys_dir) unless File.directory?(keys_dir)
276
241
 
277
242
  if signing_key && encryption_key
278
- # Save separate keys
279
243
  signing_key_path = File.join(keys_dir, ".federation-signing-key.pem")
280
244
  encryption_key_path = File.join(keys_dir, ".federation-encryption-key.pem")
281
245
  File.write(signing_key_path, signing_key.to_pem)
@@ -284,7 +248,6 @@ module OmniauthOpenidFederation
284
248
  File.chmod(0o600, encryption_key_path)
285
249
  OmniauthOpenidFederation::Logger.debug("[FederationEndpoint] Saved provided signing and encryption keys to disk")
286
250
  elsif private_key
287
- # Save single key (for transition period / dev/testing)
288
251
  signing_key_path = File.join(keys_dir, ".federation-signing-key.pem")
289
252
  encryption_key_path = File.join(keys_dir, ".federation-encryption-key.pem")
290
253
  File.write(signing_key_path, private_key.to_pem)
@@ -294,7 +257,6 @@ module OmniauthOpenidFederation
294
257
  OmniauthOpenidFederation::Logger.debug("[FederationEndpoint] Saved provided private_key to disk (used for both signing and encryption)")
295
258
  end
296
259
 
297
- # Regenerate entity statement with provided keys to ensure signature matches
298
260
  entity_statement = generate_entity_statement
299
261
  FileUtils.mkdir_p(File.dirname(entity_statement_path)) if File.dirname(entity_statement_path) != "."
300
262
  File.write(entity_statement_path, entity_statement)
@@ -305,7 +267,6 @@ module OmniauthOpenidFederation
305
267
  end
306
268
  end
307
269
 
308
- # Handle automatic key rotation if enabled
309
270
  if auto_provision_keys && entity_statement_path && config.key_rotation_period
310
271
  rotate_keys_if_needed(config)
311
272
  end
@@ -316,12 +277,6 @@ module OmniauthOpenidFederation
316
277
 
317
278
  # Automatic key provisioning: Extract or generate JWKS from available sources
318
279
  #
319
- # Priority order:
320
- # 1. Extract from entity_statement_path (cached, supports key rotation)
321
- # 2. Generate from separate signing_key and encryption_key (RECOMMENDED)
322
- # 3. Generate from single private_key (DEV/TESTING ONLY)
323
- # 4. Auto-generate new keys if no keys provided and auto_provision_keys is enabled
324
- #
325
280
  # @param signing_key [OpenSSL::PKey::RSA, nil] Signing private key
326
281
  # @param encryption_key [OpenSSL::PKey::RSA, nil] Encryption private key
327
282
  # @param private_key [OpenSSL::PKey::RSA, nil] Single private key (dev/testing only)
@@ -332,37 +287,27 @@ module OmniauthOpenidFederation
332
287
  # @param entity_statement_path_provided [Boolean] Whether entity_statement_path was provided as parameter (not auto-generated)
333
288
  # @return [Hash, nil] JWKS hash with keys array, or nil if provisioning fails
334
289
  def provision_jwks(signing_key: nil, encryption_key: nil, private_key: nil, entity_statement_path: nil, issuer: nil, subject: nil, metadata: nil, entity_statement_path_provided: false)
335
- # Priority 1-3: Use provided keys from config (highest priority)
336
290
  if encryption_key
337
- # Generate from separate signing_key and encryption_key (RECOMMENDED for production)
338
291
  signing_key_for_jwk = signing_key || private_key
339
292
  raise ConfigurationError, "Signing key is required when encryption_key is provided. Provide signing_key or private_key." unless signing_key_for_jwk
340
293
 
341
- # Check if signing and encryption keys are the same (compare public key PEM)
342
294
  # If same, generate single JWK to avoid duplicate kid values
343
295
  if signing_key_for_jwk.public_key.to_pem == encryption_key.public_key.to_pem
344
- # Same key used for both signing and encryption - generate single JWK
345
296
  single_jwk = OmniauthOpenidFederation::Utils.rsa_key_to_jwk(signing_key_for_jwk, use: nil)
346
297
  return {keys: [single_jwk]}
347
298
  else
348
- # Different keys - generate separate JWKs
349
299
  signing_jwk = OmniauthOpenidFederation::Utils.rsa_key_to_jwk(signing_key_for_jwk, use: "sig")
350
300
  encryption_jwk = OmniauthOpenidFederation::Utils.rsa_key_to_jwk(encryption_key, use: "enc")
351
301
  return {keys: [signing_jwk, encryption_jwk]}
352
302
  end
353
303
  elsif private_key || signing_key
354
- # Use single key (private_key or signing_key) for both signing and encryption
355
- # When using a single key, include only ONE JWK (not two with duplicate kid)
356
304
  single_key = private_key || signing_key
357
305
 
358
- # Generate JWK without 'use' field to indicate it can be used for both purposes
359
- # This avoids duplicate kid values which violate the spec
306
+ # Generate JWK without 'use' field to avoid duplicate kid values which violate the spec
360
307
  single_jwk = OmniauthOpenidFederation::Utils.rsa_key_to_jwk(single_key, use: nil)
361
308
  return {keys: [single_jwk]}
362
309
  end
363
310
 
364
- # Priority 4: Extract from entity statement file (cached, supports automatic key rotation)
365
- # Only if no keys were provided in config (keys from config take priority)
366
311
  extraction_failed = false
367
312
  if entity_statement_path&.then { |path| File.exist?(path) }
368
313
  begin
@@ -371,8 +316,6 @@ module OmniauthOpenidFederation
371
316
  if jwks&.dig(:keys)&.any?
372
317
  OmniauthOpenidFederation::Logger.debug("[FederationEndpoint] Extracted JWKS from entity statement file: #{entity_statement_path}")
373
318
 
374
- # Only load private keys from disk if no keys were provided in config
375
- # This ensures provided keys take priority over cached keys
376
319
  keys_dir = File.dirname(entity_statement_path)
377
320
  signing_key_path = File.join(keys_dir, ".federation-signing-key.pem")
378
321
  encryption_key_path = File.join(keys_dir, ".federation-encryption-key.pem")
@@ -411,16 +354,11 @@ module OmniauthOpenidFederation
411
354
  end
412
355
  end
413
356
 
414
- # Priority 5: Auto-generate new keys (when auto_provision_keys is enabled and no keys provided)
415
- # This generates separate signing and encryption keys
416
- # Only auto-generate if entity_statement_path was not provided as parameter (or extraction succeeded)
417
- # If entity_statement_path was provided but extraction failed, don't auto-generate
418
357
  if issuer && (!entity_statement_path_provided || !extraction_failed)
419
- # Generate a default entity_statement_path if not provided
420
358
  entity_statement_path ||= begin
421
359
  configuration
422
360
  if defined?(Rails) && Rails.root
423
- default_path = Rails.root.join("config", ".federation-entity-statement.jwt").to_s
361
+ default_path = Rails.root.join("config/.federation-entity-statement.jwt").to_s
424
362
  OmniauthOpenidFederation::Logger.info("[FederationEndpoint] No entity_statement_path provided, using default: #{OmniauthOpenidFederation::Utils.sanitize_path(default_path)}")
425
363
  default_path
426
364
  end
@@ -443,17 +381,10 @@ module OmniauthOpenidFederation
443
381
  nil
444
382
  end
445
383
 
446
- # Get the current configuration
447
- #
448
- # @return [Configuration] Current configuration
449
384
  def configuration
450
385
  @configuration ||= Configuration.new
451
386
  end
452
387
 
453
- # Generate the entity statement JWT
454
- #
455
- # @return [String] The signed entity statement JWT
456
- # @raise [ConfigurationError] If configuration is incomplete
457
388
  def generate_entity_statement
458
389
  config = configuration
459
390
  validate_configuration(config)
@@ -472,22 +403,15 @@ module OmniauthOpenidFederation
472
403
  builder.build
473
404
  end
474
405
 
475
- # Generate signed JWKS JWT
476
- #
477
- # @return [String] The signed JWKS JWT
478
- # @raise [ConfigurationError] If configuration is incomplete
479
406
  def generate_signed_jwks
480
407
  config = configuration
481
408
  validate_configuration(config)
482
409
 
483
- # Get JWKS to include in payload (current keys, not entity statement keys)
484
410
  jwks_payload = resolve_signed_jwks_payload(config)
485
411
 
486
- # Sign with entity statement key
487
412
  signing_kid = config.signed_jwks_signing_kid || config.kid || extract_kid_from_jwks(config.jwks)
488
413
  expiration_seconds = config.signed_jwks_expiration_seconds || 86400
489
414
 
490
- # Build JWT payload with JWKS
491
415
  now = Time.now.to_i
492
416
  payload = {
493
417
  iss: config.issuer,
@@ -497,7 +421,6 @@ module OmniauthOpenidFederation
497
421
  jwks: jwks_payload
498
422
  }
499
423
 
500
- # Sign JWT using jwt gem
501
424
  header = {
502
425
  alg: "RS256",
503
426
  typ: "JWT",
@@ -513,11 +436,9 @@ module OmniauthOpenidFederation
513
436
  end
514
437
  end
515
438
 
516
- # Get current JWKS for serving
517
- #
518
- # @return [Hash] Current JWKS hash
519
439
  def current_jwks
520
440
  config = configuration
441
+ validate_configuration(config)
521
442
  resolve_current_jwks(config)
522
443
  end
523
444
 
@@ -558,26 +479,6 @@ module OmniauthOpenidFederation
558
479
  # - GET /.well-known/jwks.json (standard JWKS)
559
480
  # - GET /.well-known/signed-jwks.json (signed JWKS)
560
481
  #
561
- # ALTERNATIVE: Use mount_routes helper (for backward compatibility or custom paths):
562
- # Rails.application.routes.draw do
563
- # OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
564
- # end
565
- #
566
- # @param router [ActionDispatch::Routing::Mapper] The routes mapper (pass `self` from routes.rb)
567
- # @param entity_statement_path [String] Path for entity statement endpoint (default: "/.well-known/openid-federation")
568
- # @param fetch_path [String] Path for fetch endpoint (default: "/.well-known/openid-federation/fetch")
569
- # @param jwks_path [String] Path for standard JWKS endpoint (default: "/.well-known/jwks.json")
570
- # @param signed_jwks_path [String] Path for signed JWKS endpoint (default: "/.well-known/signed-jwks.json")
571
- # @param as [String, Symbol] Route name prefix (default: :openid_federation)
572
- # @deprecated Use `mount OmniauthOpenidFederation::Engine => "/"` instead (Rails-idiomatic way)
573
- def mount_routes(router, entity_statement_path: "/.well-known/openid-federation", fetch_path: "/.well-known/openid-federation/fetch", jwks_path: "/.well-known/jwks.json", signed_jwks_path: "/.well-known/signed-jwks.json", as: :openid_federation)
574
- # Controller uses Rails-conventional naming (OmniauthOpenidFederation)
575
- # which matches natural inflection from omniauth_openid_federation
576
- router.get entity_statement_path, to: "omniauth_openid_federation/federation#show", as: as
577
- router.get fetch_path, to: "omniauth_openid_federation/federation#fetch", as: :"#{as}_fetch"
578
- router.get jwks_path, to: "omniauth_openid_federation/federation#jwks", as: :"#{as}_jwks"
579
- router.get signed_jwks_path, to: "omniauth_openid_federation/federation#signed_jwks", as: :"#{as}_signed_jwks"
580
- end
581
482
 
582
483
  # Generate fresh signing and encryption keys and write entity statement to file
583
484
  #
@@ -588,7 +489,6 @@ module OmniauthOpenidFederation
588
489
  # @param keys_output_dir [String, nil] Directory to store private keys (optional, defaults to same dir as entity_statement_path)
589
490
  # @return [Hash, nil] JWKS hash with keys array, or nil if generation fails
590
491
  def generate_fresh_keys(entity_statement_path:, issuer: nil, subject: nil, metadata: nil, keys_output_dir: nil)
591
- # Generate separate signing and encryption keys
592
492
  signing_key = OpenSSL::PKey::RSA.new(2048)
593
493
  encryption_key = OpenSSL::PKey::RSA.new(2048)
594
494
 
@@ -596,13 +496,11 @@ module OmniauthOpenidFederation
596
496
  encryption_jwk = OmniauthOpenidFederation::Utils.rsa_key_to_jwk(encryption_key, use: "enc")
597
497
  jwks = {keys: [signing_jwk, encryption_jwk]}
598
498
 
599
- # Get configuration for issuer, subject, and metadata
600
499
  config = configuration
601
500
  issuer ||= config.issuer
602
501
  subject ||= config.subject || issuer
603
502
  metadata ||= config.metadata
604
503
 
605
- # Generate minimal metadata if none provided
606
504
  unless metadata
607
505
  if issuer
608
506
  # Default to openid_relying_party (RP) entity type for clients
@@ -620,7 +518,6 @@ module OmniauthOpenidFederation
620
518
  end
621
519
  end
622
520
 
623
- # Generate entity statement with new keys
624
521
  if issuer
625
522
  builder = Federation::EntityStatementBuilder.new(
626
523
  issuer: issuer,
@@ -634,11 +531,9 @@ module OmniauthOpenidFederation
634
531
 
635
532
  entity_statement = builder.build
636
533
 
637
- # Determine keys output directory (default to same directory as entity statement)
638
534
  keys_dir = keys_output_dir || File.dirname(entity_statement_path)
639
535
  FileUtils.mkdir_p(keys_dir) unless File.directory?(keys_dir)
640
536
 
641
- # Write private keys to disk (secure storage)
642
537
  signing_key_path = File.join(keys_dir, ".federation-signing-key.pem")
643
538
  encryption_key_path = File.join(keys_dir, ".federation-encryption-key.pem")
644
539
 
@@ -647,12 +542,10 @@ module OmniauthOpenidFederation
647
542
  File.chmod(0o600, signing_key_path)
648
543
  File.chmod(0o600, encryption_key_path)
649
544
 
650
- # Write entity statement to file
651
545
  FileUtils.mkdir_p(File.dirname(entity_statement_path)) if File.dirname(entity_statement_path) != "."
652
546
  File.write(entity_statement_path, entity_statement)
653
547
  File.chmod(0o600, entity_statement_path) if File.exist?(entity_statement_path)
654
548
 
655
- # Update configuration with new keys
656
549
  config.signing_key = signing_key
657
550
  config.encryption_key = encryption_key
658
551
  config.private_key = signing_key
@@ -673,19 +566,15 @@ module OmniauthOpenidFederation
673
566
  nil
674
567
  end
675
568
 
676
- # Rotate keys if rotation period has elapsed
677
- #
678
- # @param config [Configuration] Configuration object
679
569
  def rotate_keys_if_needed(config)
680
570
  return unless config.key_rotation_period && config.entity_statement_path
681
571
 
682
572
  entity_statement_path = config.entity_statement_path
683
573
  return unless File.exist?(entity_statement_path)
684
574
 
685
- # Check if file needs rotation based on modification time
686
575
  file_mtime = File.mtime(entity_statement_path)
687
576
  rotation_period_seconds = config.key_rotation_period.to_i
688
- time_since_rotation = Time.now - file_mtime
577
+ time_since_rotation = TimeHelpers.now - file_mtime
689
578
 
690
579
  if time_since_rotation >= rotation_period_seconds
691
580
  OmniauthOpenidFederation::Logger.info(
@@ -693,7 +582,6 @@ module OmniauthOpenidFederation
693
582
  "generating new keys"
694
583
  )
695
584
 
696
- # Generate fresh keys and update entity statement
697
585
  keys_dir = File.dirname(entity_statement_path)
698
586
  jwks = generate_fresh_keys(
699
587
  entity_statement_path: entity_statement_path,
@@ -705,7 +593,6 @@ module OmniauthOpenidFederation
705
593
 
706
594
  if jwks
707
595
  config.jwks = jwks
708
- # Update kid from new signing key
709
596
  keys = jwks[:keys] || jwks["keys"] || []
710
597
  signing_key_jwk = keys.find { |k| (k[:use] || k["use"]) == "sig" } || keys.first
711
598
  config.kid = signing_key_jwk&.dig(:kid) || signing_key_jwk&.dig("kid")
@@ -722,38 +609,25 @@ module OmniauthOpenidFederation
722
609
  end
723
610
  end
724
611
 
725
- # Ensure jwks_uri and signed_jwks_uri are present in metadata
726
- # These are required by OpenID Federation 1.0 specification
727
- # Also ensures federation_fetch_endpoint is present for openid_provider entities
728
- #
729
- # @param metadata [Hash] Metadata hash
730
- # @param issuer [String] Issuer URL
731
- # @param entity_type [Symbol] Entity type (:openid_provider or :openid_relying_party)
732
- # @return [Hash] Metadata with jwks endpoints added if missing
733
612
  def ensure_jwks_endpoints(metadata, issuer, entity_type)
734
- metadata = metadata.dup # Don't modify original
613
+ metadata = metadata.dup
735
614
  entity_type ||= detect_entity_type(metadata)
736
615
 
737
- # Determine which metadata section to update
738
616
  section = if entity_type == :openid_provider
739
617
  metadata[:openid_provider] || metadata["openid_provider"] || {}
740
618
  else
741
619
  metadata[:openid_relying_party] || metadata["openid_relying_party"] || {}
742
620
  end
743
621
 
744
- # Convert to symbol keys for consistency
745
622
  section = section.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
746
623
 
747
- # Add jwks_uri and signed_jwks_uri if not present
748
624
  section[:jwks_uri] ||= "#{issuer}/.well-known/jwks.json"
749
625
  section[:signed_jwks_uri] ||= "#{issuer}/.well-known/signed-jwks.json"
750
626
 
751
- # Add federation_fetch_endpoint for openid_provider entities if not present
752
627
  if entity_type == :openid_provider
753
628
  section[:federation_fetch_endpoint] ||= "#{issuer}/.well-known/openid-federation/fetch"
754
629
  end
755
630
 
756
- # Update metadata with modified section
757
631
  if entity_type == :openid_provider
758
632
  metadata[:openid_provider] = section
759
633
  metadata.delete("openid_provider") if metadata.key?("openid_provider")
@@ -765,34 +639,54 @@ module OmniauthOpenidFederation
765
639
  metadata
766
640
  end
767
641
 
642
+ def get_subordinate_statement(subject_entity_id)
643
+ config = configuration
644
+ validate_configuration(config)
645
+
646
+ entity_type = detect_entity_type(config.metadata)
647
+ unless entity_type == :openid_provider
648
+ OmniauthOpenidFederation::Logger.debug("[FederationEndpoint] Fetch endpoint called for non-OP entity (#{entity_type}), returning nil")
649
+ return nil
650
+ end
651
+
652
+ if config.subordinate_statements_proc
653
+ return config.subordinate_statements_proc.call(subject_entity_id)
654
+ end
655
+
656
+ if config.subordinate_statements && config.subordinate_statements[subject_entity_id]
657
+ subordinate_config = config.subordinate_statements[subject_entity_id]
658
+ return generate_subordinate_statement(
659
+ subject_entity_id: subject_entity_id,
660
+ subject_metadata: subordinate_config[:metadata] || subordinate_config["metadata"],
661
+ metadata_policy: subordinate_config[:metadata_policy] || subordinate_config["metadata_policy"],
662
+ constraints: subordinate_config[:constraints] || subordinate_config["constraints"]
663
+ )
664
+ end
665
+
666
+ nil
667
+ end
668
+
768
669
  private
769
670
 
770
- # Detect entity type from metadata
771
- #
772
- # @param metadata [Hash, nil] Entity metadata
773
- # @return [Symbol] Entity type: :openid_provider or :openid_relying_party
774
671
  def detect_entity_type(metadata)
775
- return :openid_relying_party if metadata.nil? || metadata.empty?
672
+ return :openid_relying_party if StringHelpers.blank?(metadata)
776
673
 
777
- # Check for openid_relying_party first (primary use case)
778
674
  if metadata.key?(:openid_relying_party) || metadata.key?("openid_relying_party")
779
675
  return :openid_relying_party
780
676
  end
781
677
 
782
- # Check for openid_provider
783
678
  if metadata.key?(:openid_provider) || metadata.key?("openid_provider")
784
679
  return :openid_provider
785
680
  end
786
681
 
787
- # Default to openid_relying_party (primary use case for clients)
788
682
  :openid_relying_party
789
683
  end
790
684
 
791
685
  def validate_configuration(config)
792
- raise ConfigurationError, "Issuer is required. Configure with OmniauthOpenidFederation::FederationEndpoint.configure" if config.issuer.nil? || config.issuer.empty?
686
+ raise ConfigurationError, "Issuer is required. Configure with OmniauthOpenidFederation::FederationEndpoint.configure" if StringHelpers.blank?(config.issuer)
793
687
  raise ConfigurationError, "Private key is required. Configure with OmniauthOpenidFederation::FederationEndpoint.configure" if config.private_key.nil?
794
- raise ConfigurationError, "JWKS is required. Configure with OmniauthOpenidFederation::FederationEndpoint.configure" if config.jwks.nil? || config.jwks.empty?
795
- raise ConfigurationError, "Metadata is required. Configure with OmniauthOpenidFederation::FederationEndpoint.configure" if config.metadata.nil? || config.metadata.empty?
688
+ raise ConfigurationError, "JWKS is required. Configure with OmniauthOpenidFederation::FederationEndpoint.configure" if StringHelpers.blank?(config.jwks)
689
+ raise ConfigurationError, "Metadata is required. Configure with OmniauthOpenidFederation::FederationEndpoint.configure" if StringHelpers.blank?(config.metadata)
796
690
  end
797
691
 
798
692
  def resolve_current_jwks(config)
@@ -814,32 +708,19 @@ module OmniauthOpenidFederation
814
708
  first_key["kid"] || first_key[:kid]
815
709
  end
816
710
 
817
- # Generate Subordinate Statement for a subject entity
818
- # Only available for openid_provider (OP) entities that have subordinates
819
- #
820
- # @param subject_entity_id [String] Entity Identifier of the subject
821
- # @param subject_metadata [Hash, nil] Optional: Subject entity metadata to include
822
- # @param metadata_policy [Hash, nil] Optional: Metadata policy to apply
823
- # @param constraints [Hash, nil] Optional: Trust Chain constraints
824
- # @param source_endpoint [String, nil] Optional: Fetch endpoint URL
825
- # @return [String] The signed Subordinate Statement JWT
826
- # @raise [ConfigurationError] If configuration is incomplete or entity is not an OP
827
711
  def generate_subordinate_statement(subject_entity_id:, subject_metadata: nil, metadata_policy: nil, constraints: nil, source_endpoint: nil)
828
712
  config = configuration
829
713
  validate_configuration(config)
830
714
 
831
- # Only OPs can generate subordinate statements
832
715
  entity_type = detect_entity_type(config.metadata)
833
716
  unless entity_type == :openid_provider
834
717
  raise ConfigurationError, "Subordinate statements can only be generated by openid_provider entities. Current entity type: #{entity_type}"
835
718
  end
836
719
 
837
- # Get federation_fetch_endpoint from metadata or use default
838
720
  op_metadata = config.metadata[:openid_provider] || config.metadata["openid_provider"] || {}
839
721
  fetch_endpoint = op_metadata[:federation_fetch_endpoint] || op_metadata["federation_fetch_endpoint"] ||
840
722
  "#{config.issuer}/.well-known/openid-federation/fetch"
841
723
 
842
- # Build metadata for subject if provided
843
724
  metadata = subject_metadata || {}
844
725
 
845
726
  builder = Federation::EntityStatementBuilder.new(
@@ -858,41 +739,6 @@ module OmniauthOpenidFederation
858
739
  builder.build
859
740
  end
860
741
 
861
- # Get Subordinate Statement for a subject (for Fetch Endpoint)
862
- # Only available for openid_provider (OP) entities
863
- #
864
- # @param subject_entity_id [String] Entity Identifier of the subject
865
- # @return [String, nil] The Subordinate Statement JWT or nil if not found
866
- # @raise [ConfigurationError] If configuration is incomplete or entity is not an OP
867
- def get_subordinate_statement(subject_entity_id)
868
- config = configuration
869
-
870
- # Only OPs can serve subordinate statements
871
- entity_type = detect_entity_type(config.metadata)
872
- unless entity_type == :openid_provider
873
- OmniauthOpenidFederation::Logger.debug("[FederationEndpoint] Fetch endpoint called for non-OP entity (#{entity_type}), returning nil")
874
- return nil
875
- end
876
-
877
- # Use subordinate_statements_proc if configured
878
- if config.subordinate_statements_proc
879
- return config.subordinate_statements_proc.call(subject_entity_id)
880
- end
881
-
882
- # Use subordinate_statements hash if configured
883
- if config.subordinate_statements && config.subordinate_statements[subject_entity_id]
884
- subordinate_config = config.subordinate_statements[subject_entity_id]
885
- return generate_subordinate_statement(
886
- subject_entity_id: subject_entity_id,
887
- subject_metadata: subordinate_config[:metadata] || subordinate_config["metadata"],
888
- metadata_policy: subordinate_config[:metadata_policy] || subordinate_config["metadata_policy"],
889
- constraints: subordinate_config[:constraints] || subordinate_config["constraints"]
890
- )
891
- end
892
-
893
- nil
894
- end
895
-
896
742
  # Configuration class for FederationEndpoint
897
743
  # Supports automatic key provisioning with separate signing and encryption keys
898
744
  # Supports both openid_provider (OP) and openid_relying_party (RP) entity types
@@ -155,21 +155,6 @@ module OmniauthOpenidFederation
155
155
  )
156
156
  end
157
157
  end
158
-
159
- # Decode JWT using jwt gem (legacy method name kept for backward compatibility)
160
- #
161
- # @param encoded_jwt [String] The JWT to decode
162
- # @param jwks_uri [String] The JWKS URI for key lookup
163
- # @param retried [Boolean] Internal flag for retry logic (default: false)
164
- # @param entity_statement_keys [Hash, Array, nil] Entity statement keys for validation
165
- # @return [Array<Hash>] Array with [payload, header]
166
- # @raise [ValidationError] If JWT validation fails
167
- # @raise [SignatureError] If signature verification fails
168
- # @deprecated Use jwt() method instead. This method will be removed in a future version.
169
- def self.json_jwt(encoded_jwt, jwks_uri, retried: false, entity_statement_keys: nil)
170
- OmniauthOpenidFederation::Logger.warn("[Jwks::Decode] json_jwt is deprecated. Use jwt() method instead.")
171
- jwt(encoded_jwt, jwks_uri, retried: retried, entity_statement_keys: entity_statement_keys)
172
- end
173
158
  end
174
159
  end
175
160
  end