omniauth_openid_federation 2.0.0 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed139d6baa665a3a7dce282bed397079492844cef7b45de0b5787edf3de9284e
4
- data.tar.gz: c4d1f5e957c5aa2340d8cd223bfe1c591ed8b85f79417f775468868a54e4d465
3
+ metadata.gz: da45b4e4b8bb24f3b4191c0a3c297e23dad26f3d89ffefaceebc0b225117dc59
4
+ data.tar.gz: 1f8f747bc2f5815fc0d9b2005fa21a2c542dc08f6b3f223072cc7b9e95f0f089
5
5
  SHA512:
6
- metadata.gz: bb56e08a749c63510e1d1456766ed2375827cb41f4171e25fbc260a5e06563fd59559fed10c7cf95ccdf7893a70d1567a8372fbda13c454fa7be765ae775db0d
7
- data.tar.gz: 576b586169d64ba1b4989db48badd0cb1da1f145336e0786dabd8b41770344b9a19b0c43b4a28859738f0d72c63fb985069372868d41fcb64665ab6821cfcf4b
6
+ metadata.gz: 16d2a33e35399bc802f47e6f7d69e7995a20781024bfb22e7d938f8035069dc5f4bfb4a62c0de5aa4a1a42842472e7c19d4bacea342c5cad9cd92d81d59f0d90
7
+ data.tar.gz: efec81bf4ec61d381ee7c03f2dc5e598c9d4d47a7768847f4353c746849cc141b339f301f9f99c3e5f8758d43248bd2682fc7c70fa2df612620f4f6805e878da
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 2.1.0 (2026-10-29)
6
+
7
+ - BREAKING: Require Ruby `>= 3.4` (gemspec); drop Rails 6 and TruffleRuby appraisal targets from the tested matrix
8
+ - Fix trust chain resolution to bind fetched entity configurations to the requested entity id and verify trust-anchor statements with pinned `trust_anchors[].jwks`
9
+ - Fix `EntityStatementValidator` authority_hints checks to use the subject entity configuration (`subject_entity_configuration:`)
10
+ - Fix UserInfo merge so `sub` must match the ID token and identity claims (`iss`, `sub`, `aud`, `exp`, `iat`) stay from the ID token
11
+ - Fix signed JWKS handling to fail closed on signature, validation, and fetch errors instead of falling back to unsigned `jwks_uri`
12
+ - Fix signed JWKS cache keys to include the verifying `entity_jwks` digest
13
+ - Fix OAuth callback to validate `state` before handling authorization `error` responses
14
+ - Fix configured `entity_statement_path` missing-file handling to raise `ConfigurationError` instead of silent network fallback
15
+ - Fix endpoint resolution so configured entity-statement files under `Dir.tmpdir` are not deleted after resolve
16
+ - Fix `client_entity_statement_url` so `load_client_entity_statement` fetches from the URL
17
+ - Fix `Utils.validate_file_path!` allowed-directory checks to require a path-separator boundary
18
+ - Stop streaming HTTP responses at the configured byte limit and close oversized responses before buffering the remaining body
19
+ - Add `rfcs/` starter pack: process (RFC 0001), positioning (RFC 0002), and Standards Track design RFCs 0003–0006 for strategy, trust chain, token verification, and federation endpoint
20
+ - Document Ruby `>= 3.4` in README requirements
21
+
3
22
  ## 2.0.0 (2026-07-06)
4
23
 
