omniauth_openid_federation 1.3.0 → 2.0.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +120 -14
  4. data/app/controllers/omniauth_openid_federation/federation_controller.rb +3 -3
  5. data/config/polyrun_coverage.yml +15 -0
  6. data/config/polyrun_spec_quality.yml +29 -0
  7. data/config/routes.rb +20 -10
  8. data/examples/README_INTEGRATION_TESTING.md +3 -0
  9. data/examples/integration_test_flow.rb +10 -8
  10. data/examples/jobs/federation_cache_refresh_job.rb.example +7 -7
  11. data/examples/jobs/federation_files_generation_job.rb.example +3 -2
  12. data/examples/mock_op_server.rb +4 -5
  13. data/examples/mock_rp_server.rb +3 -3
  14. data/examples/standalone_multiple_endpoints_example.rb +494 -0
  15. data/lib/omniauth_openid_federation/access_token.rb +91 -450
  16. data/lib/omniauth_openid_federation/cache.rb +16 -0
  17. data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
  18. data/lib/omniauth_openid_federation/constants.rb +3 -0
  19. data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
  20. data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
  21. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +7 -14
  22. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +40 -11
  23. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +8 -91
  24. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +0 -1
  25. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +54 -21
  26. data/lib/omniauth_openid_federation/federation_endpoint.rb +40 -172
  27. data/lib/omniauth_openid_federation/http_client.rb +145 -32
  28. data/lib/omniauth_openid_federation/http_errors.rb +14 -0
  29. data/lib/omniauth_openid_federation/id_token.rb +19 -0
  30. data/lib/omniauth_openid_federation/jwe.rb +26 -0
  31. data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
  32. data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
  33. data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
  34. data/lib/omniauth_openid_federation/jws.rb +3 -11
  35. data/lib/omniauth_openid_federation/jwt_response_decoder.rb +290 -0
  36. data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
  37. data/lib/omniauth_openid_federation/rack.rb +2 -0
  38. data/lib/omniauth_openid_federation/rack_endpoint.rb +21 -9
  39. data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
  40. data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
  41. data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
  42. data/lib/omniauth_openid_federation/strategy/callback_handling.rb +135 -0
  43. data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
  44. data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +211 -0
  45. data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +433 -0
  46. data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
  47. data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
  48. data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +268 -0
  49. data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +134 -0
  50. data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +61 -0
  51. data/lib/omniauth_openid_federation/strategy.rb +27 -1910
  52. data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
  53. data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
  54. data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
  55. data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
  56. data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
  57. data/lib/omniauth_openid_federation/tasks_helper.rb +28 -790
  58. data/lib/omniauth_openid_federation/time_helpers.rb +67 -0
  59. data/lib/omniauth_openid_federation/user_info.rb +15 -0
  60. data/lib/omniauth_openid_federation/utils.rb +14 -9
  61. data/lib/omniauth_openid_federation/validators.rb +55 -44
  62. data/lib/omniauth_openid_federation/version.rb +1 -1
  63. data/lib/omniauth_openid_federation.rb +10 -3
  64. data/lib/tasks/omniauth_openid_federation.rake +5 -5
  65. data/sig/omniauth_openid_federation.rbs +8 -1
  66. data/sig/strategy.rbs +6 -6
  67. metadata +252 -43
@@ -1,11 +1,15 @@
1
+ require "cgi"
2
+ require "json"
3
+ require "base64"
4
+ require "jwt"
1
5
  require_relative "entity_statement"
2
6
  require_relative "entity_statement_validator"
3
7
  require_relative "../http_client"
4
8
  require_relative "../logger"
5
9
  require_relative "../errors"
6
10
  require_relative "../utils"
7
- require "set"
8
- require "cgi"
11
+ require_relative "../string_helpers"
12
+ require_relative "../key_extractor"
9
13
 
10
14
  # Trust Chain Resolver for OpenID Federation 1.0
11
15
  # @see https://openid.net/specs/openid-federation-1_0.html#section-10 Section 10: Trust Chain Resolution
