omniauth_openid_federation 1.2.2

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +44 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +922 -0
  5. data/SECURITY.md +28 -0
  6. data/app/controllers/omniauth_openid_federation/federation_controller.rb +160 -0
  7. data/config/routes.rb +17 -0
  8. data/examples/README_INTEGRATION_TESTING.md +399 -0
  9. data/examples/README_MOCK_OP.md +243 -0
  10. data/examples/app/controllers/users/omniauth_callbacks_controller.rb.example +37 -0
  11. data/examples/app/jobs/jwks_rotation_job.rb.example +60 -0
  12. data/examples/app/models/user.rb.example +39 -0
  13. data/examples/config/initializers/devise.rb.example +131 -0
  14. data/examples/config/initializers/federation_endpoint.rb.example +206 -0
  15. data/examples/config/mock_op.yml.example +83 -0
  16. data/examples/config/open_id_connect_config.rb.example +210 -0
  17. data/examples/config/routes.rb.example +12 -0
  18. data/examples/db/migrate/add_omniauth_to_users.rb.example +16 -0
  19. data/examples/integration_test_flow.rb +1334 -0
  20. data/examples/jobs/README.md +194 -0
  21. data/examples/jobs/federation_cache_refresh_job.rb.example +78 -0
  22. data/examples/jobs/federation_files_generation_job.rb.example +87 -0
  23. data/examples/mock_op_server.rb +775 -0
  24. data/examples/mock_rp_server.rb +435 -0
  25. data/lib/omniauth_openid_federation/access_token.rb +504 -0
  26. data/lib/omniauth_openid_federation/cache.rb +39 -0
  27. data/lib/omniauth_openid_federation/cache_adapter.rb +173 -0
  28. data/lib/omniauth_openid_federation/configuration.rb +135 -0
  29. data/lib/omniauth_openid_federation/constants.rb +13 -0
  30. data/lib/omniauth_openid_federation/endpoint_resolver.rb +168 -0
  31. data/lib/omniauth_openid_federation/engine.rb +17 -0
  32. data/lib/omniauth_openid_federation/entity_statement_reader.rb +129 -0
  33. data/lib/omniauth_openid_federation/errors.rb +52 -0
  34. data/lib/omniauth_openid_federation/federation/entity_statement.rb +331 -0
  35. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +188 -0
  36. data/lib/omniauth_openid_federation/federation/entity_statement_fetcher.rb +142 -0
  37. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +87 -0
  38. data/lib/omniauth_openid_federation/federation/entity_statement_parser.rb +198 -0
  39. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +502 -0
  40. data/lib/omniauth_openid_federation/federation/metadata_policy_merger.rb +276 -0
  41. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +210 -0
  42. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +225 -0
  43. data/lib/omniauth_openid_federation/federation_endpoint.rb +949 -0
  44. data/lib/omniauth_openid_federation/http_client.rb +70 -0
  45. data/lib/omniauth_openid_federation/instrumentation.rb +399 -0
  46. data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
  47. data/lib/omniauth_openid_federation/jwks/decode.rb +175 -0
  48. data/lib/omniauth_openid_federation/jwks/fetch.rb +153 -0
  49. data/lib/omniauth_openid_federation/jwks/normalizer.rb +49 -0
  50. data/lib/omniauth_openid_federation/jwks/rotate.rb +97 -0
  51. data/lib/omniauth_openid_federation/jwks/selector.rb +101 -0
  52. data/lib/omniauth_openid_federation/jws.rb +410 -0
  53. data/lib/omniauth_openid_federation/key_extractor.rb +173 -0
  54. data/lib/omniauth_openid_federation/logger.rb +99 -0
  55. data/lib/omniauth_openid_federation/rack_endpoint.rb +187 -0
  56. data/lib/omniauth_openid_federation/railtie.rb +15 -0
  57. data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
  58. data/lib/omniauth_openid_federation/strategy.rb +2114 -0
  59. data/lib/omniauth_openid_federation/string_helpers.rb +30 -0
  60. data/lib/omniauth_openid_federation/tasks_helper.rb +428 -0
  61. data/lib/omniauth_openid_federation/utils.rb +168 -0
  62. data/lib/omniauth_openid_federation/validators.rb +126 -0
  63. data/lib/omniauth_openid_federation/version.rb +3 -0
  64. data/lib/omniauth_openid_federation.rb +99 -0
  65. data/lib/tasks/omniauth_openid_federation.rake +376 -0
  66. data/sig/federation.rbs +218 -0
  67. data/sig/jwks.rbs +63 -0
  68. data/sig/omniauth_openid_federation.rbs +254 -0
  69. data/sig/strategy.rbs +60 -0
  70. metadata +361 -0
