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
@@ -5,6 +5,7 @@ require_relative "key_extractor"
5
5
  require_relative "utils"
6
6
  require_relative "configuration"
7
7
  require_relative "logger"
8
+ require_relative "string_helpers"
8
9
 
9
10
  # Entity Statement Reader for OpenID Federation 1.0
10
11
  # @see https://openid.net/specs/openid-federation-1_0.html OpenID Federation 1.0 Specification
@@ -30,7 +31,7 @@ module OmniauthOpenidFederation
30
31
  # @return [Array<Hash>] Array of JWK hash objects
31
32
  def fetch_keys(entity_statement_path: nil)
32
33
  entity_statement = load_entity_statement(entity_statement_path)
33
- return [] if entity_statement.nil? || entity_statement.empty?
34
+ return [] if StringHelpers.blank?(entity_statement)
34
35
 
35
36
  # Decode self-signed entity statement
36
37
  # Entity statements are self-signed, so we validate using their own JWKS
@@ -53,7 +54,7 @@ module OmniauthOpenidFederation
53
54
  # @return [Hash, nil] Hash with provider metadata or nil if not found
54
55
  def parse_metadata(entity_statement_path: nil)
55
56
  entity_statement = load_entity_statement(entity_statement_path)
56
- return nil if entity_statement.nil? || entity_statement.empty?
57
+ return nil if StringHelpers.blank?(entity_statement)
57
58
 
58
59
  # Decode JWT payload
59
60
  jwt_parts = entity_statement.split(".")
@@ -93,21 +94,45 @@ module OmniauthOpenidFederation
93
94
  def load_entity_statement(entity_statement_path)
94
95
  return nil if entity_statement_path.nil? || entity_statement_path.to_s.empty?
95
96
 
96
- # Determine allowed directories for file path validation
97
- config = OmniauthOpenidFederation::Configuration.config
98
- allowed_dirs = if defined?(Rails) && Rails.root
99
- [Rails.root.join("config").to_s]
100
- elsif config.root_path
101
- [File.join(config.root_path, "config")]
97
+ # If path is absolute and exists, allow it (for temp files in tests)
98
+ # For absolute paths that don't exist, validate path traversal but allow outside allowed_dirs
99
+ # For relative paths, validate against allowed directories
100
+ path_str = entity_statement_path.to_s
101
+ is_absolute = path_str.start_with?("/", "~")
102
+
103
+ if is_absolute && File.exist?(entity_statement_path)
104
+ validated_path = entity_statement_path
105
+ elsif is_absolute
106
+ # Absolute path - validate path traversal but allow outside allowed_dirs
107
+ begin
108
+ validated_path = Utils.validate_file_path!(
109
+ entity_statement_path,
110
+ allowed_dirs: nil # Allow absolute paths outside config directory
111
+ )
112
+ rescue SecurityError
113
+ return nil
114
+ end
115
+ else
116
+ # Relative path - must be in allowed directories
117
+ config = OmniauthOpenidFederation::Configuration.config
118
+ allowed_dirs = if defined?(Rails) && Rails.root
119
+ [Rails.root.join("config").to_s]
120
+ elsif config.root_path
121
+ [File.join(config.root_path, "config")]
122
+ end
123
+
124
+ begin
125
+ # Validate file path to prevent path traversal attacks
126
+ validated_path = Utils.validate_file_path!(
127
+ entity_statement_path,
128
+ allowed_dirs: allowed_dirs
129
+ )
130
+ rescue SecurityError
131
+ return nil
132
+ end
102
133
  end
103
134
 
104
135
  begin
105
- # Validate file path to prevent path traversal attacks
106
- validated_path = Utils.validate_file_path!(
107
- entity_statement_path,
108
- allowed_dirs: allowed_dirs
109
- )
110
-
111
136
  return nil unless File.exist?(validated_path)
112
137
 
113
138
  File.read(validated_path)
@@ -1,4 +1,3 @@
1
- require "net/http"
2
1
  require "digest"
3
2
  require "jwt"
4
3
  require "base64"