@@ -49,6 +53,7 @@ module OmniauthOpenidFederation
49
53
  @timeout = timeout
50
54
  @resolved_statements = []
51
55
  @visited_entities = Set.new
56
+ @entity_configurations = {}
52
57
  end
53
58
 
54
59
  # Resolve the trust chain
@@ -59,24 +64,21 @@ module OmniauthOpenidFederation
59
64
  def resolve!
60
65
  OmniauthOpenidFederation::Logger.debug("[TrustChainResolver] Starting trust chain resolution for: #{@leaf_entity_id}")
61
66
 
62
- # Step 1: Fetch Leaf Entity's Entity Configuration
63
67
  leaf_config = fetch_entity_configuration(@leaf_entity_id)
64
- validate_entity_statement(leaf_config, nil) # No issuer for Entity Configuration
68
+ validate_entity_statement(leaf_config, nil)
65
69
  @resolved_statements << leaf_config
66
70
  @visited_entities.add(@leaf_entity_id)
67
71
 
68
- # Step 2: Follow authority_hints to build the chain
69
72
  current_entity_id = @leaf_entity_id
70
73
  current_config = leaf_config
71
74
 
72
75
  while current_config && !is_trust_anchor?(current_config)
73
76
  authority_hints = extract_authority_hints(current_config)
74
77
 
75
- if authority_hints.nil? || authority_hints.empty?
78
+ if StringHelpers.blank?(authority_hints)
76
79
  raise ValidationError, "Entity #{current_entity_id} has no authority_hints and is not a Trust Anchor"
77
80
  end
78
81
 
79
- # Try each authority hint until we find a valid chain
80
82
  found_next = false
81
83
  authority_hints.each do |authority_id|
82
84
  next if @visited_entities.include?(authority_id)
@@ -86,28 +88,24 @@ module OmniauthOpenidFederation
86
88
  end
87
89
 
88
90
  begin
89
- # Fetch Subordinate Statement from authority
91
+ issuer_config = fetch_entity_configuration(authority_id)
90
92
  subordinate_statement = fetch_subordinate_statement(
91
93
  issuer: authority_id,
92
- subject: current_entity_id
94
+ subject: current_entity_id,
95
+ issuer_config: issuer_config
93
96
  )
94
97
 
95
- # Validate Subordinate Statement
96
- issuer_config = fetch_entity_configuration(authority_id)
97
98
  validate_entity_statement(subordinate_statement, issuer_config)
98
99
 
99
- # Add to chain
100
100
  @resolved_statements << subordinate_statement
101
101
  @visited_entities.add(authority_id)
102
102
 
103
- # Continue with issuer as next entity
104
103
  current_entity_id = authority_id
105
104
  current_config = issuer_config
106
105
  found_next = true
107
106
  break
108
107
  rescue ValidationError, FetchError => e
109
108
  OmniauthOpenidFederation::Logger.warn("[TrustChainResolver] Failed to resolve via #{authority_id}: #{e.message}")
