Class: OmniauthOpenidFederation::Federation::SignedJWKS
- Inherits:
-
Object
- Object
- OmniauthOpenidFederation::Federation::SignedJWKS
- Defined in:
- lib/omniauth_openid_federation/federation/signed_jwks.rb,
sig/federation.rbs
Overview
Signed JWKS implementation for OpenID Federation 1.0
Constant Summary collapse
- FetchError =
Compatibility aliases for backward compatibility
OmniauthOpenidFederation::FetchError
- ValidationError =
OmniauthOpenidFederation::ValidationError
Class Method Summary collapse
-
.fetch!(signed_jwks_uri, entity_jwks, cache_key: nil, cache_ttl: nil, force_refresh: false) ⇒ Hash
Fetch and validate signed JWKS.
Instance Method Summary collapse
-
#fetch_and_validate ⇒ Hash
Fetch and validate signed JWKS.
-
#initialize(signed_jwks_uri, entity_jwks) ⇒ SignedJWKS
constructor
Initialize signed JWKS fetcher.
Constructor Details
#initialize(signed_jwks_uri, entity_jwks) ⇒ SignedJWKS
Initialize signed JWKS fetcher
118 119 120 121 |
# File 'lib/omniauth_openid_federation/federation/signed_jwks.rb', line 118 def initialize(signed_jwks_uri, entity_jwks) @signed_jwks_uri = signed_jwks_uri @entity_jwks = entity_jwks end |
Class Method Details
.fetch!(signed_jwks_uri, entity_jwks, cache_key: nil, cache_ttl: nil, force_refresh: false) ⇒ Hash
Fetch and validate signed JWKS
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/omniauth_openid_federation/federation/signed_jwks.rb', line 62 def self.fetch!(signed_jwks_uri, entity_jwks, cache_key: nil, cache_ttl: nil, force_refresh: false) cache_key ||= OmniauthOpenidFederation::Cache.key_for_signed_jwks(signed_jwks_uri) config = OmniauthOpenidFederation::Configuration.config cache_ttl ||= config.cache_ttl rotate_on_errors = config.rotate_on_errors # Use cache adapter if available, otherwise fetch directly if CacheAdapter.available? if force_refresh # Force refresh: clear cache and fetch fresh CacheAdapter.delete(cache_key) end if cache_ttl.nil? # Manual rotation: cache forever, only rotate on errors if rotate_on_errors is enabled begin CacheAdapter.fetch(cache_key, expires_in: nil) do new(signed_jwks_uri, entity_jwks).fetch_and_validate end rescue KeyRelatedError, KeyRelatedValidationError => e # Rotate on key-related errors if configured if rotate_on_errors OmniauthOpenidFederation::Logger.warn("[SignedJWKS] Key-related error detected, rotating cache: #{e.}") CacheAdapter.delete(cache_key) new(signed_jwks_uri, entity_jwks).fetch_and_validate else raise end end else # TTL-based cache: expires after cache_ttl seconds # Rotate on errors if configured begin CacheAdapter.fetch(cache_key, expires_in: cache_ttl) do new(signed_jwks_uri, entity_jwks).fetch_and_validate end rescue KeyRelatedError, KeyRelatedValidationError => e # Rotate on key-related errors if configured if rotate_on_errors OmniauthOpenidFederation::Logger.warn("[SignedJWKS] Key-related error detected, rotating cache: #{e.}") CacheAdapter.delete(cache_key) new(signed_jwks_uri, entity_jwks).fetch_and_validate else raise end end end else new(signed_jwks_uri, entity_jwks).fetch_and_validate end end |
Instance Method Details
#fetch_and_validate ⇒ Hash
Fetch and validate signed JWKS
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/omniauth_openid_federation/federation/signed_jwks.rb', line 128 def fetch_and_validate # Rate limiting to prevent DoS unless RateLimiter.allow?(@signed_jwks_uri) raise FetchError, "Rate limit exceeded for signed JWKS fetching" end # Fetch signed JWKS using HttpClient with retry logic begin response = HttpClient.get(@signed_jwks_uri) rescue OmniauthOpenidFederation::NetworkError => e sanitized_uri = Utils.sanitize_uri(@signed_jwks_uri) OmniauthOpenidFederation::Logger.error("[SignedJWKS] Failed to fetch signed JWKS from #{sanitized_uri}") raise FetchError, "Failed to fetch signed JWKS: #{e.}", e.backtrace end unless response.status.success? error_msg = "Failed to fetch signed JWKS: HTTP #{response.status}" OmniauthOpenidFederation::Logger.error("[SignedJWKS] #{error_msg}") # If it's a key-related error (401, 403, 404), this might indicate key rotation if Constants::KEY_ROTATION_HTTP_CODES.include?(response.status.code) raise KeyRelatedError, error_msg else raise FetchError, error_msg end end signed_jwks_jwt = response.body.to_s # Validate it's a JWT (must have exactly 3 parts: header.payload.signature) unless Utils.valid_jwt_format?(signed_jwks_jwt) raise ValidationError, "Signed JWKS is not in JWT format" end # Convert entity JWKS to format expected by JWT gem jwks_hash = Jwks::Normalizer.to_jwks_hash(@entity_jwks) # Decode and validate signed JWKS begin OmniauthOpenidFederation::Logger.debug("[SignedJWKS] Validating signed JWKS signature") decoded = ::JWT.decode( signed_jwks_jwt, nil, true, {algorithms: ["RS256"], jwks: jwks_hash} ) # Extract JWKS from decoded JWT payload # The JWT payload can be in two formats: # 1. OpenID Federation format: { iss, sub, iat, exp, jwks: { keys: [...] } } # 2. Legacy format: { keys: [...] } (direct JWKS) full_payload = decoded.first # Check if payload has 'jwks' field (OpenID Federation format) if full_payload.key?("jwks") || full_payload.key?(:jwks) jwks_payload = full_payload["jwks"] || full_payload[:jwks] elsif full_payload.key?("keys") || full_payload.key?(:keys) # Legacy format: payload is the JWKS directly jwks_payload = full_payload else error_msg = "Signed JWKS payload does not contain 'jwks' or 'keys' field" OmniauthOpenidFederation::Logger.error("[SignedJWKS] #{error_msg}") raise ValidationError, error_msg end OmniauthOpenidFederation::Logger.debug("[SignedJWKS] Successfully validated signed JWKS") # Ensure it's a HashWithIndifferentAccess if available Utils.to_indifferent_hash(jwks_payload) rescue JWT::VerificationError => e # More specific exception must be rescued first error_msg = "Signed JWKS signature validation failed: #{e.class} - #{e.}" OmniauthOpenidFederation::Logger.error("[SignedJWKS] #{error_msg}") raise KeyRelatedValidationError, error_msg rescue JWT::DecodeError => e error_msg = "Failed to decode signed JWKS: #{e.class} - #{e.}" OmniauthOpenidFederation::Logger.error("[SignedJWKS] #{error_msg}") raise KeyRelatedValidationError, error_msg end end |