Class: Safire::Protocols::UdapMetadata

Inherits:
Entity
  • Object
show all
Includes:
URIValidation
Defined in:
lib/safire/protocols/udap_metadata.rb

Overview

UDAP Metadata obtained from the UDAP well-known discovery endpoint. Attributes are defined as per UDAP Security STU2.

All twelve required attributes must be present and non-nil in a conformant discovery response. #valid? checks both field presence and the value-level constraints mandated by STU2.

Constant Summary collapse

REQUIRED_ATTRIBUTES =
%i[
  udap_versions_supported
  udap_profiles_supported
  udap_authorization_extensions_supported
  udap_certifications_supported
  grant_types_supported
  scopes_supported
  token_endpoint
  token_endpoint_auth_methods_supported
  token_endpoint_auth_signing_alg_values_supported
  registration_endpoint
  registration_endpoint_jwt_signing_alg_values_supported
  signed_metadata
].freeze
OPTIONAL_ATTRIBUTES =
%i[
  udap_authorization_extensions_required
  udap_certifications_required
  authorization_endpoint
].freeze
ATTRIBUTES =
(REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
STRING_URL_ATTRIBUTES =
%i[token_endpoint registration_endpoint].freeze
BASE64URL_SEGMENT =
/\A[A-Za-z0-9\-_]+\z/
ARRAY_ATTRIBUTES =
%i[
  udap_versions_supported
  udap_profiles_supported
  udap_authorization_extensions_supported
  udap_certifications_supported
  grant_types_supported
  scopes_supported
  token_endpoint_auth_methods_supported
  token_endpoint_auth_signing_alg_values_supported
  registration_endpoint_jwt_signing_alg_values_supported
  udap_authorization_extensions_required
  udap_certifications_required
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Entity

#to_hash

Constructor Details

#initialize(metadata) ⇒ UdapMetadata

Returns a new instance of UdapMetadata.



90
91
92
# File 'lib/safire/protocols/udap_metadata.rb', line 90

def initialize()
  super(, ATTRIBUTES)
end

Instance Attribute Details

#authorization_endpointString? (readonly)

Returns URL of the server's Authorization Endpoint; conditionally required when #grant_types_supported includes "authorization_code".

Returns:

  • (String, nil)

    URL of the server's Authorization Endpoint; conditionally required when #grant_types_supported includes "authorization_code"



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#grant_types_supportedArray<String>? (readonly)

Returns OAuth2 grant types supported at the token endpoint.

Returns:

  • (Array<String>, nil)

    OAuth2 grant types supported at the token endpoint



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#registration_endpointString? (readonly)

Returns URL of the server's UDAP Dynamic Client Registration Endpoint.

Returns:

  • (String, nil)

    URL of the server's UDAP Dynamic Client Registration Endpoint



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#registration_endpoint_jwt_signing_alg_values_supportedArray<String>? (readonly)

Returns JWT signing algorithms supported for registration requests.

Returns:

  • (Array<String>, nil)

    JWT signing algorithms supported for registration requests



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#scopes_supportedArray<String>? (readonly)

Returns scopes a client may request.

Returns:

  • (Array<String>, nil)

    scopes a client may request



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#signed_metadataString? (readonly)

Returns a signed JWT containing a subset of the server's metadata claims.

Returns:

  • (String, nil)

    a signed JWT containing a subset of the server's metadata claims



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#token_endpointString? (readonly)

Returns URL of the server's OAuth2 Token Endpoint.

Returns:

  • (String, nil)

    URL of the server's OAuth2 Token Endpoint



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#token_endpoint_auth_methods_supportedArray<String>? (readonly)

Returns client authentication methods at the token endpoint; must include "private_key_jwt" per STU2.

Returns:

  • (Array<String>, nil)

    client authentication methods at the token endpoint; must include "private_key_jwt" per STU2



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#token_endpoint_auth_signing_alg_values_supportedArray<String>? (readonly)

Returns JWT signing algorithms supported for client authentication.

Returns:

  • (Array<String>, nil)

    JWT signing algorithms supported for client authentication



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#udap_authorization_extensions_requiredArray<String>? (readonly)

Returns extensions the server requires; values must be a subset of #udap_authorization_extensions_supported; conditionally required when #udap_authorization_extensions_supported is non-empty.

Returns:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#udap_authorization_extensions_supportedArray<String>? (readonly)

Returns UDAP authorization extensions the server supports.

Returns:

  • (Array<String>, nil)

    UDAP authorization extensions the server supports



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#udap_certifications_requiredArray<String>? (readonly)

Returns certifications the server requires; values must be a subset of #udap_certifications_supported; conditionally required when #udap_certifications_supported is non-empty.

Returns:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#udap_certifications_supportedArray<String>? (readonly)

Returns UDAP certifications the server supports.

Returns:

  • (Array<String>, nil)

    UDAP certifications the server supports



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#udap_profiles_supportedArray<String>? (readonly)

Returns UDAP profiles advertised; must include "udap_dcr" and "udap_authn".

Returns:

  • (Array<String>, nil)

    UDAP profiles advertised; must include "udap_dcr" and "udap_authn"



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

#udap_versions_supportedArray<String>? (readonly)

Returns UDAP versions supported; must contain "1" per STU2.

Returns:

  • (Array<String>, nil)

    UDAP versions supported; must contain "1" per STU2



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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
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
207
208
209
210
211
212
213
214
215
216
217
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/safire/protocols/udap_metadata.rb', line 47

class UdapMetadata < Safire::Entity
  include URIValidation

  REQUIRED_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint
    registration_endpoint_jwt_signing_alg_values_supported
    signed_metadata
  ].freeze

  OPTIONAL_ATTRIBUTES = %i[
    udap_authorization_extensions_required
    udap_certifications_required
    authorization_endpoint
  ].freeze

  ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
  STRING_URL_ATTRIBUTES = %i[token_endpoint registration_endpoint].freeze
  BASE64URL_SEGMENT = /\A[A-Za-z0-9\-_]+\z/
  ARRAY_ATTRIBUTES = %i[
    udap_versions_supported
    udap_profiles_supported
    udap_authorization_extensions_supported
    udap_certifications_supported
    grant_types_supported
    scopes_supported
    token_endpoint_auth_methods_supported
    token_endpoint_auth_signing_alg_values_supported
    registration_endpoint_jwt_signing_alg_values_supported
    udap_authorization_extensions_required
    udap_certifications_required
  ].freeze

  attr_reader(*ATTRIBUTES)

  def initialize()
    super(, ATTRIBUTES)
  end

  # Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
  #
  # This is a user-callable helper. Safire performs discovery without automatically
  # asserting server compliance — it is the caller's responsibility to invoke this
  # method when they wish to verify conformance.
  #
  # Checks performed:
  # - All required fields are present (nil? check; empty arrays are valid required values)
  # - All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  # - +udap_versions_supported+ must equal <tt>["1"]</tt> exactly (STU2 fixed value)
  # - +udap_profiles_supported+ includes +"udap_dcr"+ and +"udap_authn"+
  # - +token_endpoint_auth_methods_supported+ must equal <tt>["private_key_jwt"]</tt> exactly (STU2 fixed value)
  # - +scopes_supported+, +grant_types_supported+, and both signing algorithm arrays each have at least one element
  # - +token_endpoint+ and +registration_endpoint+ are absolute HTTPS URLs;
  #   +authorization_endpoint+ is also validated when present
  # - +signed_metadata+ is a compact-JWS string (three base64url-encoded dot-separated segments);
  #   JWT header algorithm (+alg+), required claim presence, and signature are not validated here —
  #   these are deferred to the cryptographic validator (future PR)
  # - endpoint URL checks accept localhost HTTP to support development without TLS
  # - +authorization_endpoint+ present when +authorization_code+ is in +grant_types_supported+
  # - +udap_authz+ present in +udap_profiles_supported+ when +client_credentials+ is in +grant_types_supported+
  # - +authorization_code+ present in +grant_types_supported+ when +refresh_token+ is also present
  # - +udap_authorization_extensions_required+ present when +udap_authorization_extensions_supported+
  #   is non-empty, and its values are a subset of the supported list
  # - +udap_certifications_required+ present when +udap_certifications_supported+ is non-empty,
  #   and its values are a subset of the supported list
  #
  # A warning is logged for each STU2 violation detected.
  #
  # @return [Boolean] true if all checks pass, false if any violation is found
  def valid?
    fields_present = required_fields_present?
    arrays_valid = array_fields_valid?
    return false unless fields_present && arrays_valid

    [
      version_valid?,
      required_profiles_valid?,
      auth_methods_valid?,
      non_empty_arrays_valid?,
      string_values_valid?,
      conditional_presence_valid?,
      required_subset_valid?
    ].all?
  end

  # Profile checks — test profile advertisement only, not whether required fields are present.

  # @return [Boolean] true when the server advertises the +udap_dcr+ profile
  def dynamic_registration_profile? = profile?('udap_dcr')

  # @return [Boolean] true when the server advertises the +udap_authn+ profile
  def jwt_client_auth_profile? = profile?('udap_authn')

  # @return [Boolean] true when the server advertises the +udap_authz+ profile
  def client_authorization_profile? = profile?('udap_authz')

  # @return [Boolean] true when the server advertises the +udap_to+ (Tiered OAuth) profile
  def tiered_oauth_profile? = profile?('udap_to')

  # Capability checks — combine profile advertisement with any additional preconditions.

  # @return [Boolean] true when the server supports UDAP Dynamic Client Registration
  #   (advertises +udap_dcr+ profile and provides a valid +registration_endpoint+)
  def supports_dynamic_registration?
    dynamic_registration_profile? && valid_https_url?(registration_endpoint)
  end

  # @return [Boolean] true when the server supports JWT client authentication
  #   (advertises +udap_authn+ profile and provides a valid +token_endpoint+)
  def supports_jwt_client_auth?
    jwt_client_auth_profile? && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the UDAP client authorization profile
  #   (advertises +udap_authz+ profile, supports the +client_credentials+ grant, and provides
  #   a valid +token_endpoint+)
  def supports_client_authorization?
    client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
  end

  # @return [Boolean] true when the server supports the authorization_code grant type
  def supports_authorization_code?
    grant_type?('authorization_code')
  end

  # @return [Boolean] true when the server supports the refresh_token grant type
  def supports_refresh_token?
    grant_type?('refresh_token')
  end

  # @return [Boolean] true when the server supports Tiered OAuth (+udap_to+ profile)
  def supports_tiered_oauth? = tiered_oauth_profile?

  # @return [Boolean] true when the server provides a signed_metadata value in compact-JWS format
  def supports_signed_metadata?
    .is_a?(String) && compact_jws_format?()
  end

  private

  def profile?(name)
    array_includes?(:udap_profiles_supported, name)
  end

  def grant_type?(name)
    array_includes?(:grant_types_supported, name)
  end

  def array_includes?(attr, value)
    values = public_send(attr)
    values.is_a?(Array) && values.include?(value)
  end

  def array_any?(attr)
    values = public_send(attr)
    values.is_a?(Array) && values.any?
  end

  def array_or_empty(attr)
    values = public_send(attr)
    values.is_a?(Array) ? values : []
  end

  def warn_noncompliance(message)
    Safire.logger.warn("UDAP metadata non-compliance: #{message}")
  end

  def required_fields_present?
    missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
    missing.each { |attr| warn_noncompliance("required field '#{attr}' is missing") }
    missing.empty?
  end

  def array_fields_valid?
    invalid = ARRAY_ATTRIBUTES.reject do |attr|
      value = public_send(attr)
      value.nil? || (value.is_a?(Array) && value.all?(String))
    end
    invalid.each { |attr| warn_noncompliance("field '#{attr}' must be an array of strings") }
    invalid.empty?
  end

  def version_valid?
    return true if udap_versions_supported == ['1']

    warn_noncompliance("udap_versions_supported must be the fixed array ['1'] (UDAP Security STU2 fixed value)")
    false
  end

  def required_profiles_valid?
    valid = true
    %w[udap_dcr udap_authn].each do |profile|
      next if profile?(profile)

      warn_noncompliance("'#{profile}' is missing from udap_profiles_supported (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def auth_methods_valid?
    return true if token_endpoint_auth_methods_supported == ['private_key_jwt']

    warn_noncompliance(
      "token_endpoint_auth_methods_supported must be the fixed array ['private_key_jwt'] " \
      '(required by UDAP Security STU2)'
    )
    false
  end

  def non_empty_arrays_valid?
    valid = true
    %i[
      scopes_supported
      grant_types_supported
      token_endpoint_auth_signing_alg_values_supported
      registration_endpoint_jwt_signing_alg_values_supported
    ].each do |attr|
      next if array_any?(attr)

      warn_noncompliance("#{attr} must be a non-empty array (required by UDAP Security STU2)")
      valid = false
    end
    valid
  end

  def string_values_valid?
    [url_fields_valid?, ].all?
  end

  def url_fields_valid?
    attrs = STRING_URL_ATTRIBUTES.dup
    attrs << :authorization_endpoint unless authorization_endpoint.nil?
    invalid = attrs.reject { |attr| valid_https_url?(public_send(attr)) }
    invalid.each do |attr|
      warn_noncompliance("#{attr} must be an absolute HTTPS URL (localhost HTTP is accepted for development)")
    end
    invalid.empty?
  end

  def 
    return true if .is_a?(String) && compact_jws_format?()

    warn_noncompliance('signed_metadata must be a compact-JWS string (header.payload.signature)')
    false
  end

  def compact_jws_format?(value)
    parts = value.split('.', -1)
    parts.length == 3 && parts.all? { |p| BASE64URL_SEGMENT.match?(p) }
  end

  def valid_https_url?(value)
    value.is_a?(String) && classify_uri(value).nil?
  end

  def conditional_presence_valid?
    [
      authorization_endpoint_conditionally_present?,
      extensions_required_conditionally_present?,
      certifications_required_conditionally_present?,
      authz_profile_conditionally_present?,
      refresh_token_requires_authorization_code?
    ].all?
  end

  def authorization_endpoint_conditionally_present?
    return true unless grant_type?('authorization_code')
    return true unless authorization_endpoint.nil?

    warn_noncompliance('authorization_endpoint is required when authorization_code grant type is supported')
    false
  end

  def extensions_required_conditionally_present?
    required_conditionally_present?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_conditionally_present?
    required_conditionally_present?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_conditionally_present?(required_attr, supported_attr)
    return true unless array_any?(supported_attr)
    return true unless public_send(required_attr).nil?

    warn_noncompliance("#{required_attr} must be present when #{supported_attr} is non-empty")
    false
  end

  def authz_profile_conditionally_present?
    return true unless grant_type?('client_credentials')
    return true if profile?('udap_authz')

    warn_noncompliance(
      "'udap_authz' is required in udap_profiles_supported when client_credentials grant type is supported"
    )
    false
  end

  def refresh_token_requires_authorization_code?
    return true unless grant_type?('refresh_token')
    return true if grant_type?('authorization_code')

    warn_noncompliance(
      "'refresh_token' grant type requires 'authorization_code' to also be in grant_types_supported"
    )
    false
  end

  def required_subset_valid?
    [
      extensions_required_subset_valid?,
      certifications_required_subset_valid?
    ].all?
  end

  def extensions_required_subset_valid?
    required_subset_valid_for?(
      :udap_authorization_extensions_required,
      :udap_authorization_extensions_supported
    )
  end

  def certifications_required_subset_valid?
    required_subset_valid_for?(
      :udap_certifications_required,
      :udap_certifications_supported
    )
  end

  def required_subset_valid_for?(required_attr, supported_attr)
    return true unless array_any?(required_attr)

    unsupported = array_or_empty(required_attr) - array_or_empty(supported_attr)
    return true unless unsupported.any?

    warn_noncompliance("#{required_attr} contains values not in #{supported_attr}: #{unsupported.join(', ')}")
    false
  end
end

Instance Method Details

#client_authorization_profile?Boolean

Returns true when the server advertises the udap_authz profile.

Returns:

  • (Boolean)

    true when the server advertises the udap_authz profile



149
# File 'lib/safire/protocols/udap_metadata.rb', line 149

def client_authorization_profile? = profile?('udap_authz')

#dynamic_registration_profile?Boolean

Returns true when the server advertises the udap_dcr profile.

Returns:

  • (Boolean)

    true when the server advertises the udap_dcr profile



143
# File 'lib/safire/protocols/udap_metadata.rb', line 143

def dynamic_registration_profile? = profile?('udap_dcr')

#jwt_client_auth_profile?Boolean

Returns true when the server advertises the udap_authn profile.

Returns:

  • (Boolean)

    true when the server advertises the udap_authn profile



146
# File 'lib/safire/protocols/udap_metadata.rb', line 146

def jwt_client_auth_profile? = profile?('udap_authn')

#supports_authorization_code?Boolean

Returns true when the server supports the authorization_code grant type.

Returns:

  • (Boolean)

    true when the server supports the authorization_code grant type



176
177
178
# File 'lib/safire/protocols/udap_metadata.rb', line 176

def supports_authorization_code?
  grant_type?('authorization_code')
end

#supports_client_authorization?Boolean

Returns true when the server supports the UDAP client authorization profile (advertises udap_authz profile, supports the client_credentials grant, and provides a valid token_endpoint).

Returns:

  • (Boolean)

    true when the server supports the UDAP client authorization profile (advertises udap_authz profile, supports the client_credentials grant, and provides a valid token_endpoint)



171
172
173
# File 'lib/safire/protocols/udap_metadata.rb', line 171

def supports_client_authorization?
  client_authorization_profile? && grant_type?('client_credentials') && valid_https_url?(token_endpoint)
end

#supports_dynamic_registration?Boolean

Returns true when the server supports UDAP Dynamic Client Registration (advertises udap_dcr profile and provides a valid registration_endpoint).

Returns:

  • (Boolean)

    true when the server supports UDAP Dynamic Client Registration (advertises udap_dcr profile and provides a valid registration_endpoint)



158
159
160
# File 'lib/safire/protocols/udap_metadata.rb', line 158

def supports_dynamic_registration?
  dynamic_registration_profile? && valid_https_url?(registration_endpoint)
end

#supports_jwt_client_auth?Boolean

Returns true when the server supports JWT client authentication (advertises udap_authn profile and provides a valid token_endpoint).

Returns:

  • (Boolean)

    true when the server supports JWT client authentication (advertises udap_authn profile and provides a valid token_endpoint)



164
165
166
# File 'lib/safire/protocols/udap_metadata.rb', line 164

def supports_jwt_client_auth?
  jwt_client_auth_profile? && valid_https_url?(token_endpoint)
end

#supports_refresh_token?Boolean

Returns true when the server supports the refresh_token grant type.

Returns:

  • (Boolean)

    true when the server supports the refresh_token grant type



181
182
183
# File 'lib/safire/protocols/udap_metadata.rb', line 181

def supports_refresh_token?
  grant_type?('refresh_token')
end

#supports_signed_metadata?Boolean

Returns true when the server provides a signed_metadata value in compact-JWS format.

Returns:

  • (Boolean)

    true when the server provides a signed_metadata value in compact-JWS format



189
190
191
# File 'lib/safire/protocols/udap_metadata.rb', line 189

def supports_signed_metadata?
  .is_a?(String) && compact_jws_format?()
end

#supports_tiered_oauth?Boolean

Returns true when the server supports Tiered OAuth (+udap_to+ profile).

Returns:

  • (Boolean)

    true when the server supports Tiered OAuth (+udap_to+ profile)



186
# File 'lib/safire/protocols/udap_metadata.rb', line 186

def supports_tiered_oauth? = tiered_oauth_profile?

#tiered_oauth_profile?Boolean

Returns true when the server advertises the udap_to (Tiered OAuth) profile.

Returns:

  • (Boolean)

    true when the server advertises the udap_to (Tiered OAuth) profile



152
# File 'lib/safire/protocols/udap_metadata.rb', line 152

def tiered_oauth_profile? = profile?('udap_to')

#valid?Boolean

Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.

This is a user-callable helper. Safire performs discovery without automatically asserting server compliance — it is the caller's responsibility to invoke this method when they wish to verify conformance.

Checks performed:

  • All required fields are present (nil? check; empty arrays are valid required values)
  • All array-valued fields are arrays of strings before any profile/grant/subset checks are performed
  • udap_versions_supported must equal ["1"] exactly (STU2 fixed value)
  • udap_profiles_supported includes "udap_dcr" and "udap_authn"
  • token_endpoint_auth_methods_supported must equal ["private_key_jwt"] exactly (STU2 fixed value)
  • scopes_supported, grant_types_supported, and both signing algorithm arrays each have at least one element
  • token_endpoint and registration_endpoint are absolute HTTPS URLs; authorization_endpoint is also validated when present
  • signed_metadata is a compact-JWS string (three base64url-encoded dot-separated segments); JWT header algorithm (+alg+), required claim presence, and signature are not validated here — these are deferred to the cryptographic validator (future PR)
  • endpoint URL checks accept localhost HTTP to support development without TLS
  • authorization_endpoint present when authorization_code is in grant_types_supported
  • udap_authz present in udap_profiles_supported when client_credentials is in grant_types_supported
  • authorization_code present in grant_types_supported when refresh_token is also present
  • udap_authorization_extensions_required present when udap_authorization_extensions_supported is non-empty, and its values are a subset of the supported list
  • udap_certifications_required present when udap_certifications_supported is non-empty, and its values are a subset of the supported list

A warning is logged for each STU2 violation detected.

Returns:

  • (Boolean)

    true if all checks pass, false if any violation is found



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/safire/protocols/udap_metadata.rb', line 124

def valid?
  fields_present = required_fields_present?
  arrays_valid = array_fields_valid?
  return false unless fields_present && arrays_valid

  [
    version_valid?,
    required_profiles_valid?,
    auth_methods_valid?,
    non_empty_arrays_valid?,
    string_values_valid?,
    conditional_presence_valid?,
    required_subset_valid?
  ].all?
end