110
- # Instrument trust chain validation failure
111
109
  OmniauthOpenidFederation::Instrumentation.notify_trust_chain_validation_failed(
112
110
  entity_id: current_entity_id,
113
111
  trust_anchor: authority_id,
@@ -124,10 +122,8 @@ module OmniauthOpenidFederation
124
122
  end
125
123
  end
126
124
 
127
- # Step 3: Verify we reached a Trust Anchor
128
125
  unless is_trust_anchor?(current_config)
129
126
  error_msg = "Trust chain did not terminate at a configured Trust Anchor"
130
- # Instrument trust chain validation failure
131
127
  OmniauthOpenidFederation::Instrumentation.notify_trust_chain_validation_failed(
132
128
  entity_id: @leaf_entity_id,
133
129
  trust_anchor: current_entity_id,
@@ -153,26 +149,26 @@ module OmniauthOpenidFederation
153
149
  end
154
150
 
155
151
  def fetch_entity_configuration(entity_id)
152
+ return @entity_configurations[entity_id] if @entity_configurations.key?(entity_id)
153
+
156
154
  entity_statement_url = OmniauthOpenidFederation::Utils.build_entity_statement_url(entity_id)
157
155
  OmniauthOpenidFederation::Logger.debug("[TrustChainResolver] Fetching Entity Configuration from: #{entity_statement_url}")
158
156
 
159
157
  begin
160
- EntityStatement.fetch!(entity_statement_url, timeout: @timeout)
158
+ config = EntityStatement.fetch!(entity_statement_url, timeout: @timeout)
159
+ @entity_configurations[entity_id] = config
161
160
  rescue OmniauthOpenidFederation::NetworkError => e
162
161
  raise FetchError, "Failed to fetch entity configuration from #{entity_statement_url}: #{e.message}"
163
162
  end
164
163
  end
165
164
 
166
- def fetch_subordinate_statement(issuer:, subject:)
167
- # Try to get fetch endpoint from issuer's Entity Configuration
168
- issuer_config = fetch_entity_configuration(issuer)
165
+ def fetch_subordinate_statement(issuer:, subject:, issuer_config:)
169
166
  fetch_endpoint = extract_fetch_endpoint(issuer_config)
170
167
 
171
168
  unless fetch_endpoint
172
169
  raise FetchError, "Issuer #{issuer} does not provide a fetch endpoint"
173
170
  end
174
171
 
175
- # Build fetch URL with iss and sub parameters
176
172
  fetch_url = "#{fetch_endpoint}?iss=#{CGI.escape(issuer)}&sub=#{CGI.escape(subject)}"
177
173
  OmniauthOpenidFederation::Logger.debug("[TrustChainResolver] Fetching Subordinate Statement from: #{fetch_url}")
178
174
 
@@ -210,6 +206,43 @@ module OmniauthOpenidFederation
210
206
  issuer_entity_configuration: issuer_config
211
207
  )
212
208
  validator.validate!
209
+ verify_entity_statement_signature!(statement, issuer_config)
210
+ end
211
+
212
+ def verify_entity_statement_signature!(statement, issuer_config)
213
+ jwt_string = statement.entity_statement
214
+ parts = jwt_string.split(".")
215
+ if parts.length != EntityStatementValidator::JWT_PARTS_COUNT
216
+ raise ValidationError, "Invalid JWT format: expected #{EntityStatementValidator::JWT_PARTS_COUNT} parts, got #{parts.length}"
217
+ end
218
+
219
+ header = JSON.parse(Base64.urlsafe_decode64(parts[0]))
220
+ kid = header["kid"] || header[:kid]
221
+
222
+ signing_keys = issuer_signing_keys(statement, issuer_config)
223
+ keys = signing_keys["keys"] || signing_keys[:keys] || []
224
+ signing_key_data = keys.find { |key| (key["kid"] || key[:kid]) == kid }
225
+
226
+ unless signing_key_data
227
+ raise ValidationError, "Signing key with kid '#{kid}' not found in issuer JWKS for signature verification"
228
+ end
229
+
230
+ public_key = OmniauthOpenidFederation::KeyExtractor.jwk_to_openssl_key(signing_key_data)
231
+ ::JWT.decode(jwt_string, public_key, true, {algorithm: "RS256"})
232
+ rescue ::JWT::DecodeError, ::JWT::VerificationError => e
233
+ error_msg = "Entity statement signature validation failed: #{e.class} - #{e.message}"
234
+ OmniauthOpenidFederation::Logger.error("[TrustChainResolver] #{error_msg}")
235
+ raise SignatureError, error_msg, e.backtrace
236
+ end
237
+
238
+ def issuer_signing_keys(statement, issuer_config)
239
+ if issuer_config.nil?
240
+ parsed = statement.parse
241
+ parsed[:jwks] || parsed["jwks"] || {}
242
+ else
243
+ issuer_parsed = issuer_config.parse
244
+ issuer_parsed[:jwks] || issuer_parsed["jwks"] || {}
245
+ end
213
246
  end
214
247
 
215
248
  def is_trust_anchor?(entity_config)