Class: OmniauthOpenidFederation::Federation::EntityStatement

Inherits:
Object
  • Object
show all
Defined in:
lib/omniauth_openid_federation/federation/entity_statement.rb

Overview

Entity Statement implementation for OpenID Federation 1.0

Examples:

Fetch and validate an entity statement from full URL

statement = EntityStatement.fetch!(
  "https://provider.example.com/.well-known/openid-federation",
  fingerprint: "expected-fingerprint-hash"
)
 = statement.parse

Fetch and validate an entity statement from issuer and endpoint

statement = EntityStatement.fetch_from_issuer!(
  "https://provider.example.com",
  entity_statement_endpoint: "/.well-known/openid-federation",
  fingerprint: "expected-fingerprint-hash"
)
 = statement.parse

Constant Summary collapse

FetchError =

Compatibility aliases for backward compatibility

OmniauthOpenidFederation::FetchError
ValidationError =
OmniauthOpenidFederation::ValidationError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entity_statement_content, fingerprint: nil) ⇒ EntityStatement

Fetch entity statement from URL

Parameters:

  • url (String)

    The URL to fetch the entity statement from

  • fingerprint (String, nil) (defaults to: nil)

    Expected SHA-256 fingerprint for validation

  • previous_statement (String, EntityStatement, Hash, nil)

    Previous statement for validation

  • timeout (Integer)

    HTTP request timeout in seconds (default: 10)

Raises:



63
64
65
66
67
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 63

def initialize(entity_statement_content, fingerprint: nil)
  @entity_statement = entity_statement_content
  @fingerprint = fingerprint || calculate_fingerprint
  @metadata = nil
end

Instance Attribute Details

#entity_statementObject (readonly)

Returns the value of attribute entity_statement.



51
52
53
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 51

def entity_statement
  @entity_statement
end

#fingerprintObject (readonly)

Returns the value of attribute fingerprint.



51
52
53
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 51

def fingerprint
  @fingerprint
end

#metadataObject (readonly)

Returns the value of attribute metadata.



51
52
53
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 51

def 
  @metadata
end

Class Method Details

.fetch!(url, fingerprint: nil, previous_statement: nil, timeout: 10) ⇒ EntityStatement

Fetch entity statement from URL

Parameters:

  • url (String)

    The URL to fetch the entity statement from

  • fingerprint (String, nil) (defaults to: nil)

    Expected SHA-256 fingerprint for validation

  • previous_statement (String, EntityStatement, Hash, nil) (defaults to: nil)

    Previous statement for validation

  • timeout (Integer) (defaults to: 10)

    HTTP request timeout in seconds (default: 10)

Returns:

Raises:



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 96

def self.fetch!(url, fingerprint: nil, previous_statement: nil, timeout: 10)
  # Use HttpClient for retry logic and configurable SSL verification
  # Note: HttpClient uses HTTP gem, but entity statements might need Net::HTTP
  # For now, we'll use a simple HTTP.get approach with HttpClient's retry logic
  begin
    # Convert URL to URI for HttpClient
    response = HttpClient.get(url, timeout: timeout)
  rescue OmniauthOpenidFederation::NetworkError => e
    OmniauthOpenidFederation::Logger.error("[EntityStatement] Failed to fetch entity statement: #{e.message}")
    raise FetchError, "Failed to fetch entity statement from #{url}: #{e.message}", e.backtrace
  end

  unless response.status.success?
    error_msg = "Failed to fetch entity statement from #{url}: HTTP #{response.status}"
    OmniauthOpenidFederation::Logger.error("[EntityStatement] #{error_msg}")
    raise FetchError, error_msg
  end

  # HTTP gem returns body as StringIO or similar, convert to string
  entity_statement = response.body.to_s

  instance = new(entity_statement, fingerprint: nil) # Don't set fingerprint in constructor

  # Validate using full OpenID Federation validation (includes signature validation)
  # This is required for OpenID Federation compliance
  begin
    EntityStatementParser.parse(entity_statement, validate_signature: true, validate_full: true)
    OmniauthOpenidFederation::Logger.debug("[EntityStatement] Full validation successful")
  rescue SignatureError, ValidationError => e
    error_msg = "Entity statement validation failed: #{e.message}"
    OmniauthOpenidFederation::Logger.error("[EntityStatement] #{error_msg}")
    # Instrument entity statement validation failure
    OmniauthOpenidFederation::Instrumentation.notify_entity_statement_validation_failed(
      entity_statement_url: url,
      validation_step: "full_validation",
      error_message: e.message,
      error_class: e.class.name
    )
    raise ValidationError, error_msg, e.backtrace
  end

  # Validate if fingerprint provided
  if fingerprint
    calculated_fingerprint = instance.calculate_fingerprint
    unless instance.validate_fingerprint(fingerprint)
      error_msg = "Entity statement fingerprint mismatch. Expected: #{fingerprint}, Got: #{calculated_fingerprint}"
      OmniauthOpenidFederation::Logger.error("[EntityStatement] #{error_msg}")
      # Instrument fingerprint mismatch
      OmniauthOpenidFederation::Instrumentation.notify_fingerprint_mismatch(
        entity_statement_url: url,
        expected_fingerprint: fingerprint,
        calculated_fingerprint: calculated_fingerprint
      )
      raise ValidationError, error_msg
    end
    OmniauthOpenidFederation::Logger.debug("[EntityStatement] Fingerprint validation successful: #{fingerprint}")
  end

  # Validate against previous statement if provided
  if previous_statement
    unless instance.validate_against_previous(previous_statement)
      error_msg = "Entity statement validation against previous statement failed"
      OmniauthOpenidFederation::Logger.error("[EntityStatement] #{error_msg}")
      raise ValidationError, error_msg
    end
    OmniauthOpenidFederation::Logger.debug("[EntityStatement] Previous statement validation successful")
  end

  instance