5
24
  - BREAKING: Replace `openid_connect` and `json-jwt` with `OmniauthOpenidFederation::OidcClient`, `AccessToken`, and `IdToken` built on `oauth2` and `jwt`
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # omniauth_openid_federation
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/omniauth_openid_federation.svg?v=1.1.0)](https://badge.fury.io/rb/omniauth_openid_federation) [![Test Status](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg?token=CX3O9M1GIT)](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_omniauth_openid_federation.rb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_omniauth_openid_federation.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/omniauth_openid_federation.svg?v=1.1.0)](https://badge.fury.io/rb/omniauth_openid_federation) [![Test Status](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg?token=CX3O9M1GIT)](https://app.codecov.io/github/amkisko/omniauth_openid_federation.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_omniauth_openid_federation.rb&metric=alert_status)](https://sonarcloud.io/project/overview?id=amkisko_omniauth_openid_federation.rb)
4
4
 
5
5
  OmniAuth strategy for OpenID Federation providers with comprehensive security features, supporting signed request objects, ID token encryption, and full OpenID Federation 1.0 compliance.
6
6
 
7
+ Design changes go through `rfcs/` (RFC 0001).
8
+
7
9
  ## Installation
8
10
 
9
11
  ```ruby
@@ -471,8 +473,8 @@ rake openid_federation:test_authentication_flow[
471
473
 
472
474
  ## Requirements
473
475
 
474
- - Ruby >= 3.0
475
- - Rails >= 6.1 (or compatible Rack application)
476
+ - Ruby >= 3.4
477
+ - Rails >= 7.2 (or compatible Rack application; see Appraisal gemfiles)
476
478
  - OpenSSL (for RSA key operations)
477
479
 
478
480
  ## Example Files
@@ -492,7 +494,7 @@ bundle install
492
494
  bundle exec polyrun parallel-rspec --workers 5 -c polyrun.yml
493
495
  ```
494
496
 
495
- Same flow as CI; optional wrappers [`bin/rspec_parallel`](bin/rspec_parallel) and [`bin/appraisals`](bin/appraisals) (runs **`polyrun parallel-rspec`** per Appraisal gemfile). Details: **[POLYRUN.md](POLYRUN.md)**.
497
+ Same flow as CI; optional wrappers [`bin/rspec_parallel`](bin/rspec_parallel) and [`bin/appraisals`](bin/appraisals) (runs **`polyrun parallel-rspec`** per Appraisal gemfile). Details: **[spec/README.md](spec/README.md)** and **`polyrun.yml`**.
496
498
 
497
499
  ## Contributing
498
500
 
@@ -517,6 +519,19 @@ Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidel
517
519
  - [anyway_config](https://github.com/palkan/anyway_config) - Configuration management
518
520
  - [action_reporter](https://github.com/basecamp/action_reporter) - Error reporting
519
521
 
522
+ ## Links
523
+
524
+ - [GitHub](https://github.com/amkisko/omniauth_openid_federation.rb)
525
+ - [GitLab](https://gitlab.com/amkisko/omniauth_openid_federation.rb)
526
+ - [RubyGems](https://rubygems.org/gems/omniauth_openid_federation)
527
+ - [Versions Atom](https://rubygems.org/gems/omniauth_openid_federation/versions.atom) (feed id `10007301350098`)
528
+ - [libraries.io](https://libraries.io/rubygems/omniauth_openid_federation)
529
+ - [Deps.dev](https://deps.dev/rubygems/omniauth_openid_federation)
530
+ - [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_omniauth_openid_federation.rb)
531
+ - [Snyk](https://snyk.io/test/github/amkisko/omniauth_openid_federation.rb)
532
+ - [Codecov](https://app.codecov.io/github/amkisko/omniauth_openid_federation.rb)
533
+ - [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/omniauth_openid_federation.rb)
534
+
520
535
  ## License
521
536
 
522
537
  MIT License. See [LICENSE.md](LICENSE.md) for details.
data/SECURITY.md CHANGED
@@ -2,27 +2,24 @@
2
2
 
3
3
  ## Reporting a Vulnerability
4
4
 
5
- **Do NOT** open a public GitHub issue for security vulnerabilities.
5
+ Email security details to: security@kiskolabs.com
6
6
 
7
- Email security details to: **security@kiskolabs.com**
8
-
9
- Include: description, steps to reproduce, potential impact, and suggested fix (if available).
7
+ Send the report only to that address. Include description, steps to reproduce, potential impact, and a suggested fix when you have one.
10
8
 
11
9
  ### Response Timeline
12
10
 
13
- - We will acknowledge receipt of your report
14
- - We will provide an initial assessment
15
- - We will keep you informed of our progress and resolution timeline
11
+ - We will acknowledge receipt of your report.
12
+ - We will provide an initial assessment.
13
+ - We will keep you informed of our progress and resolution timeline.
16
14
 
17
15
  ### Disclosure Policy
18
16
 
19
- - We will work with you to understand and resolve the issue
20
- - We will credit you for the discovery (unless you prefer to remain anonymous)
21
- - We will publish a security advisory after the vulnerability is patched
22
- - We will coordinate public disclosure with you
17
+ - We will work with you to understand and resolve the issue.
18
+ - We will credit you for the discovery unless you prefer to remain anonymous.
19
+ - We will publish a security advisory after the vulnerability is patched.
20
+ - We will coordinate public disclosure with you.
23
21
 
24
22
  ## Automation Security
25
23
 
26
- * **Context Isolation:** It is strictly forbidden to include production credentials, API keys, or Personally Identifiable Information (PII) in prompts sent to third-party LLMs or automation services.
27
-
28
- * **Supply Chain:** All automated dependencies must be verified.
24
+ - Keep production credentials, API keys, and personal data out of prompts sent to third-party models or automation services.
25
+ - Verify automated dependencies.
@@ -15,11 +15,32 @@ module OmniauthOpenidFederation
15
15
  # Generate cache key for signed JWKS
16
16
  #
17
17
  # @param signed_jwks_uri [String] The signed JWKS URI
18
+ # @param entity_jwks [Hash, Array, nil] Entity JWKS used to verify the signed set
18
19
  # @return [String] Cache key
19
- def self.key_for_signed_jwks(signed_jwks_uri)
20
- "omniauth_openid_federation:signed_jwks:#{Digest::SHA256.hexdigest(signed_jwks_uri)}"
20
+ def self.key_for_signed_jwks(signed_jwks_uri, entity_jwks: nil)
21
+ material = "#{signed_jwks_uri}:#{canonical_jwks_digest(entity_jwks)}"
22
+ "omniauth_openid_federation:signed_jwks:#{Digest::SHA256.hexdigest(material)}"
21
23
  end
22
24
 
25
+ def self.canonical_jwks_digest(entity_jwks)
26
+ return "none" if entity_jwks.nil?
27
+
28
+ require "json"
29
+ Digest::SHA256.hexdigest(JSON.generate(deep_sort_keys(entity_jwks)))
30
+ end
31
+
32
+ def self.deep_sort_keys(value)
33
+ case value
34
+ when Hash
35
+ value.keys.map(&:to_s).sort.index_with { |key| deep_sort_keys(value[key] || value[key.to_sym]) }
36
+ when Array
37
+ value.map { |item| deep_sort_keys(item) }
38
+ else
39
+ value
40
+ end
41
+ end
42
+ private_class_method :canonical_jwks_digest, :deep_sort_keys
43
+
23
44
  # Generate cache key for locally served JWKS (federation endpoint)
24
45
  #
25
46
  # @param issuer [String] Federation endpoint issuer entity identifier
@@ -47,9 +68,10 @@ module OmniauthOpenidFederation
47
68
  # Delete signed JWKS cache
48
69
  #
49
70
  # @param signed_jwks_uri [String] The signed JWKS URI
50
- def self.delete_signed_jwks(signed_jwks_uri)
71
+ # @param entity_jwks [Hash, Array, nil] Entity JWKS used when the entry was cached
72
+ def self.delete_signed_jwks(signed_jwks_uri, entity_jwks: nil)
51
73
  return unless CacheAdapter.available?
52
- CacheAdapter.delete(key_for_signed_jwks(signed_jwks_uri))
74
+ CacheAdapter.delete(key_for_signed_jwks(signed_jwks_uri, entity_jwks: entity_jwks))
53
75
  end
54
76
  end
55
77
  end
@@ -17,5 +17,8 @@ module OmniauthOpenidFederation
17
17
 
18
18
  # Minimum RSA modulus size for OIDC signing and encryption keys (FTN and common federation profiles)
19
19
  MIN_RSA_KEY_BITS = 2048
20
+
21
+ # Maximum HTTP response body size for federation/JWKS fetches (1 MiB)
22
+ MAX_HTTP_RESPONSE_BODY_BYTES = 1_048_576
20
23
  end
21
24
  end
@@ -39,9 +39,13 @@ module OmniauthOpenidFederation
39
39
  # @param issuer_entity_configuration [Hash, EntityStatement, nil] Optional: Entity Configuration of the issuer
40
40
  # Required for validating Subordinate Statements (when iss != sub)
41
41
  # @param clock_skew_tolerance [Integer, nil] Clock skew tolerance in seconds (default: from config)
42
- def initialize(jwt_string:, issuer_entity_configuration: nil, clock_skew_tolerance: nil)
42
+ def initialize(jwt_string:, issuer_entity_configuration: nil, subject_entity_configuration: nil, clock_skew_tolerance: nil)
43
43
  @jwt_string = jwt_string
44
44
  @issuer_entity_configuration = issuer_entity_configuration
45
+ # Subject Entity Configuration supplies authority_hints for Subordinate Statements.
46
+ # When omitted, issuer_entity_configuration is used for backward compatibility with
47
+ # callers that historically passed the subject config under that name.
48
+ @subject_entity_configuration = subject_entity_configuration
45
49
  @clock_skew_tolerance = clock_skew_tolerance || OmniauthOpenidFederation.config.clock_skew_tolerance
46
50
  @header = nil
47
51
  @payload = nil
@@ -153,32 +157,42 @@ module OmniauthOpenidFederation
153
157
  @is_subordinate_statement = !@is_entity_configuration
154
158
  end
155
159
 
156
- # Step 7: For Subordinate Statements, validate iss matches authority_hints
160
+ # Step 7: For Subordinate Statements, validate iss matches subject's authority_hints
157
161
  def validate_authority_hints
158
- unless @issuer_entity_configuration
159
- # If issuer configuration not provided, we can't validate authority_hints
160
- # This is acceptable for basic validation, but should be done for full validation
161
- OmniauthOpenidFederation::Logger.warn("[EntityStatementValidator] Cannot validate authority_hints: issuer entity configuration not provided")
162
+ subject_source = @subject_entity_configuration || @issuer_entity_configuration
163
+ unless subject_source
164
+ OmniauthOpenidFederation::Logger.warn("[EntityStatementValidator] Cannot validate authority_hints: subject entity configuration not provided")
162
165
  return
163
166
  end
164
167
 
165
- issuer_config = if @issuer_entity_configuration.is_a?(Hash)
166
- @issuer_entity_configuration
167
- elsif @issuer_entity_configuration.respond_to?(:parse)
168
- @issuer_entity_configuration.parse
169
- else
170
- raise ValidationError, "Invalid issuer entity configuration format"
171
- end
172
-
173
- authority_hints = issuer_config[:claims]&.fetch("authority_hints", []) ||
174
- issuer_config["claims"]&.fetch("authority_hints", []) ||
175
- issuer_config.fetch("authority_hints", [])
168
+ subject_config = normalize_entity_configuration_hash(subject_source)
169
+ authority_hints = extract_authority_hints_from_config(subject_config)
176
170
 
177
171
  unless authority_hints.is_a?(Array) && authority_hints.include?(@payload["iss"])
178
172
  raise ValidationError, "Subordinate Statement issuer '#{@payload["iss"]}' MUST be listed in the authority_hints array of the subject's Entity Configuration"
179
173
  end
180
174
  end
181
175
 
176
+ def normalize_entity_configuration_hash(config)
177
+ if config.is_a?(Hash)
178
+ config
179
+ elsif config.respond_to?(:parse)
180
+ config.parse
181
+ else
182
+ raise ValidationError, "Invalid entity configuration format"
183
+ end
184
+ end
185
+
186
+ def extract_authority_hints_from_config(config)
187
+ claims = config[:claims] || config["claims"]
188
+ if claims.is_a?(Hash)
189
+ hints = claims[:authority_hints] || claims["authority_hints"]
190
+ return hints if hints
191
+ end
192
+
193
+ config[:authority_hints] || config["authority_hints"] || []
194
+ end
195
+
182
196
  def validate_iat_claim
183
197
  iat = @payload["iat"]
184
198
  if iat.nil?
@@ -60,7 +60,10 @@ module OmniauthOpenidFederation
60
60
  # @raise [FetchError] If fetching fails
61
61
  # @raise [ValidationError] If validation fails
62
62
  def self.fetch!(signed_jwks_uri, entity_jwks, cache_key: nil, cache_ttl: nil, force_refresh: false)
63
- cache_key ||= OmniauthOpenidFederation::Cache.key_for_signed_jwks(signed_jwks_uri)
63
+ cache_key ||= OmniauthOpenidFederation::Cache.key_for_signed_jwks(
64
+ signed_jwks_uri,
65
+ entity_jwks: entity_jwks
66
+ )
64
67
  config = OmniauthOpenidFederation::Configuration.config
65
68
  cache_ttl ||= config.cache_ttl
66
69
  rotate_on_errors = config.rotate_on_errors
@@ -1,3 +1,5 @@
1
+ require_relative "trust_chain_signature"
2
+
1
3
  require "cgi"
2
4
  require "json"
3
5
  require "base64"
@@ -37,6 +39,8 @@ module OmniauthOpenidFederation
37
39
  #
38
40
  # Resolves and validates trust chains from a Leaf Entity to a Trust Anchor.
39
41
  class TrustChainResolver
42
+ include TrustChainSignature
43
+
40
44
  # Initialize resolver
41
45
  #
42
46
  # @param leaf_entity_id [String] Entity Identifier of the Leaf Entity
@@ -72,7 +76,7 @@ module OmniauthOpenidFederation
72
76
  current_entity_id = @leaf_entity_id
73
77
  current_config = leaf_config
74
78
 
75
- while current_config && !is_trust_anchor?(current_config)
79
+ while current_config && !trust_anchor_for(current_entity_id)
76
80
  authority_hints = extract_authority_hints(current_config)
77
81
 
78
82
  if StringHelpers.blank?(authority_hints)
@@ -89,13 +93,21 @@ module OmniauthOpenidFederation
89
93
 
90
94
  begin
91
95
  issuer_config = fetch_entity_configuration(authority_id)
96
+ validate_entity_statement(issuer_config, nil)
97
+
92
98
  subordinate_statement = fetch_subordinate_statement(
93
99
  issuer: authority_id,
94
100
  subject: current_entity_id,
95
101
  issuer_config: issuer_config
96
102
  )
97
103
 
98
- validate_entity_statement(subordinate_statement, issuer_config)
104
+ validate_entity_statement(
105
+ subordinate_statement,
106
+ issuer_config,
107
+ subject_config: current_config,
108
+ expected_issuer: authority_id,
109
+ expected_subject: current_entity_id
110
+ )
99
111
 
100
112
  @resolved_statements << subordinate_statement
101
113
  @visited_entities.add(authority_id)
@@ -104,7 +116,8 @@ module OmniauthOpenidFederation
104
116
  current_config = issuer_config
105
117
  found_next = true
106
118
  break
107
- rescue ValidationError, FetchError => e
119
+ rescue ValidationError, FetchError, SignatureError => e
120
+ @visited_entities.add(authority_id)
108
121
  OmniauthOpenidFederation::Logger.warn("[TrustChainResolver] Failed to resolve via #{authority_id}: #{e.message}")
109
122
  OmniauthOpenidFederation::Instrumentation.notify_trust_chain_validation_failed(
110
123
  entity_id: current_entity_id,
@@ -122,7 +135,7 @@ module OmniauthOpenidFederation
122
135
  end
123
136
  end
124
137
 
125
- unless is_trust_anchor?(current_config)
138
+ unless trust_anchor_for(current_entity_id)
126
139
  error_msg = "Trust chain did not terminate at a configured Trust Anchor"
127
140
  OmniauthOpenidFederation::Instrumentation.notify_trust_chain_validation_failed(
128
141
  entity_id: @leaf_entity_id,
@@ -156,12 +169,24 @@ module OmniauthOpenidFederation
156
169
 
157
170
  begin
158
171
  config = EntityStatement.fetch!(entity_statement_url, timeout: @timeout)
172
+ assert_entity_configuration_ids!(config, entity_id)
159
173
  @entity_configurations[entity_id] = config
160
174
  rescue OmniauthOpenidFederation::NetworkError => e
161
175
  raise FetchError, "Failed to fetch entity configuration from #{entity_statement_url}: #{e.message}"
162
176
  end
163
177
  end
164
178
 
179
+ def assert_entity_configuration_ids!(config, expected_entity_id)
180
+ parsed = config.parse
181
+ iss = entity_issuer(parsed)
182
+ sub = field(parsed, :sub)
183
+
184
+ unless iss == expected_entity_id && sub == expected_entity_id
185
+ raise ValidationError,
186
+ "Entity configuration iss/sub must equal requested entity id '#{expected_entity_id}' (iss=#{iss}, sub=#{sub})"
187
+ end
188
+ end
189
+
165
190
  def fetch_subordinate_statement(issuer:, subject:, issuer_config:)
166
191
  fetch_endpoint = extract_fetch_endpoint(issuer_config)
167
192
 
@@ -186,10 +211,17 @@ module OmniauthOpenidFederation
186
211
  end
187
212
 
188
213
  def extract_fetch_endpoint(entity_config)
189
- # Fetch endpoint is typically at /.well-known/openid-federation/fetch
190
- # or can be specified in metadata
191
214
  parsed = entity_config.parse
192
- issuer = parsed[:issuer] || parsed[:iss] || parsed["issuer"] || parsed["iss"]
215
+ payload = entity_config.decode_payload
216
+ metadata = field(payload, :metadata) || {}
217
+ federation_entity = field(metadata, :federation_entity) || {}
218
+ openid_provider = field(metadata, :openid_provider) || {}
219
+
220
+ endpoint = field(federation_entity, :federation_fetch_endpoint) || field(openid_provider, :federation_fetch_endpoint)
221
+
222
+ return endpoint if StringHelpers.present?(endpoint)
223
+
224
+ issuer = entity_issuer(parsed)
193
225
  return nil unless issuer
194
226
 
195
227
  "#{issuer}/.well-known/openid-federation/fetch"
@@ -200,58 +232,46 @@ module OmniauthOpenidFederation
200
232
  parsed[:authority_hints] || parsed["authority_hints"]
201
233
  end
202
234
 
203
- def validate_entity_statement(statement, issuer_config)
235
+ def validate_entity_statement(statement, issuer_config, subject_config: nil, expected_issuer: nil, expected_subject: nil)
204
236
  validator = EntityStatementValidator.new(
205
237
  jwt_string: statement.entity_statement,
206
- issuer_entity_configuration: issuer_config
238
+ issuer_entity_configuration: issuer_config,
239
+ subject_entity_configuration: subject_config
207
240
  )
208
241
  validator.validate!
209
- verify_entity_statement_signature!(statement, issuer_config)
210
- end
211
242
 
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
243
+ if expected_issuer || expected_subject
244
+ parsed = statement.parse
245
+ iss = entity_issuer(parsed)
246
+ sub = parsed[:sub] || parsed["sub"]
218
247
 
219
- header = JSON.parse(Base64.urlsafe_decode64(parts[0]))
220
- kid = header["kid"] || header[:kid]
248
+ if expected_issuer && iss != expected_issuer
249
+ raise ValidationError, "Subordinate statement iss '#{iss}' does not match expected issuer '#{expected_issuer}'"
250
+ end
251
+ if expected_subject && sub != expected_subject
252
+ raise ValidationError, "Subordinate statement sub '#{sub}' does not match expected subject '#{expected_subject}'"
253
+ end
254
+ end
221
255
 
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 }
256
+ verify_entity_statement_signature!(statement, issuer_config)
257
+ end
225
258
 
226
- unless signing_key_data
227
- raise ValidationError, "Signing key with kid '#{kid}' not found in issuer JWKS for signature verification"
228
- end
259
+ def normalize_jwks(jwks)
260
+ return {} if jwks.nil?
261
+ return jwks if jwks.is_a?(Hash)
262
+ return {"keys" => jwks} if jwks.is_a?(Array)
229
263
 
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
264
+ {}
236
265
  end
237
266
 
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
267
+ def trust_anchor_for(entity_id)
268
+ @trust_anchors.find { |ta| ta[:entity_id] == entity_id }
246
269
  end
247
270
 
248
271
  def is_trust_anchor?(entity_config)
249
272
  parsed = entity_config.parse
250
- entity_id = parsed[:issuer] || parsed[:iss] || parsed["issuer"] || parsed["iss"]
251
-
252
- @trust_anchors.any? do |ta|
253
- ta[:entity_id] == entity_id
254
- end
273
+ entity_id = entity_issuer(parsed)
274
+ !trust_anchor_for(entity_id).nil?
255
275
  end
256
276
  end
257
277
  end
@@ -0,0 +1,62 @@
1
+ module OmniauthOpenidFederation
2
+ module Federation
3
+ module TrustChainSignature
4
+ private
5
+
6
+ def verify_entity_statement_signature!(statement, issuer_config)
7
+ jwt_string = statement.entity_statement
8
+ kid, algorithm = signature_header(jwt_string)
9
+ public_key = signing_public_key(statement, issuer_config, kid)
10
+ ::JWT.decode(jwt_string, public_key, true, {algorithm: algorithm})
11
+ rescue ::JWT::DecodeError, ::JWT::VerificationError => e
12
+ error_msg = "Entity statement signature validation failed: #{e.class} - #{e.message}"
13
+ OmniauthOpenidFederation::Logger.error("[TrustChainResolver] #{error_msg}")
14
+ raise SignatureError, error_msg, e.backtrace
15
+ end
16
+
17
+ def signature_header(jwt_string)
18
+ parts = jwt_string.split(".")
19
+ if parts.length != EntityStatementValidator::JWT_PARTS_COUNT
20
+ raise ValidationError, "Invalid JWT format: expected #{EntityStatementValidator::JWT_PARTS_COUNT} parts, got #{parts.length}"
21
+ end
22
+
23
+ header = JSON.parse(Base64.urlsafe_decode64(parts[0]))
24
+ kid = field(header, :kid)
25
+ algorithm = field(header, :alg) || "RS256"
26
+
27
+ unless EntityStatementValidator::SUPPORTED_ALGORITHMS.include?(algorithm)
28
+ raise ValidationError, "Unsupported entity statement algorithm: #{algorithm}"
29
+ end
30
+
31
+ [kid, algorithm]
32
+ end
33
+
34
+ def signing_public_key(statement, issuer_config, kid)
35
+ signing_keys = issuer_signing_keys(statement, issuer_config)
36
+ keys = field(signing_keys, :keys) || []
37
+ signing_key_data = keys.find { |key| field(key, :kid) == kid }
38
+ unless signing_key_data
39
+ raise ValidationError, "Signing key with kid '#{kid}' not found in issuer JWKS for signature verification"
40
+ end
41
+
42
+ OmniauthOpenidFederation::KeyExtractor.jwk_to_openssl_key(signing_key_data)
43
+ end
44
+
45
+ def issuer_signing_keys(statement, issuer_config)
46
+ parsed = (issuer_config || statement).parse
47
+ trust_anchor = trust_anchor_for(entity_issuer(parsed))
48
+ return normalize_jwks(trust_anchor[:jwks]) if trust_anchor
49
+
50
+ field(parsed, :jwks) || {}
51
+ end
52
+
53
+ def entity_issuer(parsed)
54
+ parsed[:issuer] || parsed[:iss] || parsed["issuer"] || parsed["iss"]
55
+ end
56
+
57
+ def field(hash, key)
58
+ hash[key] || hash[key.to_s]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -62,12 +62,49 @@ module OmniauthOpenidFederation
62
62
  next
63
63
  end
64
64
 
65
- return response
65
+ return enforce_body_limit!(response)
66
66
  rescue *RETRYABLE_ERRORS => error
67
67
  retries = handle_network_error(method, uri, error, retries, max_retries, max_attempts, retry_delay)
68
68
  end
69
69
  end
70
70
 
71
+ def self.enforce_body_limit!(response)
72
+ max_bytes = Constants::MAX_HTTP_RESPONSE_BODY_BYTES
73
+ content_length = response.headers&.[]("Content-Length") || response.headers&.[]("content-length")
74
+ if content_length && content_length.to_i > max_bytes
75
+ raise OmniauthOpenidFederation::NetworkError,
76
+ "HTTP response Content-Length exceeds limit (#{max_bytes} bytes)"
77
+ end
78
+
79
+ response = buffer_streamed_response(response, max_bytes) if response.body.is_a?(HTTP::Response::Body)
80
+ body = response.body
81
+ size = if body.respond_to?(:bytesize)
82
+ body.bytesize
83
+ else
84
+ body.to_s.bytesize
85
+ end
86
+
87
+ if size > max_bytes
88
+ raise OmniauthOpenidFederation::NetworkError,
89
+ "HTTP response body exceeds limit (#{max_bytes} bytes)"
90
+ end
91
+ response
92
+ end
93
+
94
+ def self.buffer_streamed_response(response, max_bytes)
95
+ contents = String.new(encoding: response.body.encoding)
96
+ response.body.each do |chunk|
97
+ if contents.bytesize + chunk.bytesize > max_bytes
98
+ response.connection.close
99
+ raise OmniauthOpenidFederation::NetworkError, "HTTP response body exceeds limit (#{max_bytes} bytes)"
100
+ end
101
+ contents << chunk
102
+ end
103
+ HTTP::Response.new(status: response.status.code, version: response.version,
104
+ headers: response.headers, proxy_headers: response.proxy_headers,
105
+ request: response.request, body: contents)
106
+ end
107
+
71
108
  def self.perform_request(http_client, headers, method, uri, form)
72
109
  client = http_client
73
110
  client = client.headers(headers) unless headers.empty?
@@ -178,6 +215,6 @@ module OmniauthOpenidFederation
178
215
 
179
216
  private_class_method :request, :perform_request, :should_retry_for_status?, :retry_after_status,
180
217
  :handle_network_error, :build_http_client, :build_http_options_hash, :ssl_verify_mode, :ssl_ca_file,
181
- :max_retries_for, :resolve_timeout, :retryable_http_status?, :retry_delay_for
218
+ :max_retries_for, :resolve_timeout, :retryable_http_status?, :retry_delay_for, :enforce_body_limit!, :buffer_streamed_response
182
219
  end
183
220
  end
@@ -176,12 +176,10 @@ module OmniauthOpenidFederation
176
176
  return nil if OmniauthOpenidFederation::StringHelpers.blank?(signed_jwks_uri)
177
177
 
178
178
  OmniauthOpenidFederation::Federation::SignedJWKS.fetch!(signed_jwks_uri, parsed[:entity_jwks])
179
- rescue OmniauthOpenidFederation::SecurityError => error
180
- OmniauthOpenidFederation::Logger.error("[JwtResponseDecoder] Security error: #{error.message}")
181
- nil
182
- rescue
183
- OmniauthOpenidFederation::Logger.warn("[JwtResponseDecoder] Failed to fetch signed JWKS, falling back to standard JWKS")
184
- nil
179
+ rescue OmniauthOpenidFederation::SecurityError, OmniauthOpenidFederation::ValidationError,
180
+ OmniauthOpenidFederation::FetchError => error
181
+ OmniauthOpenidFederation::Logger.error("[JwtResponseDecoder] Signed JWKS failure (fail closed): #{error.message}")
182
+ raise
185
183
  end
186
184
 
187
185
  def load_entity_statement_keys_for_jwks_validation(strategy_options)
@@ -12,30 +12,13 @@ module OmniauthOpenidFederation
12
12
  code_param = code_param_raw ? OmniauthOpenidFederation::Validators.sanitize_request_param(code_param_raw) : nil
13
13
  error_param = error_param_raw ? OmniauthOpenidFederation::Validators.sanitize_request_param(error_param_raw) : nil
14
14
  error_description_param = error_description_raw ? OmniauthOpenidFederation::Validators.sanitize_request_param(error_description_raw) : nil
15
- if error_param
16
- error_msg = "Authorization error: #{error_param}"
17
- error_msg += " - #{error_description_param}" if error_description_param
18
- OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
19
- stage: "callback_phase",
20
- error_message: error_msg,
21
- error_class: "AuthorizationError",
22
- request_info: {
23
- remote_ip: request.env["REMOTE_ADDR"],
24
- user_agent: request.env["HTTP_USER_AGENT"],
25
- path: request.path
26
- }
27
- )
28
- env["omniauth_openid_federation.instrumented"] = true
29
- return fail!(:authorization_error, OmniauthOpenidFederation::ValidationError.new(error_msg))
30
- end
31
15
 
32
- # CSRF protection: constant-time state comparison
16
+ # CSRF protection: constant-time state comparison before any OAuth outcome
33
17
  state_session = session["omniauth.state"]
34
18
 
35
19
  if OmniauthOpenidFederation::StringHelpers.blank?(state_param) ||
36
20
  state_session.nil? ||
37
21
  !OmniauthOpenidFederation::SecureCompare.secure_compare(state_param.to_s, state_session.to_s)
38
- # Instrument CSRF detection
39
22
  OmniauthOpenidFederation::Instrumentation.notify_csrf_detected(
40
23
  state_param: state_param ? "[PRESENT]" : "[MISSING]",
41
24
  state_session: state_session ? "[PRESENT]" : "[MISSING]",
@@ -45,14 +28,29 @@ module OmniauthOpenidFederation
45
28
  path: request.path
46
29
  }
47
30
  )
48
- # Mark as instrumented to prevent double instrumentation in fail!
49
31
  env["omniauth_openid_federation.instrumented"] = true
50
32
  return fail!(:csrf_detected, OmniauthOpenidFederation::SecurityError.new("CSRF detected"))
51
33
  end
52
34
 
53
- # Clear state from session
54
35
  session.delete("omniauth.state")
55
36
 
37
+ if error_param
38
+ error_msg = "Authorization error: #{error_param}"
39
+ error_msg += " - #{error_description_param}" if error_description_param
40
+ OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
41
+ stage: "callback_phase",
42
+ error_message: error_msg,
43
+ error_class: "AuthorizationError",
44
+ request_info: {
45
+ remote_ip: request.env["REMOTE_ADDR"],
46
+ user_agent: request.env["HTTP_USER_AGENT"],
47
+ path: request.path
48
+ }
49
+ )
50
+ env["omniauth_openid_federation.instrumented"] = true
51
+ return fail!(:authorization_error, OmniauthOpenidFederation::ValidationError.new(error_msg))
52
+ end
53
+
56
54
  if OmniauthOpenidFederation::StringHelpers.blank?(code_param)
57
55
  # Instrument unexpected authentication break
58
56
  OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
@@ -9,7 +9,12 @@ module OmniauthOpenidFederation
9
9
  return load_client_entity_statement_from_file(entity_statement_path)
10
10
  end
11
11
 
12
- # Priority 2: Check cache (if Rails.cache is available)
12
+ # Priority 2: Fetch from configured URL
13
+ if OmniauthOpenidFederation::StringHelpers.present?(entity_statement_url)
14
+ return load_client_entity_statement_from_url(entity_statement_url)
15
+ end
16
+
17
+ # Priority 3: Check cache (if Rails.cache is available)
13
18
  # This respects background job cache refresh and key rotation
14
19
  if defined?(Rails) && Rails.cache
15
20
  cache_key = "federation:entity_statement"
@@ -22,14 +22,15 @@ module OmniauthOpenidFederation
22
22
 
23
23
  begin
24
24
  # Resolve endpoints from entity statement
25
- # Use temporary file if we have content but no path
25
+ # Use temporary file if we have content but no readable configured path
26
+ created_temp_entity_statement = false
26
27
  entity_statement_path = if options.entity_statement_path && File.exist?(resolve_entity_statement_path(options.entity_statement_path))
27
28
  resolve_entity_statement_path(options.entity_statement_path)
28
29
  else
29
- # Create temporary file for EndpointResolver
30
30
  temp_file = Tempfile.new(["entity_statement", ".jwt"])
31
31
  temp_file.write(entity_statement_content)
32
32
  temp_file.close
33
+ created_temp_entity_statement = true
33
34
  temp_file.path
34
35
  end
35
36
 
@@ -38,8 +39,7 @@ module OmniauthOpenidFederation
38
39
  config: {} # Don't pass client_options here - we want entity statement values
39
40
  )
40
41
 
41
- # Clean up temp file if we created one
42
- if entity_statement_path.start_with?(Dir.tmpdir)
42
+ if created_temp_entity_statement
43
43
  begin
44
44
  File.unlink(entity_statement_path)
45
45
  rescue
@@ -175,13 +175,14 @@ module OmniauthOpenidFederation
175
175
 
176
176
  if entity_statement_content
177
177
  begin
178
- # Use temporary file for EndpointResolver
178
+ created_temp_entity_statement = false
179
179
  entity_statement_path = if options.entity_statement_path && File.exist?(resolve_entity_statement_path(options.entity_statement_path))
180
180
  resolve_entity_statement_path(options.entity_statement_path)
181
181
  else
182
182
  temp_file = Tempfile.new(["entity_statement", ".jwt"])
183
183
  temp_file.write(entity_statement_content)
184
184
  temp_file.close
185
+ created_temp_entity_statement = true
185
186
  temp_file.path
186
187
  end
187
188
 
@@ -193,8 +194,7 @@ module OmniauthOpenidFederation
193
194
 
194
195
  if resolved[:audience] && OmniauthOpenidFederation::StringHelpers.present?(resolved[:audience])
195
196
  OmniauthOpenidFederation::Logger.debug("[Strategy] Resolved audience from entity statement: #{resolved[:audience]}")
196
- # Clean up temp file if we created one
197
- if entity_statement_path.start_with?(Dir.tmpdir)
197
+ if created_temp_entity_statement
198
198
  begin
199
199
  File.unlink(entity_statement_path)
200
200
  rescue
@@ -217,8 +217,7 @@ module OmniauthOpenidFederation
217
217
  OmniauthOpenidFederation::Logger.debug("[Strategy] Could not get entity issuer from entity statement: #{e.message}")
218
218
  end
219
219
 
220
- # Clean up temp file if we created one
221
- if entity_statement_path.start_with?(Dir.tmpdir)
220
+ if created_temp_entity_statement
222
221
  begin
223
222
  File.unlink(entity_statement_path)
224
223
  rescue
@@ -116,49 +116,43 @@ module OmniauthOpenidFederation
116
116
  end
117
117
  end
118
118
 
119
- # 2. Try signed JWKS (if entity statement has signed_jwks_uri)
120
- # This is more likely to have the latest keys during key rotation
119
+ # 2. Try signed JWKS (if entity statement has signed_jwks_uri).
120
+ # When signed_jwks_uri is advertised, signature/validation failures fail closed
121
+ # instead of falling back to an unsigned jwks_uri.
121
122
  if entity_statement_content
122
- begin
123
- parsed = OmniauthOpenidFederation::Federation::EntityStatementHelper.parse_for_signed_jwks_from_content(
124
- entity_statement_content
123
+ parsed = OmniauthOpenidFederation::Federation::EntityStatementHelper.parse_for_signed_jwks_from_content(
124
+ entity_statement_content
125
+ )
126
+ if parsed && parsed[:signed_jwks_uri] && parsed[:entity_jwks]
127
+ OmniauthOpenidFederation::Logger.debug("[Strategy] Fetching signed JWKS for ID token validation (kid: #{kid})")
128
+ signed_jwks = OmniauthOpenidFederation::Federation::SignedJWKS.fetch!(
129
+ parsed[:signed_jwks_uri],
130
+ parsed[:entity_jwks]
125
131
  )
126
- if parsed && parsed[:signed_jwks_uri] && parsed[:entity_jwks]
127
- OmniauthOpenidFederation::Logger.debug("[Strategy] Fetching signed JWKS for ID token validation (kid: #{kid})")
128
- signed_jwks = OmniauthOpenidFederation::Federation::SignedJWKS.fetch!(
129
- parsed[:signed_jwks_uri],
130
- parsed[:entity_jwks]
131
- )
132
- # Ensure it's in the format expected by JWT.decode
133
- jwks_hash = if signed_jwks.is_a?(Hash) && signed_jwks.key?("keys")
134
- signed_jwks
135
- elsif signed_jwks.is_a?(Hash) && signed_jwks.key?(:keys)
136
- {"keys" => signed_jwks[:keys]}
137
- elsif signed_jwks.is_a?(Array)
138
- {"keys" => signed_jwks}
139
- end
140
132
 
141
- keys = jwks_hash&.dig("keys")
142
- if keys&.is_a?(Array) && !keys.empty?
143
- # Track first valid JWKS
144
- first_valid_jwks ||= jwks_hash
145
- # If kid is nil, return JWKS anyway (let JWT decoding fail with proper error)
146
- if kid.nil?
147
- OmniauthOpenidFederation::Logger.debug("[Strategy] Kid is nil, returning signed JWKS for validation attempt")
148
- return jwks_hash
149
- end
150
- # Check if kid is in this JWKS
151
- key_data = keys.find { |key| (key["kid"] || key[:kid]) == kid }
152
- if key_data
153
- OmniauthOpenidFederation::Logger.debug("[Strategy] Found kid '#{kid}' in signed JWKS")
154
- return jwks_hash
155
- else
156
- OmniauthOpenidFederation::Logger.debug("[Strategy] Kid '#{kid}' not found in signed JWKS, trying standard JWKS URI")
157
- end
133
+ jwks_hash = if signed_jwks.is_a?(Hash) && signed_jwks.key?("keys")
134
+ signed_jwks
135
+ elsif signed_jwks.is_a?(Hash) && signed_jwks.key?(:keys)
136
+ {"keys" => signed_jwks[:keys]}
137
+ elsif signed_jwks.is_a?(Array)
138
+ {"keys" => signed_jwks}
139
+ end
140
+
141
+ keys = jwks_hash&.dig("keys")
142
+ if keys&.is_a?(Array) && !keys.empty?
143
+ first_valid_jwks ||= jwks_hash
144
+ if kid.nil?
145
+ OmniauthOpenidFederation::Logger.debug("[Strategy] Kid is nil, returning signed JWKS for validation attempt")
146
+ return jwks_hash
147
+ end
148
+ key_data = keys.find { |key| (key["kid"] || key[:kid]) == kid }
149
+ if key_data
150
+ OmniauthOpenidFederation::Logger.debug("[Strategy] Found kid '#{kid}' in signed JWKS")
151
+ return jwks_hash
152
+ else
153
+ OmniauthOpenidFederation::Logger.debug("[Strategy] Kid '#{kid}' not found in signed JWKS, trying standard JWKS URI")
158
154
  end
159
155
  end
160
- rescue => e
161
- OmniauthOpenidFederation::Logger.debug("[Strategy] Could not fetch signed JWKS: #{e.message}")
162
156
  end
163
157
  end
164
158
 
@@ -20,9 +20,10 @@ module OmniauthOpenidFederation
20
20
  if File.exist?(path)
21
21
  OmniauthOpenidFederation::Logger.debug("[Strategy] Loading provider entity statement from file: #{path}")
22
22
  return File.read(path).strip
23
- else
24
- OmniauthOpenidFederation::Logger.warn("[Strategy] Provider entity statement file not found: #{path}, will try to fetch from URL")
25
23
  end
24
+
25
+ raise OmniauthOpenidFederation::ConfigurationError,
26
+ "Provider entity statement file not found: #{path}"
26
27
  end
27
28
 
28
29
  # Priority 2: Fetch from URL if provided
@@ -1,8 +1,40 @@
1
+ require_relative "../secure_compare"
2
+ require_relative "../string_helpers"
3
+
1
4
  module OmniauthOpenidFederation
2
5
  module Strategy
3
6
  module UserinfoDecoding
7
+ IDENTITY_CLAIM_KEYS = %w[iss sub aud exp iat].freeze
8
+
4
9
  private
5
10
 
11
+ def merge_userinfo_with_id_token_claims(id_token_claims, userinfo_hash)
12
+ normalized_id = stringify_claim_keys(id_token_claims)
13
+ normalized_userinfo = stringify_claim_keys(userinfo_hash)
14
+
15
+ id_sub = normalized_id["sub"]
16
+ userinfo_sub = normalized_userinfo["sub"]
17
+
18
+ if OmniauthOpenidFederation::StringHelpers.blank?(userinfo_sub) ||
19
+ OmniauthOpenidFederation::StringHelpers.blank?(id_sub) ||
20
+ !OmniauthOpenidFederation::SecureCompare.secure_compare(userinfo_sub.to_s, id_sub.to_s)
21
+ raise OmniauthOpenidFederation::ValidationError,
22
+ "UserInfo sub must match ID token sub"
23
+ end
24
+
25
+ merged = normalized_id.merge(normalized_userinfo)
26
+ IDENTITY_CLAIM_KEYS.each do |claim|
27
+ merged[claim] = normalized_id[claim] if normalized_id.key?(claim)
28
+ end
29
+ merged
30
+ end
31
+
32
+ def stringify_claim_keys(claims)
33
+ (claims || {}).each_with_object({}) do |(key, value), hash|
34
+ hash[key.to_s] = value
35
+ end
36
+ end
37
+
6
38
  def decode_userinfo(userinfo)
7
39
  if userinfo.is_a?(String)
8
40
  if encrypted_token?(userinfo)
@@ -162,7 +162,9 @@ module OmniAuth
162
162
  begin
163
163
  userinfo = access_token.userinfo!
164
164
  userinfo_hash = decode_userinfo(userinfo)
165
- id_token_claims.merge(userinfo_hash)
165
+ merge_userinfo_with_id_token_claims(id_token_claims, userinfo_hash)
166
+ rescue OmniauthOpenidFederation::ValidationError
167
+ raise
166
168
  rescue => e
167
169
  error_msg = "Failed to fetch or decode userinfo: #{e.class} - #{e.message}"
168
170
  OmniauthOpenidFederation::Logger.error("[Strategy] #{error_msg}")
@@ -95,7 +95,7 @@ module OmniauthOpenidFederation
95
95
  if allowed_dirs && !allowed_dirs.empty?
96
96
  allowed = allowed_dirs.any? do |dir|
97
97
  expanded_dir = File.expand_path(dir)
98
- resolved.start_with?(expanded_dir)
98
+ resolved == expanded_dir || resolved.start_with?(expanded_dir + File::SEPARATOR)
99
99
  end
100
100
  unless allowed
101
101
  raise SecurityError, "File path outside allowed directories: #{sanitize_path(path)}"
@@ -1,3 +1,3 @@
1
1
  module OmniauthOpenidFederation
2
- VERSION = "2.0.0".freeze
2
+ VERSION = "2.1.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth_openid_federation
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -99,14 +99,28 @@ dependencies:
99
99
  requirements:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: 2.1.2
102
+ version: 2.2.0
103
103
  type: :development
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - ">="
108
108
  - !ruby/object:Gem::Version
109
- version: 2.1.2
109
+ version: 2.2.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: prosopite
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '2.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.0'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: webmock
112
126
  requirement: !ruby/object:Gem::Requirement
@@ -301,14 +315,14 @@ dependencies:
301
315
  requirements:
302
316
  - - "~>"
303
317
  - !ruby/object:Gem::Version
304
- version: '3.9'
318
+ version: '4.0'
305
319
  type: :development
306
320
  prerelease: false
307
321
  version_requirements: !ruby/object:Gem::Requirement
308
322
  requirements:
309
323
  - - "~>"
310
324
  - !ruby/object:Gem::Version
311
- version: '3.9'
325
+ version: '4.0'
312
326
  - !ruby/object:Gem::Dependency
313
327
  name: webrick
314
328
  requirement: !ruby/object:Gem::Requirement
@@ -490,6 +504,7 @@ files:
490
504
  - lib/omniauth_openid_federation/federation/metadata_policy_merger.rb
491
505
  - lib/omniauth_openid_federation/federation/signed_jwks.rb
492
506
  - lib/omniauth_openid_federation/federation/trust_chain_resolver.rb
507
+ - lib/omniauth_openid_federation/federation/trust_chain_signature.rb
493
508
  - lib/omniauth_openid_federation/federation_endpoint.rb
494
509
  - lib/omniauth_openid_federation/http_client.rb
495
510
  - lib/omniauth_openid_federation/http_errors.rb
@@ -556,14 +571,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
556
571
  requirements:
557
572
  - - ">="
558
573
  - !ruby/object:Gem::Version
559
- version: '3.2'
574
+ version: '3.4'
560
575
  required_rubygems_version: !ruby/object:Gem::Requirement
561
576
  requirements:
562
577
  - - ">="
563
578
  - !ruby/object:Gem::Version
564
579
  version: 3.3.0
565
580
  requirements: []
566
- rubygems_version: 4.0.3
581
+ rubygems_version: 4.0.6
567
582
  specification_version: 4
568
583
  summary: OmniAuth strategy for OpenID Federation providers with signed request objects
569
584
  and ID token encryption.