Class: Safire::Protocols::UdapMetadata
- 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
-
#authorization_endpoint ⇒ String?
readonly
URL of the server's Authorization Endpoint; conditionally required when #grant_types_supported includes
"authorization_code". -
#grant_types_supported ⇒ Array<String>?
readonly
OAuth2 grant types supported at the token endpoint.
-
#registration_endpoint ⇒ String?
readonly
URL of the server's UDAP Dynamic Client Registration Endpoint.
-
#registration_endpoint_jwt_signing_alg_values_supported ⇒ Array<String>?
readonly
JWT signing algorithms supported for registration requests.
-
#scopes_supported ⇒ Array<String>?
readonly
Scopes a client may request.
-
#signed_metadata ⇒ String?
readonly
A signed JWT containing a subset of the server's metadata claims.
-
#token_endpoint ⇒ String?
readonly
URL of the server's OAuth2 Token Endpoint.
-
#token_endpoint_auth_methods_supported ⇒ Array<String>?
readonly
Client authentication methods at the token endpoint; must include
"private_key_jwt"per STU2. -
#token_endpoint_auth_signing_alg_values_supported ⇒ Array<String>?
readonly
JWT signing algorithms supported for client authentication.
-
#udap_authorization_extensions_required ⇒ Array<String>?
readonly
Extensions the server requires; values must be a subset of #udap_authorization_extensions_supported; conditionally required when #udap_authorization_extensions_supported is non-empty.
-
#udap_authorization_extensions_supported ⇒ Array<String>?
readonly
UDAP authorization extensions the server supports.
-
#udap_certifications_required ⇒ Array<String>?
readonly
Certifications the server requires; values must be a subset of #udap_certifications_supported; conditionally required when #udap_certifications_supported is non-empty.
-
#udap_certifications_supported ⇒ Array<String>?
readonly
UDAP certifications the server supports.
-
#udap_profiles_supported ⇒ Array<String>?
readonly
UDAP profiles advertised; must include
"udap_dcr"and"udap_authn". -
#udap_versions_supported ⇒ Array<String>?
readonly
UDAP versions supported; must contain
"1"per STU2.
Instance Method Summary collapse
-
#client_authorization_profile? ⇒ Boolean
True when the server advertises the
udap_authzprofile. -
#dynamic_registration_profile? ⇒ Boolean
True when the server advertises the
udap_dcrprofile. -
#initialize(metadata) ⇒ UdapMetadata
constructor
A new instance of UdapMetadata.
-
#jwt_client_auth_profile? ⇒ Boolean
True when the server advertises the
udap_authnprofile. -
#supports_authorization_code? ⇒ Boolean
True when the server supports the authorization_code grant type.
-
#supports_client_authorization? ⇒ Boolean
True when the server supports the UDAP client authorization profile (advertises
udap_authzprofile, supports theclient_credentialsgrant, and provides a validtoken_endpoint). -
#supports_dynamic_registration? ⇒ Boolean
True when the server supports UDAP Dynamic Client Registration (advertises
udap_dcrprofile and provides a validregistration_endpoint). -
#supports_jwt_client_auth? ⇒ Boolean
True when the server supports JWT client authentication (advertises
udap_authnprofile and provides a validtoken_endpoint). -
#supports_refresh_token? ⇒ Boolean
True when the server supports the refresh_token grant type.
-
#supports_signed_metadata? ⇒ Boolean
True when the server provides a signed_metadata value in compact-JWS format.
-
#supports_tiered_oauth? ⇒ Boolean
True when the server supports Tiered OAuth (+udap_to+ profile).
-
#tiered_oauth_profile? ⇒ Boolean
True when the server advertises the
udap_to(Tiered OAuth) profile. -
#valid? ⇒ Boolean
Checks whether the server's UDAP metadata is valid according to UDAP Security STU2.
Methods inherited from Entity
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_endpoint ⇒ String? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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 ⇒ String? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_metadata ⇒ String? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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 ⇒ String? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_required ⇒ Array<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.
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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_required ⇒ Array<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.
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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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_supported ⇒ Array<String>? (readonly)
Returns 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 = 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 && grant_type?('client_credentials') && valid_https_url?(token_endpoint) end # @return [Boolean] true when the server supports the authorization_code grant type def 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 .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() Safire.logger.warn("UDAP metadata non-compliance: #{}") 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 .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? [ , extensions_required_conditionally_present?, certifications_required_conditionally_present?, authz_profile_conditionally_present?, ].all? end def return true unless grant_type?('authorization_code') return true unless .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 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.
149 |
# File 'lib/safire/protocols/udap_metadata.rb', line 149 def = profile?('udap_authz') |
#dynamic_registration_profile? ⇒ Boolean
Returns 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.
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.
176 177 178 |
# File 'lib/safire/protocols/udap_metadata.rb', line 176 def 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).
171 172 173 |
# File 'lib/safire/protocols/udap_metadata.rb', line 171 def && 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).
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).
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.
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.
189 190 191 |
# File 'lib/safire/protocols/udap_metadata.rb', line 189 def .is_a?(String) && compact_jws_format?() end |
#supports_tiered_oauth? ⇒ Boolean
Returns 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.
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_supportedmust equal ["1"] exactly (STU2 fixed value)udap_profiles_supportedincludes"udap_dcr"and"udap_authn"token_endpoint_auth_methods_supportedmust equal ["private_key_jwt"] exactly (STU2 fixed value)scopes_supported,grant_types_supported, and both signing algorithm arrays each have at least one elementtoken_endpointandregistration_endpointare absolute HTTPS URLs;authorization_endpointis also validated when presentsigned_metadatais 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_endpointpresent whenauthorization_codeis ingrant_types_supportedudap_authzpresent inudap_profiles_supportedwhenclient_credentialsis ingrant_types_supportedauthorization_codepresent ingrant_types_supportedwhenrefresh_tokenis also presentudap_authorization_extensions_requiredpresent whenudap_authorization_extensions_supportedis non-empty, and its values are a subset of the supported listudap_certifications_requiredpresent whenudap_certifications_supportedis non-empty, and its values are a subset of the supported list
A warning is logged for each STU2 violation detected.
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 |