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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -1
- data/README.md +120 -14
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +3 -3
- data/config/polyrun_coverage.yml +15 -0
- data/config/polyrun_spec_quality.yml +29 -0
- data/config/routes.rb +20 -10
- data/examples/README_INTEGRATION_TESTING.md +3 -0
- data/examples/integration_test_flow.rb +10 -8
- data/examples/jobs/federation_cache_refresh_job.rb.example +7 -7
- data/examples/jobs/federation_files_generation_job.rb.example +3 -2
- data/examples/mock_op_server.rb +4 -5
- data/examples/mock_rp_server.rb +3 -3
- data/examples/standalone_multiple_endpoints_example.rb +494 -0
- data/lib/omniauth_openid_federation/access_token.rb +91 -450
- data/lib/omniauth_openid_federation/cache.rb +16 -0
- data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
- data/lib/omniauth_openid_federation/constants.rb +3 -0
- data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
- data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
- 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 +8 -91
- data/lib/omniauth_openid_federation/federation/signed_jwks.rb +0 -1
- data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +54 -21
- data/lib/omniauth_openid_federation/federation_endpoint.rb +40 -172
- data/lib/omniauth_openid_federation/http_client.rb +145 -32
- data/lib/omniauth_openid_federation/http_errors.rb +14 -0
- data/lib/omniauth_openid_federation/id_token.rb +19 -0
- data/lib/omniauth_openid_federation/jwe.rb +26 -0
- data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
- data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
- data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
- data/lib/omniauth_openid_federation/jws.rb +3 -11
- data/lib/omniauth_openid_federation/jwt_response_decoder.rb +290 -0
- data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
- data/lib/omniauth_openid_federation/rack.rb +2 -0
- data/lib/omniauth_openid_federation/rack_endpoint.rb +21 -9
- data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
- data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
- data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
- data/lib/omniauth_openid_federation/strategy/callback_handling.rb +135 -0
- data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
- data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +211 -0
- data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +433 -0
- data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
- data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
- data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +268 -0
- data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +134 -0
- data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +61 -0
- data/lib/omniauth_openid_federation/strategy.rb +27 -1910
- data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
- data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
- data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
- data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
- data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
- data/lib/omniauth_openid_federation/tasks_helper.rb +28 -790
- data/lib/omniauth_openid_federation/time_helpers.rb +67 -0
- data/lib/omniauth_openid_federation/user_info.rb +15 -0
- data/lib/omniauth_openid_federation/utils.rb +14 -9
- data/lib/omniauth_openid_federation/validators.rb +55 -44
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/omniauth_openid_federation.rb +10 -3
- data/lib/tasks/omniauth_openid_federation.rake +5 -5
- data/sig/omniauth_openid_federation.rbs +8 -1
- data/sig/strategy.rbs +6 -6
- 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
|
-
|
|
8
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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)
|