omniauth_openid_federation 1.3.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +1 -1
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +1 -1
- data/config/routes.rb +20 -10
- data/examples/integration_test_flow.rb +4 -4
- data/examples/mock_op_server.rb +3 -3
- data/examples/mock_rp_server.rb +3 -3
- data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
- data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +7 -14
- data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +40 -11
- data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +6 -87
- data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +3 -15
- data/lib/omniauth_openid_federation/federation_endpoint.rb +39 -171
- data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
- data/lib/omniauth_openid_federation/jws.rb +2 -1
- data/lib/omniauth_openid_federation/rack_endpoint.rb +19 -7
- data/lib/omniauth_openid_federation/tasks_helper.rb +23 -5
- data/lib/omniauth_openid_federation/time_helpers.rb +60 -0
- data/lib/omniauth_openid_federation/utils.rb +4 -7
- data/lib/omniauth_openid_federation/validators.rb +12 -36
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/omniauth_openid_federation.rb +1 -0
- data/lib/tasks/omniauth_openid_federation.rake +4 -3
- data/sig/omniauth_openid_federation.rbs +6 -0
- metadata +100 -1
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require_relative "../logger"
|
|
5
5
|
require_relative "../errors"
|
|
6
6
|
require_relative "../configuration"
|
|
7
|
+
require_relative "../string_helpers"
|
|
7
8
|
|
|
8
9
|
# Entity Statement Validator for OpenID Federation 1.0
|
|
9
10
|
# @see https://openid.net/specs/openid-federation-1_0.html#section-3.2.1 Section 3.2.1: Entity Statement Validation
|
|
@@ -53,70 +54,27 @@ module OmniauthOpenidFederation
|
|
|
53
54
|
# @return [Hash] Validated entity statement with header and claims
|
|
54
55
|
# @raise [ValidationError] If validation fails at any step
|
|
55
56
|
def validate!
|
|
56
|
-
# Step 1: Entity Statement MUST be a signed JWT
|
|
57
57
|
validate_jwt_format
|
|
58
|
-
|
|
59
|
-
# Step 2: Validate typ header (MUST be "entity-statement+jwt")
|
|
60
58
|
validate_typ_header
|
|
61
|
-
|
|
62
|
-
# Step 3: Validate alg header (MUST be present and not "none")
|
|
63
59
|
validate_alg_header
|
|
64
|
-
|
|
65
|
-
# Step 4: Validate sub claim matches Entity Identifier
|
|
66
60
|
validate_sub_claim
|
|
67
|
-
|
|
68
|
-
# Step 5: Validate iss claim is valid Entity Identifier
|
|
69
61
|
validate_iss_claim
|
|
70
|
-
|
|
71
|
-
# Step 6: Determine Entity Configuration vs Subordinate Statement
|
|
72
62
|
determine_statement_type
|
|
73
|
-
|
|
74
|
-
# Step 7: Validate authority_hints for Subordinate Statements
|
|
75
63
|
validate_authority_hints if @is_subordinate_statement
|
|
76
|
-
|
|
77
|
-
# Step 8: Validate iat claim (issued at time)
|
|
78
64
|
validate_iat_claim
|
|
79
|
-
|
|
80
|
-
# Step 9: Validate exp claim (expiration time)
|
|
81
65
|
validate_exp_claim
|
|
82
|
-
|
|
83
|
-
# Step 10: Validate jwks claim (MUST be present and valid)
|
|
84
66
|
validate_jwks_claim
|
|
85
|
-
|
|
86
|
-
# Step 11: Validate kid header (MUST be non-zero length string)
|
|
87
67
|
validate_kid_header
|
|
88
|
-
|
|
89
|
-
# Step 12: Validate kid matches key in issuer's JWKS
|
|
90
68
|
validate_kid_matching
|
|
91
|
-
|
|
92
|
-
# Step 13: Validate signature (if issuer configuration provided)
|
|
93
69
|
validate_signature if @issuer_entity_configuration || @is_entity_configuration
|
|
94
|
-
|
|
95
|
-
# Step 14: Validate crit claim (if present)
|
|
96
70
|
validate_crit_claim
|
|
97
|
-
|
|
98
|
-
# Step 15: Validate authority_hints syntax (if present)
|
|
99
71
|
validate_authority_hints_syntax if @header && @payload && @is_entity_configuration
|
|
100
|
-
|
|
101
|
-
# Step 16: Validate metadata syntax (if present)
|
|
102
72
|
validate_metadata_syntax
|
|
103
|
-
|
|
104
|
-
# Step 17: Validate metadata_policy (if present, MUST be Subordinate Statement)
|
|
105
73
|
validate_metadata_policy_presence
|
|
106
|
-
|
|
107
|
-
# Step 18: Validate metadata_policy_crit (if present, MUST be Subordinate Statement)
|
|
108
74
|
validate_metadata_policy_crit_presence
|
|
109
|
-
|
|
110
|
-
# Step 19: Validate constraints (if present, MUST be Subordinate Statement)
|
|
111
75
|
validate_constraints_presence
|
|
112
|
-
|
|
113
|
-
# Step 20: Validate trust_marks (if present, MUST be Entity Configuration)
|
|
114
76
|
validate_trust_marks_presence
|
|
115
|
-
|
|
116
|
-
# Step 21: Validate trust_mark_issuers (if present, MUST be Entity Configuration)
|
|
117
77
|
validate_trust_mark_issuers_presence
|
|
118
|
-
|
|
119
|
-
# Step 22: Validate trust_mark_owners (if present, MUST be Entity Configuration)
|
|
120
78
|
validate_trust_mark_owners_presence
|
|
121
79
|
|
|
122
80
|
{
|
|
@@ -129,14 +87,12 @@ module OmniauthOpenidFederation
|
|
|
129
87
|
|
|
130
88
|
private
|
|
131
89
|
|
|
132
|
-
# Step 1: Entity Statement MUST be a signed JWT
|
|
133
90
|
def validate_jwt_format
|
|
134
91
|
jwt_parts = @jwt_string.split(".")
|
|
135
92
|
if jwt_parts.length != JWT_PARTS_COUNT
|
|
136
93
|
raise ValidationError, "Invalid JWT format: expected #{JWT_PARTS_COUNT} parts (header.payload.signature), got #{jwt_parts.length}"
|
|
137
94
|
end
|
|
138
95
|
|
|
139
|
-
# Decode header and payload
|
|
140
96
|
begin
|
|
141
97
|
@header = JSON.parse(Base64.urlsafe_decode64(jwt_parts[0]))
|
|
142
98
|
@payload = JSON.parse(Base64.urlsafe_decode64(jwt_parts[1]))
|
|
@@ -147,7 +103,6 @@ module OmniauthOpenidFederation
|
|
|
147
103
|
end
|
|
148
104
|
end
|
|
149
105
|
|
|
150
|
-
# Step 2: Entity Statement MUST have typ header with value "entity-statement+jwt"
|
|
151
106
|
def validate_typ_header
|
|
152
107
|
typ = @header["typ"] || @header[:typ]
|
|
153
108
|
unless typ == REQUIRED_TYP
|
|
@@ -155,10 +110,9 @@ module OmniauthOpenidFederation
|
|
|
155
110
|
end
|
|
156
111
|
end
|
|
157
112
|
|
|
158
|
-
# Step 3: Entity Statement MUST have alg header that is present and not "none"
|
|
159
113
|
def validate_alg_header
|
|
160
114
|
alg = @header["alg"] || @header[:alg]
|
|
161
|
-
if
|
|
115
|
+
if StringHelpers.blank?(alg)
|
|
162
116
|
raise ValidationError, "Entity statement MUST have an alg (algorithm) header parameter"
|
|
163
117
|
end
|
|
164
118
|
if alg == "none"
|
|
@@ -170,33 +124,28 @@ module OmniauthOpenidFederation
|
|
|
170
124
|
end
|
|
171
125
|
end
|
|
172
126
|
|
|
173
|
-
# Step 4: Entity Identifier MUST match sub claim
|
|
174
127
|
# Note: This is a structural check. The actual Entity Identifier validation
|
|
175
128
|
# would require knowing the expected Entity Identifier, which is context-dependent.
|
|
176
129
|
def validate_sub_claim
|
|
177
130
|
sub = @payload["sub"]
|
|
178
|
-
if
|
|
131
|
+
if StringHelpers.blank?(sub)
|
|
179
132
|
raise ValidationError, "Entity statement MUST have a sub (subject) claim with a valid Entity Identifier"
|
|
180
133
|
end
|
|
181
|
-
# Basic Entity Identifier format validation (should be a URI)
|
|
182
134
|
unless sub.is_a?(String) && sub.start_with?("http://", "https://")
|
|
183
135
|
raise ValidationError, "Entity statement sub claim MUST be a valid Entity Identifier (URI)"
|
|
184
136
|
end
|
|
185
137
|
end
|
|
186
138
|
|
|
187
|
-
# Step 5: Entity Statement MUST have iss claim with valid Entity Identifier
|
|
188
139
|
def validate_iss_claim
|
|
189
140
|
iss = @payload["iss"]
|
|
190
|
-
if
|
|
141
|
+
if StringHelpers.blank?(iss)
|
|
191
142
|
raise ValidationError, "Entity statement MUST have an iss (issuer) claim with a valid Entity Identifier"
|
|
192
143
|
end
|
|
193
|
-
# Basic Entity Identifier format validation (should be a URI)
|
|
194
144
|
unless iss.is_a?(String) && iss.start_with?("http://", "https://")
|
|
195
145
|
raise ValidationError, "Entity statement iss claim MUST be a valid Entity Identifier (URI)"
|
|
196
146
|
end
|
|
197
147
|
end
|
|
198
148
|
|
|
199
|
-
# Step 6: Determine Entity Configuration vs Subordinate Statement
|
|
200
149
|
def determine_statement_type
|
|
201
150
|
iss = @payload["iss"]
|
|
202
151
|
sub = @payload["sub"]
|
|
@@ -213,7 +162,6 @@ module OmniauthOpenidFederation
|
|
|
213
162
|
return
|
|
214
163
|
end
|
|
215
164
|
|
|
216
|
-
# Extract authority_hints from issuer's Entity Configuration
|
|
217
165
|
issuer_config = if @issuer_entity_configuration.is_a?(Hash)
|
|
218
166
|
@issuer_entity_configuration
|
|
219
167
|
elsif @issuer_entity_configuration.respond_to?(:parse)
|
|
@@ -231,7 +179,6 @@ module OmniauthOpenidFederation
|
|
|
231
179
|
end
|
|
232
180
|
end
|
|
233
181
|
|
|
234
|
-
# Step 8: Validate iat claim (issued at time)
|
|
235
182
|
def validate_iat_claim
|
|
236
183
|
iat = @payload["iat"]
|
|
237
184
|
if iat.nil?
|
|
@@ -243,13 +190,11 @@ module OmniauthOpenidFederation
|
|
|
243
190
|
end
|
|
244
191
|
|
|
245
192
|
current_time = Time.now.to_i
|
|
246
|
-
# Allow clock skew: iat can be slightly in the future
|
|
247
193
|
if iat > (current_time + @clock_skew_tolerance)
|
|
248
194
|
raise ValidationError, "Entity statement iat (issued at) claim is too far in the future. Current time: #{current_time}, iat: #{iat}, tolerance: #{@clock_skew_tolerance}s"
|
|
249
195
|
end
|
|
250
196
|
end
|
|
251
197
|
|
|
252
|
-
# Step 9: Validate exp claim (expiration time)
|
|
253
198
|
def validate_exp_claim
|
|
254
199
|
exp = @payload["exp"]
|
|
255
200
|
if exp.nil?
|
|
@@ -261,19 +206,16 @@ module OmniauthOpenidFederation
|
|
|
261
206
|
end
|
|
262
207
|
|
|
263
208
|
current_time = Time.now.to_i
|
|
264
|
-
# Allow clock skew: exp can be slightly in the past
|
|
265
209
|
if exp < (current_time - @clock_skew_tolerance)
|
|
266
210
|
raise ValidationError, "Entity statement has expired. Current time: #{current_time}, exp: #{exp}, tolerance: #{@clock_skew_tolerance}s"
|
|
267
211
|
end
|
|
268
212
|
end
|
|
269
213
|
|
|
270
|
-
# Step 10: Validate jwks claim (MUST be present and valid)
|
|
271
214
|
def validate_jwks_claim
|
|
272
215
|
jwks = @payload["jwks"]
|
|
273
216
|
if jwks.nil?
|
|
274
217
|
# jwks is OPTIONAL only for Entity Statement returned from OP during Explicit Registration
|
|
275
218
|
# For all other cases, it is REQUIRED
|
|
276
|
-
# We'll be strict and require it unless we have context that this is an Explicit Registration response
|
|
277
219
|
raise ValidationError, "Entity statement MUST have a jwks (JWK Set) claim"
|
|
278
220
|
end
|
|
279
221
|
|
|
@@ -286,14 +228,12 @@ module OmniauthOpenidFederation
|
|
|
286
228
|
raise ValidationError, "Entity statement jwks claim MUST contain a 'keys' array"
|
|
287
229
|
end
|
|
288
230
|
|
|
289
|
-
# Validate that each key has a unique kid
|
|
290
231
|
kids = keys.map { |key| key["kid"] || key[:kid] }.compact
|
|
291
232
|
if kids.length != kids.uniq.length
|
|
292
233
|
raise ValidationError, "Entity statement jwks keys MUST have unique kid (Key ID) values"
|
|
293
234
|
end
|
|
294
235
|
end
|
|
295
236
|
|
|
296
|
-
# Step 11: Validate kid header (MUST be non-zero length string)
|
|
297
237
|
def validate_kid_header
|
|
298
238
|
kid = @header["kid"] || @header[:kid]
|
|
299
239
|
if kid.nil?
|
|
@@ -307,17 +247,13 @@ module OmniauthOpenidFederation
|
|
|
307
247
|
end
|
|
308
248
|
end
|
|
309
249
|
|
|
310
|
-
# Step 12: Validate kid matches key in issuer's JWKS
|
|
311
250
|
def validate_kid_matching
|
|
312
251
|
kid = @header["kid"] || @header[:kid]
|
|
313
252
|
jwks = @payload["jwks"] || {}
|
|
314
253
|
|
|
315
|
-
# Get issuer's JWKS
|
|
316
254
|
issuer_jwks = if @is_entity_configuration
|
|
317
|
-
# For Entity Configuration, use its own JWKS
|
|
318
255
|
jwks
|
|
319
256
|
elsif @issuer_entity_configuration
|
|
320
|
-
# For Subordinate Statement, use issuer's Entity Configuration JWKS
|
|
321
257
|
issuer_config = if @issuer_entity_configuration.is_a?(Hash)
|
|
322
258
|
@issuer_entity_configuration
|
|
323
259
|
elsif @issuer_entity_configuration.respond_to?(:parse)
|
|
@@ -328,7 +264,6 @@ module OmniauthOpenidFederation
|
|
|
328
264
|
|
|
329
265
|
issuer_config[:jwks] || issuer_config["jwks"] || issuer_config[:claims]&.fetch("jwks", {}) || issuer_config["claims"]&.fetch("jwks", {})
|
|
330
266
|
else
|
|
331
|
-
# Cannot validate kid matching without issuer configuration
|
|
332
267
|
OmniauthOpenidFederation::Logger.warn("[EntityStatementValidator] Cannot validate kid matching: issuer entity configuration not provided")
|
|
333
268
|
return
|
|
334
269
|
end
|
|
@@ -341,12 +276,9 @@ module OmniauthOpenidFederation
|
|
|
341
276
|
end
|
|
342
277
|
end
|
|
343
278
|
|
|
344
|
-
#
|
|
279
|
+
# Signature validation is typically done separately using JWT.decode
|
|
280
|
+
# This step validates that we have the necessary information to validate the signature
|
|
345
281
|
def validate_signature
|
|
346
|
-
# Signature validation is typically done separately using JWT.decode
|
|
347
|
-
# This step is a placeholder - actual signature validation should be done
|
|
348
|
-
# by the caller using the issuer's public key
|
|
349
|
-
# We validate that we have the necessary information to validate the signature
|
|
350
282
|
kid = @header["kid"] || @header[:kid]
|
|
351
283
|
jwks = @payload["jwks"] || {}
|
|
352
284
|
|
|
@@ -372,12 +304,8 @@ module OmniauthOpenidFederation
|
|
|
372
304
|
unless matching_key
|
|
373
305
|
raise ValidationError, "Cannot validate signature: signing key with kid '#{kid}' not found in issuer's JWKS"
|
|
374
306
|
end
|
|
375
|
-
|
|
376
|
-
# Note: Actual cryptographic signature verification should be done by the caller
|
|
377
|
-
# using JWT.decode with the matching key
|
|
378
307
|
end
|
|
379
308
|
|
|
380
|
-
# Step 14: Validate crit claim (if present)
|
|
381
309
|
def validate_crit_claim
|
|
382
310
|
crit = @payload["crit"] || @payload[:crit]
|
|
383
311
|
return unless crit
|
|
@@ -398,7 +326,6 @@ module OmniauthOpenidFederation
|
|
|
398
326
|
end
|
|
399
327
|
end
|
|
400
328
|
|
|
401
|
-
# Step 15: Validate authority_hints syntax (if present)
|
|
402
329
|
def validate_authority_hints_syntax
|
|
403
330
|
authority_hints = @payload["authority_hints"] || @payload[:authority_hints]
|
|
404
331
|
return unless authority_hints
|
|
@@ -418,7 +345,6 @@ module OmniauthOpenidFederation
|
|
|
418
345
|
end
|
|
419
346
|
end
|
|
420
347
|
|
|
421
|
-
# Step 16: Validate metadata syntax (if present)
|
|
422
348
|
def validate_metadata_syntax
|
|
423
349
|
metadata = @payload["metadata"] || @payload[:metadata]
|
|
424
350
|
return unless metadata
|
|
@@ -427,7 +353,6 @@ module OmniauthOpenidFederation
|
|
|
427
353
|
raise ValidationError, "Entity statement metadata claim MUST be a JSON object"
|
|
428
354
|
end
|
|
429
355
|
|
|
430
|
-
# Validate that metadata values are not null
|
|
431
356
|
metadata.each do |entity_type, entity_metadata|
|
|
432
357
|
if entity_metadata.nil?
|
|
433
358
|
raise ValidationError, "Entity statement metadata claim MUST NOT use null as metadata values"
|
|
@@ -438,7 +363,6 @@ module OmniauthOpenidFederation
|
|
|
438
363
|
end
|
|
439
364
|
end
|
|
440
365
|
|
|
441
|
-
# Step 17: Validate metadata_policy presence (MUST be Subordinate Statement)
|
|
442
366
|
def validate_metadata_policy_presence
|
|
443
367
|
metadata_policy = @payload["metadata_policy"] || @payload[:metadata_policy]
|
|
444
368
|
return unless metadata_policy
|
|
@@ -448,7 +372,6 @@ module OmniauthOpenidFederation
|
|
|
448
372
|
end
|
|
449
373
|
end
|
|
450
374
|
|
|
451
|
-
# Step 18: Validate metadata_policy_crit presence (MUST be Subordinate Statement)
|
|
452
375
|
def validate_metadata_policy_crit_presence
|
|
453
376
|
metadata_policy_crit = @payload["metadata_policy_crit"] || @payload[:metadata_policy_crit]
|
|
454
377
|
return unless metadata_policy_crit
|
|
@@ -458,7 +381,6 @@ module OmniauthOpenidFederation
|
|
|
458
381
|
end
|
|
459
382
|
end
|
|
460
383
|
|
|
461
|
-
# Step 19: Validate constraints presence (MUST be Subordinate Statement)
|
|
462
384
|
def validate_constraints_presence
|
|
463
385
|
constraints = @payload["constraints"] || @payload[:constraints]
|
|
464
386
|
return unless constraints
|
|
@@ -468,7 +390,6 @@ module OmniauthOpenidFederation
|
|
|
468
390
|
end
|
|
469
391
|
end
|
|
470
392
|
|
|
471
|
-
# Step 20: Validate trust_marks presence (MUST be Entity Configuration)
|
|
472
393
|
def validate_trust_marks_presence
|
|
473
394
|
trust_marks = @payload["trust_marks"] || @payload[:trust_marks]
|
|
474
395
|
return unless trust_marks
|
|
@@ -478,7 +399,6 @@ module OmniauthOpenidFederation
|
|
|
478
399
|
end
|
|
479
400
|
end
|
|
480
401
|
|
|
481
|
-
# Step 21: Validate trust_mark_issuers presence (MUST be Entity Configuration)
|
|
482
402
|
def validate_trust_mark_issuers_presence
|
|
483
403
|
trust_mark_issuers = @payload["trust_mark_issuers"] || @payload[:trust_mark_issuers]
|
|
484
404
|
return unless trust_mark_issuers
|
|
@@ -488,7 +408,6 @@ module OmniauthOpenidFederation
|
|
|
488
408
|
end
|
|
489
409
|
end
|
|
490
410
|
|
|
491
|
-
# Step 22: Validate trust_mark_owners presence (MUST be Entity Configuration)
|
|
492
411
|
def validate_trust_mark_owners_presence
|
|
493
412
|
trust_mark_owners = @payload["trust_mark_owners"] || @payload[:trust_mark_owners]
|
|
494
413
|
return unless trust_mark_owners
|
|
@@ -4,7 +4,7 @@ require_relative "../http_client"
|
|
|
4
4
|
require_relative "../logger"
|
|
5
5
|
require_relative "../errors"
|
|
6
6
|
require_relative "../utils"
|
|
7
|
-
|
|
7
|
+
require_relative "../string_helpers"
|
|
8
8
|
require "cgi"
|
|
9
9
|
|
|
10
10
|
# Trust Chain Resolver for OpenID Federation 1.0
|
|
@@ -59,24 +59,21 @@ module OmniauthOpenidFederation
|
|
|
59
59
|
def resolve!
|
|
60
60
|
OmniauthOpenidFederation::Logger.debug("[TrustChainResolver] Starting trust chain resolution for: #{@leaf_entity_id}")
|
|
61
61
|
|
|
62
|
-
# Step 1: Fetch Leaf Entity's Entity Configuration
|
|
63
62
|
leaf_config = fetch_entity_configuration(@leaf_entity_id)
|
|
64
|
-
validate_entity_statement(leaf_config, nil)
|
|
63
|
+
validate_entity_statement(leaf_config, nil)
|
|
65
64
|
@resolved_statements << leaf_config
|
|
66
65
|
@visited_entities.add(@leaf_entity_id)
|
|
67
66
|
|
|
68
|
-
# Step 2: Follow authority_hints to build the chain
|
|
69
67
|
current_entity_id = @leaf_entity_id
|
|
70
68
|
current_config = leaf_config
|
|
71
69
|
|
|
72
70
|
while current_config && !is_trust_anchor?(current_config)
|
|
73
71
|
authority_hints = extract_authority_hints(current_config)
|
|
74
72
|
|
|
75
|
-
if
|
|
73
|
+
if StringHelpers.blank?(authority_hints)
|
|
76
74
|
raise ValidationError, "Entity #{current_entity_id} has no authority_hints and is not a Trust Anchor"
|
|
77
75
|
end
|
|
78
76
|
|
|
79
|
-
# Try each authority hint until we find a valid chain
|
|
80
77
|
found_next = false
|
|
81
78
|
authority_hints.each do |authority_id|
|
|
82
79
|
next if @visited_entities.include?(authority_id)
|
|
@@ -86,28 +83,23 @@ module OmniauthOpenidFederation
|
|
|
86
83
|
end
|
|
87
84
|
|
|
88
85
|
begin
|
|
89
|
-
# Fetch Subordinate Statement from authority
|
|
90
86
|
subordinate_statement = fetch_subordinate_statement(
|
|
91
87
|
issuer: authority_id,
|
|
92
88
|
subject: current_entity_id
|
|
93
89
|
)
|
|
94
90
|
|
|
95
|
-
# Validate Subordinate Statement
|
|
96
91
|
issuer_config = fetch_entity_configuration(authority_id)
|
|
97
92
|
validate_entity_statement(subordinate_statement, issuer_config)
|
|
98
93
|
|
|
99
|
-
# Add to chain
|
|
100
94
|
@resolved_statements << subordinate_statement
|
|
101
95
|
@visited_entities.add(authority_id)
|
|
102
96
|
|
|
103
|
-
# Continue with issuer as next entity
|
|
104
97
|
current_entity_id = authority_id
|
|
105
98
|
current_config = issuer_config
|
|
106
99
|
found_next = true
|
|
107
100
|
break
|
|
108
101
|
rescue ValidationError, FetchError => e
|
|
109
102
|
OmniauthOpenidFederation::Logger.warn("[TrustChainResolver] Failed to resolve via #{authority_id}: #{e.message}")
|
|
110
|
-
# Instrument trust chain validation failure
|
|
111
103
|
OmniauthOpenidFederation::Instrumentation.notify_trust_chain_validation_failed(
|
|
112
104
|
entity_id: current_entity_id,
|
|
113
105
|
trust_anchor: authority_id,
|
|
@@ -124,10 +116,8 @@ module OmniauthOpenidFederation
|
|
|
124
116
|
end
|
|
125
117
|
end
|
|
126
118
|
|
|
127
|
-
# Step 3: Verify we reached a Trust Anchor
|
|
128
119
|
unless is_trust_anchor?(current_config)
|
|
129
120
|
error_msg = "Trust chain did not terminate at a configured Trust Anchor"
|
|
130
|
-
# Instrument trust chain validation failure
|
|
131
121
|
OmniauthOpenidFederation::Instrumentation.notify_trust_chain_validation_failed(
|
|
132
122
|
entity_id: @leaf_entity_id,
|
|
133
123
|
trust_anchor: current_entity_id,
|
|
@@ -164,7 +154,6 @@ module OmniauthOpenidFederation
|
|
|
164
154
|
end
|
|
165
155
|
|
|
166
156
|
def fetch_subordinate_statement(issuer:, subject:)
|
|
167
|
-
# Try to get fetch endpoint from issuer's Entity Configuration
|
|
168
157
|
issuer_config = fetch_entity_configuration(issuer)
|
|
169
158
|
fetch_endpoint = extract_fetch_endpoint(issuer_config)
|
|
170
159
|
|
|
@@ -172,7 +161,6 @@ module OmniauthOpenidFederation
|
|
|
172
161
|
raise FetchError, "Issuer #{issuer} does not provide a fetch endpoint"
|
|
173
162
|
end
|
|
174
163
|
|
|
175
|
-
# Build fetch URL with iss and sub parameters
|
|
176
164
|
fetch_url = "#{fetch_endpoint}?iss=#{CGI.escape(issuer)}&sub=#{CGI.escape(subject)}"
|
|
177
165
|
OmniauthOpenidFederation::Logger.debug("[TrustChainResolver] Fetching Subordinate Statement from: #{fetch_url}")
|
|
178
166
|
|