@@ -0,0 +1,70 @@
1
+ require_relative "constants"
2
+
3
+ # HTTP client with retry logic and SSL configuration
4
+ module OmniauthOpenidFederation
5
+ class HttpClient
6
+ # Execute an HTTP GET request with retry logic
7
+ #
8
+ # @param uri [String, URI] The URI to fetch
9
+ # @param options [Hash] Request options
10
+ # @option options [Integer] :max_retries Maximum number of retries (default: from config)
11
+ # @option options [Integer] :retry_delay Base retry delay in seconds (default: from config)
12
+ # @option options [Integer] :timeout Request timeout in seconds (default: from config)
13
+ # @return [HTTP::Response] The HTTP response
14
+ # @raise [NetworkError] If the request fails after all retries
15
+ def self.get(uri, options = {})
16
+ max_retries = options[:max_retries] || Configuration.config.max_retries
17
+ retry_delay = options[:retry_delay] || Configuration.config.retry_delay
18
+ timeout = options[:timeout] || Configuration.config.http_timeout
19
+
20
+ http_client = build_http_client(timeout)
21
+
22
+ retries = 0
23
+ begin
24
+ http_client.get(uri)
25
+ rescue HTTP::Error, Timeout::Error, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
26
+ retries += 1
27
+ if retries > max_retries
28
+ error_msg = "Failed to fetch #{uri} after #{max_retries} retries: #{e.class} - #{e.message}"
29
+ OmniauthOpenidFederation::Logger.error("[HttpClient] #{error_msg}")
30
+ raise OmniauthOpenidFederation::NetworkError, error_msg, e.backtrace
31
+ end
32
+
33
+ delay = [retry_delay * retries, Constants::MAX_RETRY_DELAY_SECONDS].min
34
+ OmniauthOpenidFederation::Logger.warn("[HttpClient] Request failed (attempt #{retries}/#{max_retries}), retrying in #{delay}s: #{e.message}")
35
+ sleep(delay)
36
+ retry
37
+ end
38
+ end
39
+
40
+ # Build HTTP client with SSL configuration
41
+ #
42
+ # @param timeout [Integer] Request timeout in seconds
43
+ # @return [HTTP::Client] Configured HTTP client
44
+ def self.build_http_client(timeout)
45
+ http_options_hash = build_http_options_hash || {}
46
+ http_options = HTTP::Options.new(http_options_hash)
47
+ HTTP::Client.new(http_options).timeout(timeout)
48
+ end
49
+
50
+ # Build HTTP options hash from configuration
51
+ #
52
+ # @return [Hash, nil] HTTP options hash or nil
53
+ def self.build_http_options_hash
54
+ config = Configuration.config
55
+
56
+ # If http_options is configured, use it (can be Hash or Proc)
57
+ if config.http_options
58
+ if config.http_options.is_a?(Proc)
59
+ config.http_options.call
60
+ else
61
+ config.http_options
62
+ end
63
+ end
64
+ end
65
+
66
+ private_class_method :build_http_options_hash
67
+
68
+ private_class_method :build_http_client
69
+ end
70
+ end
@@ -0,0 +1,399 @@
1
+ # Instrumentation for omniauth_openid_federation
2
+ # Provides configurable notifications for security events, MITM attacks, and authentication mismatches
3
+ #
4
+ # @example Configure with Sentry
5
+ # OmniauthOpenidFederation.configure do |config|
6
+ # config.instrumentation = ->(event, data) do
7
+ # Sentry.capture_message("OpenID Federation: #{event}", level: :warning, extra: data)
8
+ # end
9
+ # end
10
+ #
11
+ # @example Configure with Honeybadger
12
+ # OmniauthOpenidFederation.configure do |config|
13
+ # config.instrumentation = ->(event, data) do
14
+ # Honeybadger.notify("OpenID Federation: #{event}", context: data)
15
+ # end
16
+ # end
17
+ #
18
+ # @example Configure with custom logger
19
+ # OmniauthOpenidFederation.configure do |config|
20
+ # config.instrumentation = ->(event, data) do
21
+ # Rails.logger.warn("[Security] #{event}: #{data.inspect}")
22
+ # end
23
+ # end
24
+ #
25
+ # @example Disable instrumentation
26
+ # OmniauthOpenidFederation.configure do |config|
27
+ # config.instrumentation = nil
28
+ # end
29
+ module OmniauthOpenidFederation
30
+ module Instrumentation
31
+ # Security event types
32
+ EVENT_CSRF_DETECTED = "csrf_detected"
33
+ EVENT_SIGNATURE_VERIFICATION_FAILED = "signature_verification_failed"
34
+ EVENT_DECRYPTION_FAILED = "decryption_failed"
35
+ EVENT_TOKEN_VALIDATION_FAILED = "token_validation_failed"
36
+ EVENT_KEY_ROTATION_DETECTED = "key_rotation_detected"
37
+ EVENT_KID_NOT_FOUND = "kid_not_found"
38
+ EVENT_ENTITY_STATEMENT_VALIDATION_FAILED = "entity_statement_validation_failed"
39
+ EVENT_FINGERPRINT_MISMATCH = "fingerprint_mismatch"
40
+ EVENT_TRUST_CHAIN_VALIDATION_FAILED = "trust_chain_validation_failed"
41
+ EVENT_ENDPOINT_MISMATCH = "endpoint_mismatch"
42
+ EVENT_UNEXPECTED_AUTHENTICATION_BREAK = "unexpected_authentication_break"
43
+ EVENT_STATE_MISMATCH = "state_mismatch"
44
+ EVENT_MISSING_REQUIRED_CLAIMS = "missing_required_claims"
45
+ EVENT_AUDIENCE_MISMATCH = "audience_mismatch"
46
+ EVENT_ISSUER_MISMATCH = "issuer_mismatch"
47
+ EVENT_EXPIRED_TOKEN = "expired_token"
48
+ EVENT_INVALID_NONCE = "invalid_nonce"
49
+ EVENT_AUTHENTICITY_ERROR = "authenticity_error"
50
+
51
+ class << self
52
+ # Notify about a security event
53
+ #
54
+ # @param event [String] Event type (use constants from this module)
55
+ # @param data [Hash] Event data (will be sanitized to remove sensitive information)
56
+ # @param severity [Symbol] Event severity (:info, :warning, :error)
57
+ # @return [void]
58
+ def notify(event, data: {}, severity: :warning)
59
+ config = Configuration.config
60
+ return unless config.instrumentation
61
+
62
+ # Sanitize data to remove sensitive information
63
+ sanitized_data = sanitize_data(data)
64
+
65
+ # Build notification payload
66
+ payload = {
67
+ event: event,
68
+ severity: severity,
69
+ timestamp: Time.now.utc.iso8601,
70
+ data: sanitized_data
71
+ }
72
+
73
+ # Call the configured instrumentation callback
74
+ begin
75
+ if config.instrumentation.respond_to?(:call)
76
+ config.instrumentation.call(event, payload)
77
+ elsif config.instrumentation.respond_to?(:notify)
78
+ config.instrumentation.notify(event, payload)
79
+ else
80
+ # Assume it's a logger-like object
81
+ log_message = "[OpenID Federation Security] #{event}: #{sanitized_data.inspect}"
82
+ case severity
83
+ when :error
84
+ config.instrumentation.error(log_message)
85
+ when :warning
86
+ config.instrumentation.warn(log_message)
87
+ else
88
+ config.instrumentation.info(log_message)
89
+ end
90
+ end
91
+ rescue => e
92
+ # Don't let instrumentation failures break the authentication flow
93
+ Logger.warn("[Instrumentation] Failed to notify about #{event}: #{e.message}")
94
+ end
95
+ end
96
+
97
+ # Notify about CSRF detection
98
+ #
99
+ # @param data [Hash] Additional context (state_param, state_session, request_info)
100
+ # @return [void]
101
+ def notify_csrf_detected(data = {})
102
+ notify(
103
+ EVENT_CSRF_DETECTED,
104
+ data: {
105
+ reason: "State parameter mismatch - possible CSRF attack",
106
+ **data
107
+ },
108
+ severity: :error
109
+ )
110
+ end
111
+
112
+ # Notify about signature verification failure
113
+ #
114
+ # @param data [Hash] Additional context (token_type, kid, jwks_uri, error_message)
115
+ # @return [void]
116
+ def notify_signature_verification_failed(data = {})
117
+ notify(
118
+ EVENT_SIGNATURE_VERIFICATION_FAILED,
119
+ data: {
120
+ reason: "JWT signature verification failed - possible MITM attack or key rotation",
121
+ **data
122
+ },
123
+ severity: :error
124
+ )
125
+ end
126
+
127
+ # Notify about decryption failure
128
+ #
129
+ # @param data [Hash] Additional context (token_type, error_message)
130
+ # @return [void]
131
+ def notify_decryption_failed(data = {})
132
+ notify(
133
+ EVENT_DECRYPTION_FAILED,
134
+ data: {
135
+ reason: "Token decryption failed - possible MITM attack or key mismatch",
136
+ **data
137
+ },
138
+ severity: :error
139
+ )
140
+ end
141
+
142
+ # Notify about token validation failure
143
+ #
144
+ # @param data [Hash] Additional context (validation_type, missing_claims, error_message)
145
+ # @return [void]
146
+ def notify_token_validation_failed(data = {})
147
+ notify(
148
+ EVENT_TOKEN_VALIDATION_FAILED,
149
+ data: {
150
+ reason: "Token validation failed - possible tampering or configuration mismatch",
151
+ **data
152
+ },
153
+ severity: :error
154
+ )
155
+ end
156
+
157
+ # Notify about key rotation detection
158
+ #
159
+ # @param data [Hash] Additional context (jwks_uri, kid, available_kids)
160
+ # @return [void]
161
+ def notify_key_rotation_detected(data = {})
162
+ notify(
163
+ EVENT_KEY_ROTATION_DETECTED,
164
+ data: {
165
+ reason: "Key rotation detected - kid not found in current JWKS",
166
+ **data
167
+ },
168
+ severity: :warning
169
+ )
170
+ end
171
+
172
+ # Notify about kid not found
173
+ #
174
+ # @param data [Hash] Additional context (kid, jwks_uri, available_kids)
175
+ # @return [void]
176
+ def notify_kid_not_found(data = {})
177
+ notify(
178
+ EVENT_KID_NOT_FOUND,
179
+ data: {
180
+ reason: "Key ID not found in JWKS - possible key rotation or MITM attack",
181
+ **data
182
+ },
183
+ severity: :error
184
+ )
185
+ end
186
+
187
+ # Notify about entity statement validation failure
188
+ #
189
+ # @param data [Hash] Additional context (entity_id, validation_step, error_message)
190
+ # @return [void]
191
+ def notify_entity_statement_validation_failed(data = {})
192
+ notify(
193
+ EVENT_ENTITY_STATEMENT_VALIDATION_FAILED,
194
+ data: {
195
+ reason: "Entity statement validation failed - possible tampering or MITM attack",
196
+ **data
197
+ },
198
+ severity: :error
199
+ )
200
+ end
201
+
202
+ # Notify about fingerprint mismatch
203
+ #
204
+ # @param data [Hash] Additional context (expected_fingerprint, calculated_fingerprint, entity_statement_url)
205
+ # @return [void]
206
+ def notify_fingerprint_mismatch(data = {})
207
+ notify(
208
+ EVENT_FINGERPRINT_MISMATCH,
209
+ data: {
210
+ reason: "Entity statement fingerprint mismatch - possible MITM attack or tampering",
211
+ **data
212
+ },
213
+ severity: :error
214
+ )
215
+ end
216
+
217
+ # Notify about trust chain validation failure
218
+ #
219
+ # @param data [Hash] Additional context (entity_id, trust_anchor, validation_step, error_message)
220
+ # @return [void]
221
+ def notify_trust_chain_validation_failed(data = {})
222
+ notify(
223
+ EVENT_TRUST_CHAIN_VALIDATION_FAILED,
224
+ data: {
225
+ reason: "Trust chain validation failed - possible MITM attack or configuration issue",
226
+ **data
227
+ },
228
+ severity: :error
229
+ )
230
+ end
231
+
232
+ # Notify about endpoint mismatch
233
+ #
234
+ # @param data [Hash] Additional context (endpoint_type, expected, actual, source)
235
+ # @return [void]
236
+ def notify_endpoint_mismatch(data = {})
237
+ notify(
238
+ EVENT_ENDPOINT_MISMATCH,
239
+ data: {
240
+ reason: "Endpoint mismatch detected - possible MITM attack or configuration issue",
241
+ **data
242
+ },
243
+ severity: :warning
244
+ )
245
+ end
246
+
247
+ # Notify about unexpected authentication break
248
+ #
249
+ # @param data [Hash] Additional context (stage, error_message, error_class)
250
+ # @return [void]
251
+ def notify_unexpected_authentication_break(data = {})
252
+ notify(
253
+ EVENT_UNEXPECTED_AUTHENTICATION_BREAK,
254
+ data: {
255
+ reason: "Unexpected authentication break - something that should not fail has failed",
256
+ **data
257
+ },
258
+ severity: :error
259
+ )
260
+ end
261
+
262
+ # Notify about state mismatch
263
+ #
264
+ # @param data [Hash] Additional context (state_param, state_session)
265
+ # @return [void]
266
+ def notify_state_mismatch(data = {})
267
+ notify(
268
+ EVENT_STATE_MISMATCH,
269
+ data: {
270
+ reason: "State parameter mismatch - possible CSRF attack or session issue",
271
+ **data
272
+ },
273
+ severity: :error
274
+ )
275
+ end
276
+
277
+ # Notify about missing required claims
278
+ #
279
+ # @param data [Hash] Additional context (missing_claims, available_claims, token_type)
280
+ # @return [void]
281
+ def notify_missing_required_claims(data = {})
282
+ notify(
283
+ EVENT_MISSING_REQUIRED_CLAIMS,
284
+ data: {
285
+ reason: "Token missing required claims - possible tampering or provider issue",
286
+ **data
287
+ },
288
+ severity: :error
289
+ )
290
+ end
291
+
292
+ # Notify about audience mismatch
293
+ #
294
+ # @param data [Hash] Additional context (expected_audience, actual_audience, token_type)
295
+ # @return [void]
296
+ def notify_audience_mismatch(data = {})
297
+ notify(
298
+ EVENT_AUDIENCE_MISMATCH,
299
+ data: {
300
+ reason: "Token audience mismatch - possible MITM attack or configuration issue",
301
+ **data
302
+ },
303
+ severity: :error
304
+ )
305
+ end
306
+
307
+ # Notify about issuer mismatch
308
+ #
309
+ # @param data [Hash] Additional context (expected_issuer, actual_issuer, token_type)
310
+ # @return [void]
311
+ def notify_issuer_mismatch(data = {})
312
+ notify(
313
+ EVENT_ISSUER_MISMATCH,
314
+ data: {
315
+ reason: "Token issuer mismatch - possible MITM attack or configuration issue",
316
+ **data
317
+ },
318
+ severity: :error
319
+ )
320
+ end
321
+
322
+ # Notify about expired token
323
+ #
324
+ # @param data [Hash] Additional context (exp, current_time, token_type)
325
+ # @return [void]
326
+ def notify_expired_token(data = {})
327
+ notify(
328
+ EVENT_EXPIRED_TOKEN,
329
+ data: {
330
+ reason: "Token expired - possible clock skew or replay attack",
331
+ **data
332
+ },
333
+ severity: :warning
334
+ )
335
+ end
336
+
337
+ # Notify about invalid nonce
338
+ #
339
+ # @param data [Hash] Additional context (expected_nonce, actual_nonce)
340
+ # @return [void]
341
+ def notify_invalid_nonce(data = {})
342
+ notify(
343
+ EVENT_INVALID_NONCE,
344
+ data: {
345
+ reason: "Nonce mismatch - possible replay attack",
346
+ **data
347
+ },
348
+ severity: :error
349
+ )
350
+ end
351
+
352
+ # Notify about authenticity token error (OmniAuth CSRF protection)
353
+ #
354
+ # @param data [Hash] Additional context (error_type, error_message, phase, request_info)
355
+ # @return [void]
356
+ def notify_authenticity_error(data = {})
357
+ notify(
358
+ EVENT_AUTHENTICITY_ERROR,
359
+ data: {
360
+ reason: "OmniAuth authenticity token validation failed - CSRF protection blocked request",
361
+ **data
362
+ },
363
+ severity: :error
364
+ )
365
+ end
366
+
367
+ private
368
+
369
+ # Sanitize data to remove sensitive information
370
+ #
371
+ # @param data [Hash] Raw data
372
+ # @return [Hash] Sanitized data
373
+ def sanitize_data(data)
374
+ return {} unless data.is_a?(Hash)
375
+
376
+ sensitive_keys = [
377
+ :token, :access_token, :id_token, :refresh_token,
378
+ :private_key, :key, :secret, :password,
379
+ :authorization_code, :code,
380
+ :state, :nonce, :state_param, :state_session,
381
+ :fingerprint, :calculated_fingerprint, :expected_fingerprint
382
+ ]
383
+
384
+ data.each_with_object({}) do |(key, value), result|
385
+ key_sym = key.to_sym
386
+ result[key] = if sensitive_keys.include?(key_sym)
387
+ "[REDACTED]"
388
+ elsif value.is_a?(Hash)
389
+ sanitize_data(value)
390
+ elsif value.is_a?(Array)
391
+ value.map { |v| v.is_a?(Hash) ? sanitize_data(v) : v }
392
+ else
393
+ value
394
+ end
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,76 @@
1
+ require_relative "../logger"
2
+ require_relative "../cache"
3
+ require_relative "../cache_adapter"
4
+ require_relative "fetch"
5
+
6
+ # JWKS Cache with automatic invalidation on kid_not_found
7
+ # @see https://openid.net/specs/openid-federation-1_0.html OpenID Federation 1.0 Specification
8
+ #
9
+ # Provides intelligent caching for JWKS with automatic cache invalidation when a key ID (kid)
10
+ # is not found. This handles provider key rotation gracefully by:
11
+ # - Caching JWKS for performance
12
+ # - Invalidating cache when kid_not_found error occurs (after timeout)
13
+ # - Automatically reloading JWKS source on cache miss
14
+ #
15
+ # This prevents malicious requests from triggering cache invalidations by enforcing
16
+ # a grace period (timeout) between invalidations.
17
+ module OmniauthOpenidFederation
18
+ module Jwks
19
+ class Cache
20
+ attr_reader :jwks_source, :timeout_sec, :cache_last_update
21
+
22
+ # Initialize JWKS cache
23
+ #
24
+ # @param jwks_source [Object] The JWKS source (must respond to #jwks and optionally #reload!)
25
+ # @param timeout_sec [Integer] Minimum seconds between cache invalidations (default: 300 = 5 minutes)
26
+ def initialize(jwks_source, timeout_sec = 300)
27
+ @jwks_source = jwks_source
28
+ @timeout_sec = timeout_sec
29
+ @cache_last_update = 0
30
+ @cached_keys = nil
31
+ end
32
+
33
+ # Get JWKS with automatic cache invalidation on kid_not_found
34
+ #
35
+ # @param options [Hash] Options hash
36
+ # @option options [Boolean] :kid_not_found Whether kid was not found (triggers invalidation if timeout passed)
37
+ # @option options [String] :kid The key ID that was not found
38
+ # @return [Array<Hash>] Array of signing keys (filtered from JWKS)
39
+ def call(options = {})
40
+ # Check if we should invalidate cache due to kid_not_found
41
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - @timeout_sec
42
+ kid = options[:kid]
43
+ OmniauthOpenidFederation::Logger.info("[Jwks::Cache] Invalidating JWK cache. Kid '#{kid}' not found from previous cache")
44
+ @cached_keys = nil
45
+ @jwks_source.reload! if @jwks_source.respond_to?(:reload!)
46
+ end
47
+
48
+ # Return cached keys or fetch fresh
49
+ @cached_keys ||= begin
50
+ @cache_last_update = Time.now.to_i
51
+ jwks = @jwks_source.jwks
52
+
53
+ # Ensure jwks is in the expected format
54
+ keys = if jwks.is_a?(Hash) && (jwks.key?("keys") || jwks.key?(:keys))
55
+ jwks["keys"] || jwks[:keys] || []
56
+ elsif jwks.is_a?(Array)
57
+ jwks
58
+ else
59
+ []
60
+ end
61
+
62
+ # Filter to signing keys only (for JWT verification)
63
+ keys.select { |key| (key[:use] || key["use"]) == "sig" }
64
+ end
65
+ end
66
+
67
+ # Clear the cache
68
+ #
69
+ # @return [void]
70
+ def clear!
71
+ @cached_keys = nil
72
+ @cache_last_update = 0
73
+ end
74
+ end
75
+ end
76
+ end