@@ -94,9 +93,6 @@ module OmniauthOpenidFederation
94
93
  # @raise [FetchError] If fetching fails
95
94
  # @raise [ValidationError] If validation fails
96
95
  def self.fetch!(url, fingerprint: nil, previous_statement: nil, timeout: 10)
97
- # Use HttpClient for retry logic and configurable SSL verification
98
- # Note: HttpClient uses HTTP gem, but entity statements might need Net::HTTP
99
- # For now, we'll use a simple HTTP.get approach with HttpClient's retry logic
100
96
  begin
101
97
  # Convert URL to URI for HttpClient
102
98
  response = HttpClient.get(url, timeout: timeout)
@@ -4,6 +4,7 @@ require "openssl"
4
4
  require "time"
5
5
  require_relative "../logger"
6
6
  require_relative "../errors"
7
+ require_relative "../string_helpers"
7
8
 
8
9
  # Entity Statement Builder for OpenID Federation 1.0
9
10
  # @see https://openid.net/specs/openid-federation-1_0.html OpenID Federation 1.0 Specification
@@ -82,7 +83,6 @@ module OmniauthOpenidFederation
82
83
 
83
84
  payload = build_payload
84
85
 
85
- # Build JWT header
86
86
  # Per OpenID Federation 1.0 Section 3.1: typ MUST be "entity-statement+jwt"
87
87
  header = {
88
88
  alg: "RS256",
@@ -102,13 +102,13 @@ module OmniauthOpenidFederation
102
102
  private
103
103
 
104
104
  def validate_parameters
105
- raise ConfigurationError, "Issuer is required" if @issuer.nil? || @issuer.empty?
106
- raise ConfigurationError, "Subject is required" if @subject.nil? || @subject.empty?
105
+ raise ConfigurationError, "Issuer is required" if StringHelpers.blank?(@issuer)
106
+ raise ConfigurationError, "Subject is required" if StringHelpers.blank?(@subject)
107
107
  raise ConfigurationError, "Private key is required" if @private_key.nil?
108
- raise ConfigurationError, "JWKS is required" if @jwks.nil? || @jwks.empty?
109
- raise ConfigurationError, "Metadata is required" if @metadata.nil? || @metadata.empty?
110
- raise ConfigurationError, "JWKS must contain at least one key" if @jwks["keys"].nil? || @jwks["keys"].empty?
111
- raise ConfigurationError, "Key ID (kid) is required" if @kid.nil? || @kid.empty?
108
+ raise ConfigurationError, "JWKS is required" if StringHelpers.blank?(@jwks)
109
+ raise ConfigurationError, "Metadata is required" if StringHelpers.blank?(@metadata)
110
+ raise ConfigurationError, "JWKS must contain at least one key" if StringHelpers.blank?(@jwks["keys"])
111
+ raise ConfigurationError, "Key ID (kid) is required" if StringHelpers.blank?(@kid)
112
112
  end
113
113
 
114
114
  def build_payload
@@ -125,7 +125,6 @@ module OmniauthOpenidFederation
125
125
  metadata: @metadata
126
126
  }
127
127
 
128
- # Entity Configuration specific claims
129
128
  if is_entity_configuration
130
129
  payload[:authority_hints] = @authority_hints if @authority_hints
131
130
  payload[:trust_marks] = @trust_marks if @trust_marks
@@ -133,7 +132,6 @@ module OmniauthOpenidFederation
133
132
  payload[:trust_mark_owners] = @trust_mark_owners if @trust_mark_owners
134
133
  end
135
134
 
136
- # Subordinate Statement specific claims
137
135
  if is_subordinate_statement
138
136
  payload[:metadata_policy] = @metadata_policy if @metadata_policy
139
137
  payload[:metadata_policy_crit] = @metadata_policy_crit if @metadata_policy_crit
@@ -141,21 +139,17 @@ module OmniauthOpenidFederation
141
139
  payload[:source_endpoint] = @source_endpoint if @source_endpoint
142
140
  end
143
141
 
144
- # Common optional claims
145
142
  payload[:crit] = @crit if @crit
146
143
 
147
144
  payload