end

.fetch_from_issuer!(issuer_uri, entity_statement_endpoint: nil, fingerprint: nil, previous_statement: nil, timeout: 10) ⇒ EntityStatement

Fetch entity statement from issuer and endpoint path

Parameters:

  • issuer_uri (String, URI)

    Issuer URI (e.g., “provider.example.com”)

  • entity_statement_endpoint (String, nil) (defaults to: nil)

    Entity statement endpoint path (defaults to “/.well-known/openid-federation”)

  • fingerprint (String, nil) (defaults to: nil)

    Expected SHA-256 fingerprint for validation

  • previous_statement (String, EntityStatement, Hash, nil) (defaults to: nil)

    Previous statement for validation

  • timeout (Integer) (defaults to: 10)

    HTTP request timeout in seconds (default: 10)

Returns:

Raises:



79
80
81
82
83
84
85
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 79

def self.fetch_from_issuer!(issuer_uri, entity_statement_endpoint: nil, fingerprint: nil, previous_statement: nil, timeout: 10)
  url = OmniauthOpenidFederation::Utils.build_entity_statement_url(
    issuer_uri,
    entity_statement_endpoint: entity_statement_endpoint
  )
  fetch!(url, fingerprint: fingerprint, previous_statement: previous_statement, timeout: timeout)
end

Instance Method Details

#calculate_fingerprintString

Calculate SHA-256 fingerprint of the entity statement

Returns:

  • (String)

    The lowercase hexadecimal fingerprint



170
171
172
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 170

def calculate_fingerprint
  Digest::SHA256.hexdigest(entity_statement).downcase
end

#decode_payloadHash

Decode and return the JWT payload

Returns:

  • (Hash)

    The decoded JWT payload



309
310
311
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 309

def decode_payload
  decode_jwt_payload(entity_statement)
end

#parseHash

Parse entity statement and extract metadata

Returns:

  • (Hash)

    Hash containing issuer, subject, expiration, JWKS, and provider metadata



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 218

