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
@@ -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