148
145
  end
149
146
 
150
147
  def normalize_jwks(jwks)
151
- # Ensure JWKS is a hash with "keys" array
152
148
  if jwks.is_a?(Hash)
153
- # If it has :keys or "keys", use as-is
154
149
  if jwks.key?(:keys) || jwks.key?("keys")
155
150
  keys = jwks[:keys] || jwks["keys"]
156
151
  {"keys" => normalize_keys(keys)}
157
152
  else
158
- # If it's just a hash, wrap it
159
153
  {"keys" => [jwks]}
160
154
  end
161
155
  elsif jwks.is_a?(Array)
@@ -168,7 +162,6 @@ module OmniauthOpenidFederation
168
162
  def normalize_keys(keys)
169
163
  keys.map do |key|
170
164
  if key.is_a?(Hash)
171
- # Convert symbol keys to string keys
172
165
  key.transform_keys(&:to_s)
173
166
  else
174
167
  key
@@ -17,18 +17,47 @@ module OmniauthOpenidFederation
17
17
  # @raise [ValidationError] If parsing fails
18
18
  def self.parse_for_signed_jwks(entity_statement_path)
19
19
  # Determine allowed directories for file path validation
20
- config = Configuration.config
21
- allowed_dirs = if defined?(Rails) && Rails.root
22
- [Rails.root.join("config").to_s]
23
- elsif config.root_path
24
- [File.join(config.root_path, "config")]
25
- end
20
+ # If path is absolute and exists, allow it (for temp files in tests)
21
+ # For absolute paths that don't exist, validate path traversal but allow outside allowed_dirs
22
+ # For relative paths, validate against allowed directories
23
+ path_str = entity_statement_path.to_s
24
+ is_absolute = path_str.start_with?("/", "~")
25
+
26
+ if is_absolute && File.exist?(entity_statement_path)
27
+ validated_path = entity_statement_path
28
+ elsif is_absolute
29
+ # Absolute path - validate path traversal but allow outside allowed_dirs
30
+ begin
31
+ validated_path = Utils.validate_file_path!(
32
+ entity_statement_path,
33
+ allowed_dirs: nil # Allow absolute paths outside config directory
34
+ )
35
+ rescue SecurityError => e
36
+ OmniauthOpenidFederation::Logger.warn("[EntityStatementHelper] Security error: #{e.message}")
37
+ return nil
38
+ end
39
+ else
40
+ # Relative path - must be in allowed directories
41
+ config = Configuration.config
42
+ allowed_dirs = if defined?(Rails) && Rails.root
43
+ [Rails.root.join("config").to_s]
44
+ elsif config.root_path
45
+ [File.join(config.root_path, "config")]
46
+ end
26
47
 
27
- # Validate file path to prevent path traversal
28
- validated_path = Utils.validate_file_path!(
29
- entity_statement_path,
30
- allowed_dirs: allowed_dirs
31
- )
48
+ begin
49
+ # Validate file path to prevent path traversal
50
+ validated_path = Utils.validate_file_path!(
51
+ entity_statement_path,
52
+ allowed_dirs: allowed_dirs
53
+ )
54
+ rescue SecurityError => e
55
+ # For relative paths with path traversal, raise SecurityError instead of returning nil
56
+ # This is a security violation that should be explicitly handled
57
+ OmniauthOpenidFederation::Logger.warn("[EntityStatementHelper] Security error: #{e.message}")
58
+ raise SecurityError, e.message
59
+ end
60
+ end
32
61
 
33
62
  unless File.exist?(validated_path)
34
63
  sanitized_path = Utils.sanitize_path(validated_path)
@@ -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 alg.nil? || alg.empty?
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 sub.nil? || sub.empty?
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 iss.nil? || iss.empty?
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
- # Step 13: Validate signature
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
@@ -391,14 +319,11 @@ module OmniauthOpenidFederation
391
319
  unknown_claims = crit - standard_claims
392
320
 
393
321
  if unknown_claims.any?
