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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
require_relative "../http_client"
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "../federation/entity_statement"
|
|
5
|
+
require_relative "../jwks/fetch"
|
|
6
|
+
require_relative "../federation/signed_jwks"
|
|
7
|
+
|
|
8
|
+
module OmniauthOpenidFederation
|
|
9
|
+
module Tasks
|
|
10
|
+
module LocalEndpointTester
|
|
11
|
+
def self.run(base_url:)
|
|
12
|
+
entity_statement_url = "#{base_url}/.well-known/openid-federation"
|
|
13
|
+
validation_warnings = []
|
|
14
|
+
|
|
15
|
+
# Fetch and parse entity statement
|
|
16
|
+
begin
|
|
17
|
+
entity_statement = Federation::EntityStatement.fetch!(
|
|
18
|
+
entity_statement_url,
|
|
19
|
+
fingerprint: nil # Don't validate fingerprint for local testing
|
|
20
|
+
)
|
|
21
|
+
rescue ValidationError => e
|
|
22
|
+
# Don't block execution - treat validation errors as warnings
|
|
23
|
+
validation_warnings << e.message
|
|
24
|
+
# Try to parse anyway for diagnostic purposes
|
|
25
|
+
begin
|
|
26
|
+
require "json"
|
|
27
|
+
require "base64"
|
|
28
|
+
response = HttpClient.get(entity_statement_url)
|
|
29
|
+
entity_statement = Federation::EntityStatement.new(response.body.to_s)
|
|
30
|
+
rescue
|
|
31
|
+
raise FetchError, "Failed to fetch entity statement: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
metadata = entity_statement.parse
|
|
37
|
+
rescue ValidationError => e
|
|
38
|
+
validation_warnings << e.message
|
|
39
|
+
# Try to extract basic info even if validation fails
|
|
40
|
+
begin
|
|
41
|
+
require "json"
|
|
42
|
+
require "base64"
|
|
43
|
+
jwt_parts = entity_statement.entity_statement.split(".")
|
|
44
|
+
payload = JSON.parse(Base64.urlsafe_decode64(jwt_parts[1]))
|
|
45
|
+
# Preserve original structure (string keys from JSON)
|
|
46
|
+
metadata = {
|
|
47
|
+
issuer: payload["iss"],
|
|
48
|
+
sub: payload["sub"],
|
|
49
|
+
exp: payload["exp"],
|
|
50
|
+
iat: payload["iat"],
|
|
51
|
+
jwks: payload["jwks"],
|
|
52
|
+
metadata: payload["metadata"] || {}
|
|
53
|
+
}
|
|
54
|
+
rescue
|
|
55
|
+
raise FetchError, "Failed to parse entity statement: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Extract endpoints - handle both provider and relying party entity types
|
|
60
|
+
metadata_section = metadata[:metadata] || {}
|
|
61
|
+
provider_metadata = metadata_section[:openid_provider] || metadata_section["openid_provider"] || {}
|
|
62
|
+
rp_metadata = metadata_section[:openid_relying_party] || metadata_section["openid_relying_party"] || {}
|
|
63
|
+
|
|
64
|
+
endpoints = {}
|
|
65
|
+
|
|
66
|
+
# Provider endpoints
|
|
67
|
+
if provider_metadata.any?
|
|
68
|
+
endpoints["Authorization Endpoint"] = provider_metadata[:authorization_endpoint] || provider_metadata["authorization_endpoint"]
|
|
69
|
+
endpoints["Token Endpoint"] = provider_metadata[:token_endpoint] || provider_metadata["token_endpoint"]
|
|
70
|
+
endpoints["UserInfo Endpoint"] = provider_metadata[:userinfo_endpoint] || provider_metadata["userinfo_endpoint"]
|
|
71
|
+
endpoints["JWKS URI"] = provider_metadata[:jwks_uri] || provider_metadata["jwks_uri"]
|
|
72
|
+
endpoints["Signed JWKS URI"] = provider_metadata[:signed_jwks_uri] || provider_metadata["signed_jwks_uri"]
|
|
73
|
+
endpoints["End Session Endpoint"] = provider_metadata[:end_session_endpoint] || provider_metadata["end_session_endpoint"]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Relying Party endpoints (JWKS only)
|
|
77
|
+
if rp_metadata.any?
|
|
78
|
+
endpoints["JWKS URI"] ||= rp_metadata[:jwks_uri] || rp_metadata["jwks_uri"]
|
|
79
|
+
endpoints["Signed JWKS URI"] ||= rp_metadata[:signed_jwks_uri] || rp_metadata["signed_jwks_uri"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
endpoints.compact!
|
|
83
|
+
|
|
84
|
+
# Detect key configuration status
|
|
85
|
+
key_status = detect_key_status(metadata[:jwks])
|
|
86
|
+
|
|
87
|
+
# Test endpoints
|
|
88
|
+
results = {}
|
|
89
|
+
entity_jwks = metadata[:jwks]
|
|
90
|
+
|
|
91
|
+
endpoints.each do |name, url|
|
|
92
|
+
next unless url
|
|
93
|
+
|
|
94
|
+
begin
|
|
95
|
+
case name
|
|
96
|
+
when "JWKS URI"
|
|
97
|
+
jwks = Jwks::Fetch.run(url)
|
|
98
|
+
key_count = jwks["keys"]&.length || 0
|
|
99
|
+
results[name] = {status: :success, keys: key_count}
|
|
100
|
+
|
|
101
|
+
when "Signed JWKS URI"
|
|
102
|
+
signed_jwks = Federation::SignedJWKS.fetch!(
|
|
103
|
+
url,
|
|
104
|
+
entity_jwks,
|
|
105
|
+
force_refresh: true
|
|
106
|
+
)
|
|
107
|
+
key_count = signed_jwks["keys"]&.length || 0
|
|
108
|
+
results[name] = {status: :success, keys: key_count}
|
|
109
|
+
|
|
110
|
+
else
|
|
111
|
+
begin
|
|
112
|
+
URI.parse(url)
|
|
113
|
+
rescue URI::InvalidURIError => error
|
|
114
|
+
results[name] = {status: :error, message: "Invalid URL: #{error.message}"}
|
|
115
|
+
next
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
response = HttpClient.get(url, max_retries: 0)
|
|
119
|
+
|
|
120
|
+
results[name] = if response.status.code < 400
|
|
121
|
+
{status: :success, code: response.status.code.to_s}
|
|
122
|
+
else
|
|
123
|
+
{status: :warning, code: response.status.code.to_s, body: response.body.to_s}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
rescue FetchError, Federation::SignedJWKS::FetchError => e
|
|
127
|
+
results[name] = {status: :error, message: e.message}
|
|
128
|
+
rescue Federation::SignedJWKS::ValidationError => e
|
|
129
|
+
results[name] = {status: :error, message: e.message}
|
|
130
|
+
rescue => e
|
|
131
|
+
results[name] = {status: :error, message: e.message}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
success: true,
|
|
137
|
+
entity_statement: entity_statement,
|
|
138
|
+
metadata: metadata,
|
|
139
|
+
results: results,
|
|
140
|
+
key_status: key_status,
|
|
141
|
+
validation_warnings: validation_warnings
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.detect_key_status(jwks)
|
|
146
|
+
return {type: :unknown, count: 0, recommendation: "No keys found in entity statement"} unless jwks
|
|
147
|
+
|
|
148
|
+
keys = jwks.is_a?(Hash) ? (jwks["keys"] || jwks[:keys] || []) : []
|
|
149
|
+
return {type: :unknown, count: 0, recommendation: "No keys found in entity statement"} if keys.empty?
|
|
150
|
+
|
|
151
|
+
# Check for duplicate kids (indicates single key used for both signing and encryption)
|
|
152
|
+
kids = keys.map { |k| k["kid"] || k[:kid] }.compact
|
|
153
|
+
duplicate_kids = kids.length != kids.uniq.length
|
|
154
|
+
|
|
155
|
+
# Check use fields
|
|
156
|
+
uses = keys.map { |k| k["use"] || k[:use] }.compact.uniq
|
|
157
|
+
has_sig = uses.include?("sig")
|
|
158
|
+
has_enc = uses.include?("enc")
|
|
159
|
+
has_both_uses = has_sig && has_enc
|
|
160
|
+
|
|
161
|
+
if duplicate_kids
|
|
162
|
+
{
|
|
163
|
+
type: :single,
|
|
164
|
+
count: keys.length,
|
|
165
|
+
recommendation: "⚠️ Single key detected (duplicate Key IDs). This is NOT RECOMMENDED for production. Use separate signing and encryption keys for better security. Generate with: rake openid_federation:prepare_client_keys[separate]"
|
|
166
|
+
}
|
|
167
|
+
elsif has_both_uses && keys.length >= 2
|
|
168
|
+
{
|
|
169
|
+
type: :separate,
|
|
170
|
+
count: keys.length,
|
|
171
|
+
recommendation: "✅ Separate keys detected (recommended for production)"
|
|
172
|
+
}
|
|
173
|
+
elsif keys.length == 1
|
|
174
|
+
{
|
|
175
|
+
type: :single,
|
|
176
|
+
count: 1,
|
|
177
|
+
recommendation: "⚠️ Single key detected. This is NOT RECOMMENDED for production. Use separate signing and encryption keys for better security. Generate with: rake openid_federation:prepare_client_keys[separate]"
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
{
|
|
181
|
+
type: :unknown,
|
|
182
|
+
count: keys.length,
|
|
183
|
+
recommendation: "Key configuration unclear. Ensure keys have unique Key IDs and proper 'use' fields ('sig' for signing, 'enc' for encryption)"
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative "../configuration"
|
|
2
|
+
|
|
3
|
+
module OmniauthOpenidFederation
|
|
4
|
+
module Tasks
|
|
5
|
+
module PathResolver
|
|
6
|
+
def self.resolve(file_path)
|
|
7
|
+
return file_path if file_path.start_with?("/")
|
|
8
|
+
|
|
9
|
+
config = Configuration.config
|
|
10
|
+
if defined?(Rails) && Rails.root
|
|
11
|
+
Rails.root.join(file_path).to_s
|
|
12
|
+
elsif config.root_path
|
|
13
|
+
File.join(config.root_path, file_path)
|
|
14
|
+
else
|
|
15
|
+
File.expand_path(file_path)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|