def parse
  return @metadata if @metadata

  payload = decode_payload

  # Extract provider metadata
   = payload.fetch("metadata", {})
  .fetch("openid_provider", {})

  # Extract entity JWKS - ensure it's a hash with keys array
  entity_jwks = payload.fetch("jwks", {})
  # Normalize to ensure it has :keys or "keys" key
  if entity_jwks.nil? || !entity_jwks.is_a?(Hash)
    entity_jwks = {keys: []}
  elsif !entity_jwks.key?(:keys) && !entity_jwks.key?("keys")
    entity_jwks = {keys: []}
  end

  # Extract all entity types from metadata
   = payload.fetch("metadata", {})
   = .fetch("openid_provider", {})
   = .fetch("openid_relying_party", {})

  @metadata = {
    issuer: payload["iss"],
    sub: payload["sub"],
    exp: payload["exp"],
    iat: payload["iat"],
    jwks: entity_jwks,
    metadata: {},
    # Advanced claims (Entity Configuration specific)
    authority_hints: payload["authority_hints"] || payload[:authority_hints],
    trust_marks: payload["trust_marks"] || payload[:trust_marks],
    trust_mark_issuers: payload["trust_mark_issuers"] || payload[:trust_mark_issuers],
    trust_mark_owners: payload["trust_mark_owners"] || payload[:trust_mark_owners],
    # Advanced claims (Subordinate Statement specific)
    metadata_policy: payload["metadata_policy"] || payload[:metadata_policy],
    metadata_policy_crit: payload["metadata_policy_crit"] || payload[:metadata_policy_crit],
    constraints: payload["constraints"] || payload[:constraints],
    source_endpoint: payload["source_endpoint"] || payload[:source_endpoint],
    # Other claims
    crit: payload["crit"] || payload[:crit],
    # Determine statement type
    is_entity_configuration: (payload["iss"] == payload["sub"]),
    is_subordinate_statement: (payload["iss"] != payload["sub"])
  }

  # Extract OpenID Provider metadata if present
  if .any?
    @metadata[:metadata][:openid_provider] = {
      issuer: ["issuer"],
      authorization_endpoint: ["authorization_endpoint"],
      token_endpoint: ["token_endpoint"],
      userinfo_endpoint: ["userinfo_endpoint"],
      jwks_uri: ["jwks_uri"],
      signed_jwks_uri: ["signed_jwks_uri"],
      end_session_endpoint: ["end_session_endpoint"],
      client_registration_types_supported: ["client_registration_types_supported"],
      federation_registration_endpoint: ["federation_registration_endpoint"]
    }
  end

  # Extract OpenID Relying Party metadata if present
  if .any?
    @metadata[:metadata][:openid_relying_party] = {
      application_type: ["application_type"],
      redirect_uris: ["redirect_uris"],
      client_registration_types: ["client_registration_types"],
      signed_jwks_uri: ["signed_jwks_uri"],
      jwks_uri: ["jwks_uri"],
      organization_name: ["organization_name"],
      logo_uri: ["logo_uri"],
      grant_types: ["grant_types"],
      response_types: ["response_types"],
      scope: ["scope"]
    }
  end

  @metadata
end

#save_to_file(file_path) ⇒ Object

Save entity statement to file

Parameters:

  • file_path (String)

    Path to save the entity statement



302
303
304
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 302

def save_to_file(file_path)
  File.write(file_path, entity_statement)
end

#validate_against_previous(previous_statement) ⇒ Boolean

Validate against a previous entity statement

Parameters:

  • previous_statement (String, EntityStatement, Hash)

    The previous statement to validate against

Returns:

  • (Boolean)

    true if validation passes



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 188

def validate_against_previous(previous_statement)
  # Decode current statement
  current_payload = decode_payload

  # Handle different input types
  previous_payload = if previous_statement.is_a?(String)
    decode_jwt_payload(previous_statement)
  elsif previous_statement.instance_of?(::OmniauthOpenidFederation::Federation::EntityStatement)
    # If it's an EntityStatement instance, decode its payload
    previous_statement.decode_payload
  else
    previous_statement
  end

  # Check if issuer matches
  return false unless current_payload["iss"] == previous_payload["iss"]

  # Check if this is a valid update (e.g., exp time is later)
  current_exp = current_payload["exp"]
  previous_exp = previous_payload["exp"]

  return false if current_exp && previous_exp && current_exp < previous_exp

  # Additional validation can be added here (e.g., check authority_hints)
  true
end

#validate_fingerprint(expected_fingerprint) ⇒ Boolean

Validate fingerprint against expected value

Parameters:

  • expected_fingerprint (String)

    The expected fingerprint

Returns:

  • (Boolean)

    true if fingerprints match



178
179
180
181
182
# File 'lib/omniauth_openid_federation/federation/entity_statement.rb', line 178

def validate_fingerprint(expected_fingerprint)
  calculated = fingerprint.downcase
  expected = expected_fingerprint.to_s.downcase
  calculated == expected
end