394
- # For now, we'll log a warning but not reject
395
- # In a strict implementation, we should reject if we don't understand the claims
396
- # Future enhancement: Add strict_mode option to reject unknown crit claims
397
- OmniauthOpenidFederation::Logger.warn("[EntityStatementValidator] Entity statement contains crit claim with unknown claims: #{unknown_claims.join(", ")}. These claims MUST be understood and processed.")
322
+ raise ValidationError,
323
+ "Entity statement crit claim contains unknown claims that MUST be understood: #{unknown_claims.join(", ")}"
398
324
  end
399
325
  end
400
326
 
401
- # Step 15: Validate authority_hints syntax (if present)
402
327
  def validate_authority_hints_syntax
403
328
  authority_hints = @payload["authority_hints"] || @payload[:authority_hints]
404
329
  return unless authority_hints
@@ -418,7 +343,6 @@ module OmniauthOpenidFederation
418
343
  end
419
344
  end
420
345
 
421
- # Step 16: Validate metadata syntax (if present)
422
346
  def validate_metadata_syntax
423
347
  metadata = @payload["metadata"] || @payload[:metadata]
424
348
  return unless metadata
@@ -427,7 +351,6 @@ module OmniauthOpenidFederation
427
351
  raise ValidationError, "Entity statement metadata claim MUST be a JSON object"
428
352
  end
429
353
 
430
- # Validate that metadata values are not null
431
354
  metadata.each do |entity_type, entity_metadata|
432
355
  if entity_metadata.nil?
433
356
  raise ValidationError, "Entity statement metadata claim MUST NOT use null as metadata values"
@@ -438,7 +361,6 @@ module OmniauthOpenidFederation
438
361
  end
439
362
  end
440
363
 
441
- # Step 17: Validate metadata_policy presence (MUST be Subordinate Statement)
442
364
  def validate_metadata_policy_presence
443
365
  metadata_policy = @payload["metadata_policy"] || @payload[:metadata_policy]
444
366
  return unless metadata_policy
@@ -448,7 +370,6 @@ module OmniauthOpenidFederation
448
370
  end
449
371
  end
450
372
 
451
- # Step 18: Validate metadata_policy_crit presence (MUST be Subordinate Statement)
452
373
  def validate_metadata_policy_crit_presence
453
374
  metadata_policy_crit = @payload["metadata_policy_crit"] || @payload[:metadata_policy_crit]
454
375
  return unless metadata_policy_crit
@@ -458,7 +379,6 @@ module OmniauthOpenidFederation
458
379
  end
459
380
  end
460
381
 
461
- # Step 19: Validate constraints presence (MUST be Subordinate Statement)
462
382
  def validate_constraints_presence
463
383
  constraints = @payload["constraints"] || @payload[:constraints]
464
384
  return unless constraints
@@ -468,7 +388,6 @@ module OmniauthOpenidFederation
468
388
  end
469
389
  end
470
390
 
471
- # Step 20: Validate trust_marks presence (MUST be Entity Configuration)
472
391
  def validate_trust_marks_presence
473
392
  trust_marks = @payload["trust_marks"] || @payload[:trust_marks]
474
393
  return unless trust_marks
@@ -478,7 +397,6 @@ module OmniauthOpenidFederation
478
397
  end
479
398
  end
480
399
 
481
- # Step 21: Validate trust_mark_issuers presence (MUST be Entity Configuration)
482
400
  def validate_trust_mark_issuers_presence
483
401
  trust_mark_issuers = @payload["trust_mark_issuers"] || @payload[:trust_mark_issuers]
484
402
  return unless trust_mark_issuers
@@ -488,7 +406,6 @@ module OmniauthOpenidFederation
488
406
  end
489
407
  end
490
408
 
491
- # Step 22: Validate trust_mark_owners presence (MUST be Entity Configuration)
492
409
  def validate_trust_mark_owners_presence
493
410
  trust_mark_owners = @payload["trust_mark_owners"] || @payload[:trust_mark_owners]
494
411
  return unless trust_mark_owners
@@ -1,4 +1,3 @@
1
- require "http"
2
1
  require "jwt"
3
2
  require "base64"
4
3
  require "openssl"